From 019473dec271597b184e36eab79833aaddfb876d Mon Sep 17 00:00:00 2001 From: Hundrec Date: Fri, 4 Apr 2025 18:32:03 -0600 Subject: [PATCH 01/40] Remove 0.6.0 Changelog entries that weren't implemented + polish --- CHANGELOG.md | 48 ++++++++++++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f911d413..f30cef15c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,20 +4,34 @@ All notable changes will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + ## [0.6.2] - 2025-03-31 + ### Added -- 0.6 credits list updated + +- Updated the 0.6 credits list + ### Fixed -- Additional shader fix for Stress Pico crashing at the end (was the same issue as Senpai Pico shader error, just in a different shaderfile) + +- Additional shader fix for Stress (Pico Mix) crashing at the end (was the same issue as Senpai Pico shader error, just in a different shaderfile) + + ## [0.6.1] - 2025-03-31 + ### Fixed + - Hopefully Senpai Pico/Erect mix shader isn't brokey - NG API encryption key was added proper, so medals + leaderboards posting should work + + ## [0.6.0] - 2025-03-31 The Pit Stop 2 update! + ### Added + - Added six (!) new playable songs! Check them out in the Freeplay menu for their respective characters. - Cocoa (Pico Mix) - Senpai (Pico Mix) @@ -38,7 +52,6 @@ The Pit Stop 2 update! - The new sticker system isn't fully available to mods yet, but we're working on it! - New option in the Preferences menu: Strumline Backgrounds! - Options in the Preferences menu now display an on-screen description when selected. -- Added a little easter egg to Pico's Good Results Screen animation. - The HOME and END keys now jump to the top and bottom of the Freeplay song list, respectively. ([bb974c2](https://github.com/FunkinCrew/Funkin/commit/bb974c264270d10ff503784063e5d77bb352b3f7)) - by @AbnormalPoof in [#4103](https://github.com/FunkinCrew/Funkin/pull/4103) - Added an option to launch the game in fullscreen. ([ee53ccd](https://github.com/FunkinCrew/Funkin/commit/ee53ccd32721e0790adfe82c60d4aca419db0a7f)) - by @AbnormalPoof in [#3738](https://github.com/FunkinCrew/Funkin/pull/3738) - Added descriptions for each item in the Preferences menu. ([a17b0e8](https://github.com/FunkinCrew/Funkin/commit/a17b0e8b3cc1d56fcdc0b51eaca9fd57cdb5bce0)) - by @anysad in [#3872](https://github.com/FunkinCrew/Funkin/pull/3872) @@ -52,42 +65,31 @@ The Pit Stop 2 update! - Added `DEBUG_BUILD` value to `Constants` to indicate whether a build has debug functions enabled. ([ad45b72](https://github.com/FunkinCrew/Funkin/commit/ad45b72b1ae8eb73a12dc51bcb59f66cc55e7bbd)) - by @AbnormalPoof in [#3853](https://github.com/FunkinCrew/Funkin/pull/3853) ### Changed + - Switched from hxCodec to hxvlc for video playback. This may break a mod or two. - Check the [Funkin Modding Docs](https://funkincrew.github.io/funkin-modding-docs/09-migration/09-02-0.5.0-to-0.6.0.html) for more info on how to update your mods. - Polymod should now ignore `.git` files when loading mods. -- Lots of improvements to issue and pull request organization. - by @Hundrec and @AbnormalPoof +- Lots of improvements to GitHub issue and pull request organization. - by @Hundrec and @AbnormalPoof - Overhauled the Changelog to improve readability and properly credit contributors. ([4383fcf](https://github.com/FunkinCrew/Funkin/commit/4383fcf32c280a1c0ee7b9c80d255611d497cabc)) - by @Hundrec in [#4296](https://github.com/FunkinCrew/Funkin/pull/4296) and [#4298](https://github.com/FunkinCrew/Funkin/pull/4298) - Made various improvements to the screenshot function. ([9ce7bbc](https://github.com/FunkinCrew/Funkin/commit/9ce7bbcfbb8f30ae120c876194f89bc4c787f585)) - by @Lasercar in [#4082](https://github.com/FunkinCrew/Funkin/pull/4082) - Accept keybinds (Z and Space by default) can now be used to exit the Results screen. ([edb270d](https://github.com/FunkinCrew/Funkin/commit/edb270d15e41784dccbf75639ac731840e80fe23)) - by @JVNpixels in [#3799](https://github.com/FunkinCrew/Funkin/pull/3799) - Reordered UI keybinds in the controls menu for consistency. ([a01bcc3](https://github.com/FunkinCrew/Funkin/commit/a01bcc3da836ec52851ca9de13ef459daf61269a)) - by @lemz1 in [#3027](https://github.com/FunkinCrew/Funkin/pull/3027) - New save files now have default Freeplay controls for gamepads. ([2b7f62e](https://github.com/FunkinCrew/Funkin/commit/2b7f62edd33de5527e259d9e5643f926d35da734)) - by @MrMadera in [#3934](https://github.com/FunkinCrew/Funkin/pull/3934) -- Renamed “Auto Pause” preference to “Pause on Unfocus” for clarity. ([49a21c1](https://github.com/FunkinCrew/Funkin/commit/49a21c198236fbecbe5902bb106f78939cc6442a)) - by @JackXson-Real in [#4346](https://github.com/FunkinCrew/Funkin/pull/4346) - Made scrolling smoother in the Chart Editor. ([20d9016](https://github.com/FunkinCrew/Funkin/commit/20d90169845f1e50f849e39f4c5f818359756c78)) - by @ninjamuffin99 in [#3768](https://github.com/FunkinCrew/Funkin/pull/3768) -- The Chart Editor now clears the undo/redo history after loading a new song. ([c7bdc1a](https://github.com/FunkinCrew/Funkin/commit/c7bdc1abe265678468920ca3cc3d7eface5d6925)) - by @Lasercar in [#4308](https://github.com/FunkinCrew/Funkin/pull/4308) - Mods with missing dependencies are now skipped instead of preventing all mods from loading. ([1c2fb43](https://github.com/FunkinCrew/Funkin/commit/1c2fb43ae16cf40be5ef94c40b047e8e772b1211)) - by @AbnormalPoof in [#3993](https://github.com/FunkinCrew/Funkin/pull/3993) - Slightly improved flexibility for modding note hit animations. ([3aad825](https://github.com/FunkinCrew/Funkin/commit/3aad825f865c4ed87016983d44121e2c1610d332)) - by @TechnikTil in [#3936](https://github.com/FunkinCrew/Funkin/pull/3936) - Introduced several QoL modding changes. ([785c4be](https://github.com/FunkinCrew/Funkin/commit/785c4be88b52dc1b5899013822fc004ba7d9894d)) - by @Kade-github in [#4009](https://github.com/FunkinCrew/Funkin/pull/4009) - Lots of smaller bug fixes. -## New Contributors for 0.6.0 +### Fixed -* @PatoFlamejanteTV made their first contribution in [#3843](https://github.com/FunkinCrew/Funkin/pull/3843) -* @sphis-Sinco made their first contribution in [#3881](https://github.com/FunkinCrew/Funkin/pull/3881) -* @MrMadera made their first contribution in [#3934](https://github.com/FunkinCrew/Funkin/pull/3934) -* @Lasercar made their first contribution in [#4100](https://github.com/FunkinCrew/Funkin/pull/4065) -* @MidyGamy made their first contribution in [#4068](https://github.com/FunkinCrew/Funkin/pull/4068) -* @MrScottyPieey made their first contribution in [#4085](https://github.com/FunkinCrew/Funkin/pull/4085) -* @JackXson-Real made their first contribution in [#4346](https://github.com/FunkinCrew/Funkin/pull/4346) - -# Fixed - Fixed a bug where the song would restart from the beginning instead of moving to the Results screen. ([3667c51](https://github.com/FunkinCrew/Funkin/commit/3667c51c1efe14cfe7c810e2f35991f08f50781a)) - by @KoloInDaCrib in [#4309](https://github.com/FunkinCrew/Funkin/pull/4309) and @Lasercar in [#4330](https://github.com/FunkinCrew/Funkin/pull/4330) - Reduced stuttering when resyncing instrumental and voices tracks. ([22d41d2](https://github.com/FunkinCrew/Funkin/commit/22d41d21b88acb7422a0afcda8414682710bd2ed)) - by @TechnikTil in [#3955](https://github.com/FunkinCrew/Funkin/pull/3955) - Songs with only instrumental tracks no longer stutter. ([dfe02ec](https://github.com/FunkinCrew/Funkin/commit/dfe02ec668b61d6308f459c978d12a7487f9dc28)) - by @KoloInDaCrib in [#3861](https://github.com/FunkinCrew/Funkin/pull/3861) -- The mouse cursor is no longer visible outside of debug editors. ([1c12b84](https://github.com/FunkinCrew/Funkin/commit/1c12b8467eca350eb28138473360d5358fa620e2)) - by @sphis-Sinco in [#3881](https://github.com/FunkinCrew/Funkin/pull/3881) +- The debug mouse cursor no longer flickers before the Title Screen loads. ([1c12b84](https://github.com/FunkinCrew/Funkin/commit/1c12b8467eca350eb28138473360d5358fa620e2)) - by @sphis-Sinco in [#3881](https://github.com/FunkinCrew/Funkin/pull/3881) - Unbound keys now display as [N/A] instead of crashing the game. ([94e5d3c](https://github.com/FunkinCrew/Funkin/commit/94e5d3ca258cbaa5d9ab6f0cb26feda83b3433a2)) - by @NotHyper-474 in [#4355](https://github.com/FunkinCrew/Funkin/pull/4355) - Songs can no longer be spam-selected after selecting an instrumental in Freeplay. ([0e0c4ae](https://github.com/FunkinCrew/Funkin/commit/0e0c4aeb7745cfb9479685ccbb635cf3743cddbb)) - by @AbnormalPoof in [#3866](https://github.com/FunkinCrew/Funkin/pull/3866) - The Random capsule can now switch to Erect/Nightmare difficulties in Freeplay. ([6f32747](https://github.com/FunkinCrew/Funkin/commit/6f327474f30374f50774f432ea3ccb6d02579445)) - by @KoloInDaCrib in [#3838](https://github.com/FunkinCrew/Funkin/pull/3838) -- The millions place digit of the Freeplay score now updates properly. ([3719ca6](https://github.com/FunkinCrew/Funkin/commit/3719ca6ce7a170c3c890cdb2aeea3e4534b91736)) - by @Lasercar in [#4065](https://github.com/FunkinCrew/Funkin/pull/4065) - Fixed a rare bug where a song would not register as beaten. ([a3e2373](https://github.com/FunkinCrew/Funkin/commit/a3e23733db104b1ef00cfcff17db3a5d032a4d67)) - by @AbnormalPoof in [#3820](https://github.com/FunkinCrew/Funkin/pull/3820) - The difficulty graphic on the Results screen no longer cuts off incorrectly. ([b13bf05](https://github.com/FunkinCrew/Funkin/commit/b13bf05d16ff2977309e0c7ba3f049c0134e8902)) - by @AbnormalPoof in [#4161](https://github.com/FunkinCrew/Funkin/pull/4161) - Four-digit long Total Notes values in the Results screen no longer overflow to the right. ([2be4c0c](https://github.com/FunkinCrew/Funkin/commit/2be4c0c7196e018a43f9697f015179f3efc3fcdb)) - by @Hundrec in [#4356](https://github.com/FunkinCrew/Funkin/pull/4356) @@ -99,6 +101,16 @@ The Pit Stop 2 update! - Removed an unused class from Polymod blacklist. ([06c12e3](https://github.com/FunkinCrew/Funkin/commit/06c12e36c6bd6df4e2be32a3bec540172e79e162)) - by @AbnormalPoof in [#3729](https://github.com/FunkinCrew/Funkin/pull/3729) - Many additional small bug fixes. +## New Contributors for 0.6.0 + +* @PatoFlamejanteTV made their first contribution in [#3843](https://github.com/FunkinCrew/Funkin/pull/3843) +* @sphis-Sinco made their first contribution in [#3881](https://github.com/FunkinCrew/Funkin/pull/3881) +* @MrMadera made their first contribution in [#3934](https://github.com/FunkinCrew/Funkin/pull/3934) +* @MidyGamy made their first contribution in [#4068](https://github.com/FunkinCrew/Funkin/pull/4068) +* @Lasercar made their first contribution in [#4082](https://github.com/FunkinCrew/Funkin/pull/4082) +* @MrScottyPieey made their first contribution in [#4085](https://github.com/FunkinCrew/Funkin/pull/4085) + + ## [0.5.3] - 2024-10-18 This patch resolves a critical issue that could cause user's save data to become corrupted. It is recommended that users switch to this version immediately and avoid using version 0.5.2. From 19912b5f89cd8b0458f1dacc1b2eea929bb58aab Mon Sep 17 00:00:00 2001 From: Hundrec Date: Fri, 4 Apr 2025 19:02:47 -0600 Subject: [PATCH 02/40] Oops, one more lil space --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f30cef15c..0a34ec499 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,7 +61,7 @@ The Pit Stop 2 update! - Added offsets support for album titles. ([69d8570](https://github.com/FunkinCrew/Funkin/commit/69d8570a9eb06011ed6dd95fcbef83d90f7f8684)) - by @AbnormalPoof in [#3618](https://github.com/FunkinCrew/Funkin/pull/3618) - Added three new properties to stage data: `angle`, `scroll`, and `alpha`. ([ff56b19](https://github.com/FunkinCrew/Funkin/commit/ff56b1948aef42bbb6bb4ede4f9b2012d49ab044)) - by @AbnormalPoof in [#3720](https://github.com/FunkinCrew/Funkin/pull/3720) - Added script events for losing/gaining focus. ([4b127b6](https://github.com/FunkinCrew/Funkin/commit/4b127b64130f6f753d0574ec66a1672322e4bd13)) - by @AbnormalPoof in [#3721](https://github.com/FunkinCrew/Funkin/pull/3721) -- Added 10 new functions to `ReflectUtil`.([6216655](https://github.com/FunkinCrew/Funkin/commit/62166554e7a176245d1a63bd15122033044c4e40)) - by @AbnormalPoof in [#3622](https://github.com/FunkinCrew/Funkin/pull/3622), [#3809](https://github.com/FunkinCrew/Funkin/pull/3809), and [#4019](https://github.com/FunkinCrew/Funkin/pull/4019) +- Added 10 new functions to `ReflectUtil`. ([6216655](https://github.com/FunkinCrew/Funkin/commit/62166554e7a176245d1a63bd15122033044c4e40)) - by @AbnormalPoof in [#3622](https://github.com/FunkinCrew/Funkin/pull/3622), [#3809](https://github.com/FunkinCrew/Funkin/pull/3809), and [#4019](https://github.com/FunkinCrew/Funkin/pull/4019) - Added `DEBUG_BUILD` value to `Constants` to indicate whether a build has debug functions enabled. ([ad45b72](https://github.com/FunkinCrew/Funkin/commit/ad45b72b1ae8eb73a12dc51bcb59f66cc55e7bbd)) - by @AbnormalPoof in [#3853](https://github.com/FunkinCrew/Funkin/pull/3853) ### Changed From c08847f48dc2c1b4a7baca0e2f5ed737dbcb2474 Mon Sep 17 00:00:00 2001 From: Hundrec Date: Sat, 5 Apr 2025 00:02:18 -0600 Subject: [PATCH 03/40] Add a few fixes and changes from 0.6.0 --- CHANGELOG.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a34ec499..93ac9acea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,8 @@ The Pit Stop 2 update! - Switched from hxCodec to hxvlc for video playback. This may break a mod or two. - Check the [Funkin Modding Docs](https://funkincrew.github.io/funkin-modding-docs/09-migration/09-02-0.5.0-to-0.6.0.html) for more info on how to update your mods. - Polymod should now ignore `.git` files when loading mods. +- The pause menu can now be opened and closed rapidly. +- Adjusted difficulty ratings and scroll speeds for many songs. - Lots of improvements to GitHub issue and pull request organization. - by @Hundrec and @AbnormalPoof - Overhauled the Changelog to improve readability and properly credit contributors. ([4383fcf](https://github.com/FunkinCrew/Funkin/commit/4383fcf32c280a1c0ee7b9c80d255611d497cabc)) - by @Hundrec in [#4296](https://github.com/FunkinCrew/Funkin/pull/4296) and [#4298](https://github.com/FunkinCrew/Funkin/pull/4298) - Made various improvements to the screenshot function. ([9ce7bbc](https://github.com/FunkinCrew/Funkin/commit/9ce7bbcfbb8f30ae120c876194f89bc4c787f585)) - by @Lasercar in [#4082](https://github.com/FunkinCrew/Funkin/pull/4082) @@ -79,10 +81,15 @@ The Pit Stop 2 update! - Mods with missing dependencies are now skipped instead of preventing all mods from loading. ([1c2fb43](https://github.com/FunkinCrew/Funkin/commit/1c2fb43ae16cf40be5ef94c40b047e8e772b1211)) - by @AbnormalPoof in [#3993](https://github.com/FunkinCrew/Funkin/pull/3993) - Slightly improved flexibility for modding note hit animations. ([3aad825](https://github.com/FunkinCrew/Funkin/commit/3aad825f865c4ed87016983d44121e2c1610d332)) - by @TechnikTil in [#3936](https://github.com/FunkinCrew/Funkin/pull/3936) - Introduced several QoL modding changes. ([785c4be](https://github.com/FunkinCrew/Funkin/commit/785c4be88b52dc1b5899013822fc004ba7d9894d)) - by @Kade-github in [#4009](https://github.com/FunkinCrew/Funkin/pull/4009) -- Lots of smaller bug fixes. +- Lots of smaller changes. ### Fixed +- Shaders no longer create thin seams within atlas sprites. +- Completing a song in Practice Mode no longer plays a new rank animation in the Freeplay menu. +- Fixed lots of charting issues across many songs. +- The Chart Editor grid now properly adjusts to the new BPM after switching variations. +- Fixed a few crashes in the Stage Editor. - Fixed a bug where the song would restart from the beginning instead of moving to the Results screen. ([3667c51](https://github.com/FunkinCrew/Funkin/commit/3667c51c1efe14cfe7c810e2f35991f08f50781a)) - by @KoloInDaCrib in [#4309](https://github.com/FunkinCrew/Funkin/pull/4309) and @Lasercar in [#4330](https://github.com/FunkinCrew/Funkin/pull/4330) - Reduced stuttering when resyncing instrumental and voices tracks. ([22d41d2](https://github.com/FunkinCrew/Funkin/commit/22d41d21b88acb7422a0afcda8414682710bd2ed)) - by @TechnikTil in [#3955](https://github.com/FunkinCrew/Funkin/pull/3955) - Songs with only instrumental tracks no longer stutter. ([dfe02ec](https://github.com/FunkinCrew/Funkin/commit/dfe02ec668b61d6308f459c978d12a7487f9dc28)) - by @KoloInDaCrib in [#3861](https://github.com/FunkinCrew/Funkin/pull/3861) From dc876be57cb5c5159e44f220542e82094126c6c4 Mon Sep 17 00:00:00 2001 From: Hundrec Date: Thu, 10 Apr 2025 22:49:12 -0600 Subject: [PATCH 04/40] Two more Chart Editor entries in Changelog --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93ac9acea..13cc8626a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,10 +51,11 @@ The Pit Stop 2 update! - Added new stickers which appear when exiting Pico songs! - The new sticker system isn't fully available to mods yet, but we're working on it! - New option in the Preferences menu: Strumline Backgrounds! -- Options in the Preferences menu now display an on-screen description when selected. +- New song event type in the Chart Editor: Set Health Icon! + - This event is now used in Stress (Pico Mix). - The HOME and END keys now jump to the top and bottom of the Freeplay song list, respectively. ([bb974c2](https://github.com/FunkinCrew/Funkin/commit/bb974c264270d10ff503784063e5d77bb352b3f7)) - by @AbnormalPoof in [#4103](https://github.com/FunkinCrew/Funkin/pull/4103) - Added an option to launch the game in fullscreen. ([ee53ccd](https://github.com/FunkinCrew/Funkin/commit/ee53ccd32721e0790adfe82c60d4aca419db0a7f)) - by @AbnormalPoof in [#3738](https://github.com/FunkinCrew/Funkin/pull/3738) -- Added descriptions for each item in the Preferences menu. ([a17b0e8](https://github.com/FunkinCrew/Funkin/commit/a17b0e8b3cc1d56fcdc0b51eaca9fd57cdb5bce0)) - by @anysad in [#3872](https://github.com/FunkinCrew/Funkin/pull/3872) +- Added on-screen descriptions for each item in the Preferences menu. ([a17b0e8](https://github.com/FunkinCrew/Funkin/commit/a17b0e8b3cc1d56fcdc0b51eaca9fd57cdb5bce0)) - by @anysad in [#3872](https://github.com/FunkinCrew/Funkin/pull/3872) - Added precise scrolling in the Chart Editor using Ctrl-Mouse Wheel. ([0d8e4a5](https://github.com/FunkinCrew/Funkin/commit/0d8e4a53305d6d069454812766300122f3581e31)) - by @ninjamuffin99 in [#3806](https://github.com/FunkinCrew/Funkin/pull/3806) - Added a “None” option to the character selector in the Chart Editor. ([d9637d3](https://github.com/FunkinCrew/Funkin/commit/d9637d3a19466b9fc68a102676c46a74ef504909)) - by @Lasercar in [#4279](https://github.com/FunkinCrew/Funkin/pull/4279) - Added the ability to flip the character in the Animation Editor. ([de02137](https://github.com/FunkinCrew/Funkin/commit/de02137d7c7d1779e85aeda34743f506a5b9cc27)) - by @AbnormalPoof in [#3028](https://github.com/FunkinCrew/Funkin/pull/3028) @@ -71,6 +72,8 @@ The Pit Stop 2 update! - Polymod should now ignore `.git` files when loading mods. - The pause menu can now be opened and closed rapidly. - Adjusted difficulty ratings and scroll speeds for many songs. +- Chart Editor event fields now allow for values to be as specific as desired. + - For example, the Zoom Camera event can now be set to 0.9857. - Lots of improvements to GitHub issue and pull request organization. - by @Hundrec and @AbnormalPoof - Overhauled the Changelog to improve readability and properly credit contributors. ([4383fcf](https://github.com/FunkinCrew/Funkin/commit/4383fcf32c280a1c0ee7b9c80d255611d497cabc)) - by @Hundrec in [#4296](https://github.com/FunkinCrew/Funkin/pull/4296) and [#4298](https://github.com/FunkinCrew/Funkin/pull/4298) - Made various improvements to the screenshot function. ([9ce7bbc](https://github.com/FunkinCrew/Funkin/commit/9ce7bbcfbb8f30ae120c876194f89bc4c787f585)) - by @Lasercar in [#4082](https://github.com/FunkinCrew/Funkin/pull/4082) From b83ae65a65c326391569d15ac90e0da7829ab03a Mon Sep 17 00:00:00 2001 From: Abnormal <86753001+AbnormalPoof@users.noreply.github.com> Date: Sat, 29 Mar 2025 02:08:28 -0500 Subject: [PATCH 05/40] docs: Add missing documentation to the remaining functions in `Module` --- source/funkin/modding/module/Module.hx | 86 ++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/source/funkin/modding/module/Module.hx b/source/funkin/modding/module/Module.hx index 57f0cc2da..012b9869f 100644 --- a/source/funkin/modding/module/Module.hx +++ b/source/funkin/modding/module/Module.hx @@ -57,6 +57,9 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte // TODO: Half of these aren't actually being called!!!!!!! + /** + * Called when ANY script event is dispatched. + */ public function onScriptEvent(event:ScriptEvent) {} /** @@ -71,55 +74,138 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte */ public function onDestroy(event:ScriptEvent) {} + /** + * Called every frame. + */ public function onUpdate(event:UpdateScriptEvent) {} + /** + * Called when the game is paused. + */ public function onPause(event:PauseScriptEvent) {} + /** + * Called when the game is resumed. + */ public function onResume(event:ScriptEvent) {} + /** + * Called when the song begins. + */ public function onSongStart(event:ScriptEvent) {} + /** + * Called when the song ends. + */ public function onSongEnd(event:ScriptEvent) {} + /** + * Called when the player dies. + */ public function onGameOver(event:ScriptEvent) {} + /** + * Called when a note on the strumline has been rendered and is now onscreen. + * This gets dispatched for both the player and opponent strumlines. + */ public function onNoteIncoming(event:NoteScriptEvent) {} + /** + * Called when a note has been hit. + * This gets dispatched for both the player and opponent strumlines. + */ public function onNoteHit(event:HitNoteScriptEvent) {} + /** + * Called when a note has been missed. + * This gets dispatched for both the player and opponent strumlines. + */ public function onNoteMiss(event:NoteScriptEvent) {} + /** + * Called when the player presses a key without any notes present. + */ public function onNoteGhostMiss(event:GhostMissNoteScriptEvent) {} + /** + * Called when a step is hit in the song. + */ public function onStepHit(event:SongTimeScriptEvent) {} + /** + * Called when a beat is hit in the song. + */ public function onBeatHit(event:SongTimeScriptEvent) {} + /** + * Called when a song event is triggered. + */ public function onSongEvent(event:SongEventScriptEvent) {} + /** + * Called when the countdown begins. + */ public function onCountdownStart(event:CountdownScriptEvent) {} + /** + * Called for every step in the countdown. + */ public function onCountdownStep(event:CountdownScriptEvent) {} + /** + * Called when the countdown ends, but BEFORE the song starts. + */ public function onCountdownEnd(event:CountdownScriptEvent) {} + /** + * Called when the song's chart has been parsed and loaded. + */ public function onSongLoaded(event:SongLoadScriptEvent) {} + /** + * Called when the game is about to switch to a new state. + */ public function onStateChangeBegin(event:StateChangeScriptEvent) {} + /** + * Called after the game has switched to a new state. + */ public function onStateChangeEnd(event:StateChangeScriptEvent) {} + /** + * Called when the game regains focus. + * This does not get called if "Auto Pause" is disabled. + */ public function onFocusGained(event:FocusScriptEvent) {} + /** + * Called when the game loses focus. + * This does not get called if "Auto Pause" is disabled. + */ public function onFocusLost(event:FocusScriptEvent) {} + /** + * Called when the game is about to open a substate. + */ public function onSubStateOpenBegin(event:SubStateScriptEvent) {} + /** + * Called when a substate has been opened. + */ public function onSubStateOpenEnd(event:SubStateScriptEvent) {} + /** + * Called when the game is about to close a substate. + */ public function onSubStateCloseBegin(event:SubStateScriptEvent) {} + /** + * Called when a substate has been closed. + */ public function onSubStateCloseEnd(event:SubStateScriptEvent) {} + /** + * Called when the song has been restarted. + */ public function onSongRetry(event:SongRetryEvent) {} } From 5be4f3bbb98507bd346931833484dd2359b4098d Mon Sep 17 00:00:00 2001 From: Abnormal <86753001+AbnormalPoof@users.noreply.github.com> Date: Sat, 12 Apr 2025 12:39:16 -0500 Subject: [PATCH 06/40] chore: Remove `ComboMilestone` --- source/funkin/play/PlayState.hx | 30 ----- .../funkin/play/components/ComboMilestone.hx | 126 ------------------ 2 files changed, 156 deletions(-) delete mode 100644 source/funkin/play/components/ComboMilestone.hx diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 7ac949424..a05c220d7 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -32,7 +32,6 @@ import funkin.api.newgrounds.Events; import funkin.modding.events.ScriptEventDispatcher; import funkin.play.character.BaseCharacter; import funkin.play.character.CharacterData.CharacterDataParser; -import funkin.play.components.ComboMilestone; import funkin.play.components.HealthIcon; import funkin.play.components.PopUpStuff; import funkin.play.cutscene.dialogue.Conversation; @@ -1510,35 +1509,6 @@ class PlayState extends MusicBeatSubState // Break up into individual lines to aid debugging. - var shouldShowComboText:Bool = false; - // TODO: Re-enable combo text (how to do this without sections?). - // if (currentSong != null) - // { - // shouldShowComboText = (Conductor.instance.currentBeat % 8 == 7); - // var daSection = .getSong()[Std.int(Conductor.instance.currentBeat / 16)]; - // shouldShowComboText = shouldShowComboText && (daSection != null && daSection.mustHitSection); - // shouldShowComboText = shouldShowComboText && (Highscore.tallies.combo > 5); - // - // var daNextSection = .getSong()[Std.int(Conductor.instance.currentBeat / 16) + 1]; - // var isEndOfSong = .getSong().length < Std.int(Conductor.instance.currentBeat / 16); - // shouldShowComboText = shouldShowComboText && (isEndOfSong || (daNextSection != null && !daNextSection.mustHitSection)); - // } - - if (shouldShowComboText) - { - var animShit:ComboMilestone = new ComboMilestone(-100, 300, Highscore.tallies.combo); - animShit.scrollFactor.set(0.6, 0.6); - animShit.zIndex = 1100; - animShit.cameras = [camHUD]; - add(animShit); - - var frameShit:Float = (1 / 24) * 2; // equals 2 frames in the animation - - new FlxTimer().start(((Conductor.instance.beatLengthMs / 1000) * 1.25) - frameShit, function(tmr) { - animShit.forceFinish(); - }); - } - if (playerStrumline != null) playerStrumline.onBeatHit(); if (opponentStrumline != null) opponentStrumline.onBeatHit(); diff --git a/source/funkin/play/components/ComboMilestone.hx b/source/funkin/play/components/ComboMilestone.hx deleted file mode 100644 index 22ce2d671..000000000 --- a/source/funkin/play/components/ComboMilestone.hx +++ /dev/null @@ -1,126 +0,0 @@ -package funkin.play.components; - -import flixel.FlxSprite; -import flixel.group.FlxGroup.FlxTypedGroup; -import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; -import flixel.util.FlxTimer; -import funkin.audio.FunkinSound; - -class ComboMilestone extends FlxTypedSpriteGroup -{ - var effectStuff:FlxSprite; - - var wasComboSetup:Bool = false; - var daCombo:Int = 0; - - var grpNumbers:FlxTypedGroup; - - var onScreenTime:Float = 0; - - public function new(x:Float, y:Float, daCombo:Int = 0) - { - super(x, y); - - this.daCombo = daCombo; - - effectStuff = new FlxSprite(0, 0); - effectStuff.frames = Paths.getSparrowAtlas('comboMilestone'); - effectStuff.animation.addByPrefix('funny', 'NOTE COMBO animation', 24, false); - effectStuff.animation.play('funny'); - effectStuff.animation.finishCallback = function(nameThing) { - kill(); - }; - effectStuff.setGraphicSize(Std.int(effectStuff.width * 0.7)); - add(effectStuff); - - grpNumbers = new FlxTypedGroup(); - // add(grpNumbers); - } - - public function forceFinish():Void - { - if (onScreenTime < 0.9) - { - new FlxTimer().start((Conductor.instance.beatLengthMs / 1000) * 0.25, function(tmr) { - forceFinish(); - }); - } - else - effectStuff.animation.play('funny', true, false, 18); - } - - override function update(elapsed:Float) - { - onScreenTime += elapsed; - - if (effectStuff.animation.curAnim.curFrame == 17) effectStuff.animation.pause(); - - if (effectStuff.animation.curAnim.curFrame == 2 && !wasComboSetup) - { - setupCombo(daCombo); - } - - if (effectStuff.animation.curAnim.curFrame == 18) - { - grpNumbers.forEach(function(spr:ComboMilestoneNumber) { - spr.animation.reset(); - }); - } - - if (effectStuff.animation.curAnim.curFrame == 20) - { - grpNumbers.forEach(function(spr:ComboMilestoneNumber) { - spr.kill(); - }); - } - - super.update(elapsed); - } - - function setupCombo(daCombo:Int) - { - FunkinSound.playOnce(Paths.sound('comboSound')); - - wasComboSetup = true; - var loopNum:Int = 0; - - while (daCombo > 0) - { - var comboNumber:ComboMilestoneNumber = new ComboMilestoneNumber(450 - (100 * loopNum), 20 + 14 * loopNum, daCombo % 10); - comboNumber.setGraphicSize(Std.int(comboNumber.width * 0.7)); - grpNumbers.add(comboNumber); - add(comboNumber); - - loopNum += 1; - - daCombo = Math.floor(daCombo / 10); - } - } -} - -class ComboMilestoneNumber extends FlxSprite -{ - public function new(x:Float, y:Float, digit:Int) - { - super(x - 20, y); - - var stringNum:String = Std.string(digit); - frames = Paths.getSparrowAtlas('comboMilestoneNumbers'); - animation.addByPrefix(stringNum, stringNum, 24, false); - animation.play(stringNum); - updateHitbox(); - } - - var shiftedX:Bool = false; - - override function update(elapsed:Float) - { - if (animation.curAnim.curFrame == 2 && !shiftedX) - { - shiftedX = true; - x += 20; - } - - super.update(elapsed); - } -} From 068c9fb43d49ff6ea13e5a73f77a42207954be2c Mon Sep 17 00:00:00 2001 From: Kolo <67389779+JustKolosaki@users.noreply.github.com> Date: Sat, 5 Apr 2025 12:45:51 +0200 Subject: [PATCH 07/40] no thoughts save empty also more code clarity ig --- source/funkin/save/Save.hx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx index 503fc8348..ec527122e 100644 --- a/source/funkin/save/Save.hx +++ b/source/funkin/save/Save.hx @@ -43,7 +43,7 @@ class Save { if (_instance == null) { - return _instance = load(); + return load(); } return _instance; } @@ -55,7 +55,10 @@ class Save trace("[SAVE] Loading save..."); // Bind save data. - return loadFromSlot(BASE_SAVE_SLOT); + var loadedSave:Save = loadFromSlot(BASE_SAVE_SLOT); + if (_instance == null) _instance = loadedSave; + + return loadedSave; } public static function clearData():Void From deb4e1921dcd2bb82886de42c50c9e04dae89cc8 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Mon, 14 Apr 2025 12:52:06 -0400 Subject: [PATCH 08/40] Fix an issue where Stress would crash on the second playthrough --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index 013a83b33..166f5e2e3 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 013a83b334272a73cf279a78acec3f2d19c84da6 +Subproject commit 166f5e2e3a344f70fc6e62c20b9fb4b99f9ef41e From cdc468ba15683b3c0f54015e337673c77ddd7962 Mon Sep 17 00:00:00 2001 From: Abnormal <86753001+AbnormalPoof@users.noreply.github.com> Date: Wed, 15 Jan 2025 16:41:17 -0600 Subject: [PATCH 09/40] feat: Sticker Registry --- source/funkin/InitState.hx | 2 + source/funkin/data/stickers/CHANGELOG.md | 9 ++ source/funkin/data/stickers/StickerData.hx | 39 +++++++ .../funkin/data/stickers/StickerRegistry.hx | 84 ++++++++++++++ source/funkin/data/stickers/StickerSet.hx | 109 ++++++++++++++++++ source/funkin/modding/PolymodHandler.hx | 2 + .../ui/transition/ScriptedStickerSet.hx | 8 ++ .../funkin/ui/transition/StickerSubState.hx | 102 +++------------- 8 files changed, 270 insertions(+), 85 deletions(-) create mode 100644 source/funkin/data/stickers/CHANGELOG.md create mode 100644 source/funkin/data/stickers/StickerData.hx create mode 100644 source/funkin/data/stickers/StickerRegistry.hx create mode 100644 source/funkin/data/stickers/StickerSet.hx create mode 100644 source/funkin/ui/transition/ScriptedStickerSet.hx diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index f4efd19e4..0dc64756b 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -18,6 +18,7 @@ import funkin.data.freeplay.player.PlayerRegistry; import funkin.data.freeplay.style.FreeplayStyleRegistry; import funkin.data.notestyle.NoteStyleRegistry; import funkin.data.song.SongRegistry; +import funkin.data.stickers.StickerRegistry; import funkin.data.event.SongEventRegistry; import funkin.data.stage.StageRegistry; import funkin.data.story.level.LevelRegistry; @@ -190,6 +191,7 @@ class InitState extends FlxState FreeplayStyleRegistry.instance.loadEntries(); AlbumRegistry.instance.loadEntries(); StageRegistry.instance.loadEntries(); + StickerRegistry.instance.loadEntries(); // TODO: CharacterDataParser doesn't use json2object, so it's way slower than the other parsers and more prone to syntax errors. // Move it to use a BaseRegistry. diff --git a/source/funkin/data/stickers/CHANGELOG.md b/source/funkin/data/stickers/CHANGELOG.md new file mode 100644 index 000000000..d278a5f9c --- /dev/null +++ b/source/funkin/data/stickers/CHANGELOG.md @@ -0,0 +1,9 @@ +# Sticker Set Data Schema Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] +Initial release. diff --git a/source/funkin/data/stickers/StickerData.hx b/source/funkin/data/stickers/StickerData.hx new file mode 100644 index 000000000..e0d82d323 --- /dev/null +++ b/source/funkin/data/stickers/StickerData.hx @@ -0,0 +1,39 @@ +package funkin.data.stickers; + +/** + * A type definition for a sticker set. + * It includes things like its name, the artist, and the stickers. + * @see https://lib.haxe.org/p/json2object/ + */ +typedef StickerData = +{ + /** + * Semantic version of the sticker set data. + */ + public var version:String; + + /** + * Readable name of the sticker set. + */ + public var name:String; + + /** + * The asset key for this sticker set. + */ + public var assetPath:String; + + /** + * The artist of the sticker set. + */ + public var artist:String; + + /** + * The stickers in the set. + */ + public var stickers:Map>; + + /** + * The sticker packs in this set. + */ + public var stickerPacks:Map>; +} diff --git a/source/funkin/data/stickers/StickerRegistry.hx b/source/funkin/data/stickers/StickerRegistry.hx new file mode 100644 index 000000000..08ec6194b --- /dev/null +++ b/source/funkin/data/stickers/StickerRegistry.hx @@ -0,0 +1,84 @@ +package funkin.data.stickers; + +import funkin.data.stickers.StickerSet; +import funkin.data.stickers.StickerData; +import funkin.ui.transition.ScriptedStickerSet; + +class StickerRegistry extends BaseRegistry +{ + /** + * The current version string for the sticker set data format. + * Handle breaking changes by incrementing this value + * and adding migration to the `migrateStickerData()` function. + */ + public static final STICKER_DATA_VERSION:thx.semver.Version = '1.0.0'; + + public static final STICKER_DATA_VERSION_RULE:thx.semver.VersionRule = '1.0.x'; + + public static final instance:StickerRegistry = new StickerRegistry(); + + public function new() + { + super('STICKER', 'stickersets', STICKER_DATA_VERSION_RULE); + } + + /** + * Read, parse, and validate the JSON data and produce the corresponding data object. + * @param id The ID of the entry to load. + * @return The parsed data object. + */ + public function parseEntryData(id:String):Null + { + // JsonParser does not take type parameters, + // otherwise this function would be in BaseRegistry. + var parser:json2object.JsonParser = new json2object.JsonParser(); + parser.ignoreUnknownVariables = false; + + switch (loadEntryFile(id)) + { + case {fileName: fileName, contents: contents}: + parser.fromJson(contents, fileName); + default: + return null; + } + + if (parser.errors.length > 0) + { + printErrors(parser.errors, id); + return null; + } + return parser.value; + } + + /** + * Parse and validate the JSON data and produce the corresponding data object. + * + * NOTE: Must be implemented on the implementation class. + * @param contents The JSON as a string. + * @param fileName An optional file name for error reporting. + * @return The parsed data object. + */ + public function parseEntryDataRaw(contents:String, ?fileName:String):Null + { + var parser:json2object.JsonParser = new json2object.JsonParser(); + parser.ignoreUnknownVariables = false; + parser.fromJson(contents, fileName); + + if (parser.errors.length > 0) + { + printErrors(parser.errors, fileName); + return null; + } + return parser.value; + } + + function createScriptedEntry(clsName:String):StickerSet + { + return ScriptedStickerSet.init(clsName, 'unknown'); + } + + function getScriptedClassNames():Array + { + return ScriptedStickerSet.listScriptClasses(); + } +} diff --git a/source/funkin/data/stickers/StickerSet.hx b/source/funkin/data/stickers/StickerSet.hx new file mode 100644 index 000000000..1cea8293a --- /dev/null +++ b/source/funkin/data/stickers/StickerSet.hx @@ -0,0 +1,109 @@ +package funkin.data.stickers; + +import funkin.data.stickers.StickerData; +import funkin.data.stickers.StickerRegistry; +import funkin.data.IRegistryEntry; +import funkin.graphics.FunkinSprite; + +/** + * A class representing the data for a sticker set as displayed in the Sticker SubState. + */ +class StickerSet implements IRegistryEntry +{ + /** + * The internal ID for this sticker set. + */ + public final id:String; + + /** + * The full data for this sticker set. + */ + public final _data:StickerData; + + public function new(id:String) + { + this.id = id; + this._data = _fetchData(id); + + if (_data == null) + { + throw 'Could not parse sticker set data for id: $id'; + } + } + + /** + * Return the name of the sticker set. + * @return The name of the sticker set + */ + public function getStickerSetName():String + { + return _data.name; + } + + /** + * Return the artist of the sticker set. + * @return The list of artists + */ + public function getStickerSetArtist():String + { + return _data.artist; + } + + /** + * Get the asset key for the album art. + * @return The asset key + */ + public function getStickerSetAssetKey():String + { + return _data.assetPath; + } + + /** + * Gets the stickers for a given sticker name. + * @param stickerName The name of the sticker to get. + * @return The sticker. + */ + public function getStickers(stickerName:String):Array + { + return _data.stickers[stickerName]; + } + + /** + * Gets the sticker pack for a given pack name. + * @param packName The name of the pack to get. + * @return The sticker pack. + */ + public function getPack(packName:String):Array + { + return _data.stickerPacks[packName]; + } + + public function toString():String + { + return 'StickerSet($id)'; + } + + public function destroy():Void {} + + static function _fetchData(id:String):Null + { + return StickerRegistry.instance.parseEntryDataWithMigration(id, StickerRegistry.instance.fetchEntryVersion(id)); + } +} + +class StickerSprite extends FunkinSprite +{ + public var timing:Float = 0; + + public function new(x:Float, y:Float, filePath:String):Void + { + super(x, y); + if (!Assets.exists(Paths.image(filePath))) + { + throw 'File path does not exist! ($filePath)'; + } + loadTexture(filePath); + updateHitbox(); + scrollFactor.set(); + } +} diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx index f706296ac..ca42bb9bf 100644 --- a/source/funkin/modding/PolymodHandler.hx +++ b/source/funkin/modding/PolymodHandler.hx @@ -11,6 +11,7 @@ import funkin.play.notes.notekind.NoteKindManager; import funkin.data.song.SongRegistry; import funkin.data.freeplay.player.PlayerRegistry; import funkin.data.stage.StageRegistry; +import funkin.data.stickers.StickerRegistry; import funkin.data.freeplay.album.AlbumRegistry; import funkin.modding.module.ModuleHandler; import funkin.play.character.CharacterData.CharacterDataParser; @@ -480,6 +481,7 @@ class PolymodHandler SpeakerRegistry.instance.loadEntries(); AlbumRegistry.instance.loadEntries(); StageRegistry.instance.loadEntries(); + StickerRegistry.instance.loadEntries(); CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry. NoteKindManager.loadScripts(); diff --git a/source/funkin/ui/transition/ScriptedStickerSet.hx b/source/funkin/ui/transition/ScriptedStickerSet.hx new file mode 100644 index 000000000..aaada82db --- /dev/null +++ b/source/funkin/ui/transition/ScriptedStickerSet.hx @@ -0,0 +1,8 @@ +package funkin.ui.transition; + +/** + * A script that can be tied to a StickerSet. + * Create a scripted class that extends StickerSet to use this. + */ +@:hscriptClass +class ScriptedStickerSet extends funkin.data.stickers.StickerSet implements polymod.hscript.HScriptedClass {} diff --git a/source/funkin/ui/transition/StickerSubState.hx b/source/funkin/ui/transition/StickerSubState.hx index 9e98dbb6d..5df961690 100644 --- a/source/funkin/ui/transition/StickerSubState.hx +++ b/source/funkin/ui/transition/StickerSubState.hx @@ -11,6 +11,11 @@ import flixel.math.FlxMath; import flixel.util.FlxSort; import funkin.ui.mainmenu.MainMenuState; import flixel.addons.transition.FlxTransitionableState; +import openfl.display.BitmapData; +import funkin.data.stickers.StickerRegistry; +import funkin.data.stickers.StickerSet; +import funkin.ui.freeplay.FreeplayState; +import openfl.geom.Matrix; import funkin.audio.FunkinSound; import flixel.FlxState; @@ -165,23 +170,26 @@ class StickerSubState extends MusicBeatSubState grpStickers.clear(); } - var stickerInfo:StickerInfo = new StickerInfo(this.stickerSet); + var stickerSets:Array = StickerRegistry.instance.listEntryIds(); var stickers:Map> = new Map>(); + for (stickerSets in stickerInfo.getPack(this.stickerPack)) - { - stickers.set(stickerSets, stickerInfo.getStickers(stickerSets)); + for (stickerSets in stickerInfo.getPack("all")) + var assetKey:String = stickerSet.getStickerSetAssetKey(); + for (sticker in stickerSet.getPack("all")) + // add the asset key at the beginning of each sticker name because it's just easier lol + var stickerPack:Array = stickerSet.getStickers(sticker).map(s -> '${assetKey}/${s}'); + stickers.set(sticker, stickerPack); + } } var xPos:Float = -100; var yPos:Float = -100; while (xPos <= FlxG.width) { - var stickersId:String = FlxG.random.getObject(stickers.keyValues()); - var stickers = stickers.get(stickersId); - if (stickers == null) throw 'Could not get sticker group ${stickersId}'; - - var sticker:String = FlxG.random.getObject(stickers); - var sticky:StickerSprite = new StickerSprite(0, 0, stickerInfo.name, sticker); + var stickerSet:String = FlxG.random.getObject(stickers.keyValues()); + var sticker:String = FlxG.random.getObject(stickers.get(stickerSet)); + var sticky:StickerSprite = new StickerSprite(0, 0, sticker); sticky.visible = false; sticky.x = xPos; @@ -317,79 +325,3 @@ class StickerSubState extends MusicBeatSubState super.destroy(); } } - -class StickerSprite extends FunkinSprite -{ - public var timing:Float = 0; - - public function new(x:Float, y:Float, stickerSet:String, stickerName:String):Void - { - super(x, y); - loadTexture('transitionSwag/' + stickerSet + '/' + stickerName); - updateHitbox(); - scrollFactor.set(); - } -} - -class StickerInfo -{ - public var name:String; - public var artist:String; - public var stickers:Map>; - public var stickerPacks:Map>; - - public function new(stickerSet:String):Void - { - var path = Paths.file('images/transitionSwag/' + stickerSet + '/stickers.json'); - var json = Json.parse(Assets.getText(path)); - - // doin this dipshit nonsense cuz i dunno how to deal with casting a json object with - // a dash in its name (sticker-packs) - var jsonInfo:StickerShit = cast json; - - this.name = jsonInfo.name; - this.artist = jsonInfo.artist; - - stickerPacks = new Map>(); - - for (field in Reflect.fields(json.stickerPacks)) - { - var stickerFunny = json.stickerPacks; - var stickerStuff = Reflect.field(stickerFunny, field); - - stickerPacks.set(field, cast stickerStuff); - } - - // creates a similar for loop as before but for the stickers - stickers = new Map>(); - - for (field in Reflect.fields(json.stickers)) - { - var stickerFunny = json.stickers; - var stickerStuff = Reflect.field(stickerFunny, field); - - stickers.set(field, cast stickerStuff); - } - } - - public function getStickers(stickerName:String):Array - { - return this.stickers[stickerName]; - } - - public function getPack(packName:String, fallback:String = "all"):Array - { - if (!this.stickerPacks.exists(packName)) return this.stickerPacks[fallback]; - - return this.stickerPacks[packName]; - } -} - -// somethin damn cute just for the json to cast to! -typedef StickerShit = -{ - name:String, - artist:String, - stickers:Map>, - stickerPacks:Map> -} From 6390725ef0f29d3f87ab3b5073d3dda95a6658c8 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Wed, 2 Apr 2025 17:45:18 -0400 Subject: [PATCH 10/40] Redo data format for sticker packs to make it simpler and allow for song-specific selections. --- assets | 2 +- source/funkin/InitState.hx | 1 + .../funkin/data/freeplay/player/CHANGELOG.md | 5 + .../funkin/data/freeplay/player/PlayerData.hx | 9 ++ source/funkin/data/song/CHANGELOG.md | 5 + source/funkin/data/song/SongData.hx | 7 + source/funkin/data/stickers/CHANGELOG.md | 4 +- source/funkin/data/stickers/StickerData.hx | 17 +-- .../funkin/data/stickers/StickerRegistry.hx | 21 +-- source/funkin/data/stickers/StickerSet.hx | 109 --------------- source/funkin/play/GameOverSubState.hx | 24 ++-- source/funkin/play/PauseSubState.hx | 26 ++-- source/funkin/play/PlayState.hx | 3 +- source/funkin/play/ResultState.hx | 79 ++++++----- source/funkin/play/song/Song.hx | 10 ++ source/funkin/ui/debug/DebugMenuSubState.hx | 2 +- .../ui/debug/results/ResultsDebugSubState.hx | 1 + source/funkin/ui/freeplay/FreeplayState.hx | 2 +- .../freeplay/charselect/PlayableCharacter.hx | 5 + source/funkin/ui/story/StoryMenuState.hx | 2 +- .../ui/transition/ScriptedStickerSet.hx | 8 -- .../stickers/ScriptedStickerPack.hx | 8 ++ .../ui/transition/stickers/StickerPack.hx | 82 +++++++++++ .../ui/transition/stickers/StickerSprite.hx | 16 +++ .../{ => stickers}/StickerSubState.hx | 127 ++++++------------ source/funkin/util/Constants.hx | 5 + 26 files changed, 297 insertions(+), 283 deletions(-) delete mode 100644 source/funkin/data/stickers/StickerSet.hx delete mode 100644 source/funkin/ui/transition/ScriptedStickerSet.hx create mode 100644 source/funkin/ui/transition/stickers/ScriptedStickerPack.hx create mode 100644 source/funkin/ui/transition/stickers/StickerPack.hx create mode 100644 source/funkin/ui/transition/stickers/StickerSprite.hx rename source/funkin/ui/transition/{ => stickers}/StickerSubState.hx (72%) diff --git a/assets b/assets index 166f5e2e3..086ae8b58 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 166f5e2e3a344f70fc6e62c20b9fb4b99f9ef41e +Subproject commit 086ae8b587d39ba4907646c1776c20b07ed0f0ae diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index 0dc64756b..5973937bf 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -250,6 +250,7 @@ class InitState extends FlxState songId: "cum", characterId: "pico", difficultyId: "nightmare", + variationId: "erect", isNewHighscore: true, scoreData: { diff --git a/source/funkin/data/freeplay/player/CHANGELOG.md b/source/funkin/data/freeplay/player/CHANGELOG.md index 7a31e11ca..ace9a27c2 100644 --- a/source/funkin/data/freeplay/player/CHANGELOG.md +++ b/source/funkin/data/freeplay/player/CHANGELOG.md @@ -5,5 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.1] +### Added +- Added `stickerPack` to specify which sticker pack to use when transitioning into and out of song featuring this character. + - If the value isn't present, it will use a default sticker pack instead. + ## [1.0.0] Initial release. diff --git a/source/funkin/data/freeplay/player/PlayerData.hx b/source/funkin/data/freeplay/player/PlayerData.hx index dc972657d..a6e962598 100644 --- a/source/funkin/data/freeplay/player/PlayerData.hx +++ b/source/funkin/data/freeplay/player/PlayerData.hx @@ -31,6 +31,15 @@ class PlayerData @:default(false) public var showUnownedChars:Bool = false; + /** + * The default sticker pack to use for songs featuring this playable character. + * Can be overridden by specific songs. + * @default `default` + */ + @:optional + @:default(funkin.util.Constants.DEFAULT_STICKER_PACK) + public var stickerPack:String = Constants.DEFAULT_STICKER_PACK; + /** * Which freeplay style to use for this character. */ diff --git a/source/funkin/data/song/CHANGELOG.md b/source/funkin/data/song/CHANGELOG.md index 86c9f6912..e95c9d98d 100644 --- a/source/funkin/data/song/CHANGELOG.md +++ b/source/funkin/data/song/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.2.5] +### Added +- Added `playData.stickerPack` to specify which sticker pack to use when transitioning into and out of a song. + - If the value isn't present, it will use the character's sticker pack instead. + ## [2.2.4] ### Added - Added `playData.characters.opponentVocals` to specify which vocal track(s) to play for the opponent. diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx index 074ed0b44..b5da02d00 100644 --- a/source/funkin/data/song/SongData.hx +++ b/source/funkin/data/song/SongData.hx @@ -471,6 +471,13 @@ class SongPlayData implements ICloneable @:optional public var album:Null; + /** + * The sticker pack for the song to use during transitions. + * If `null`, display the character's sticker pack. + */ + @:optional + public var stickerPack:Null; + /** * The start time for the audio preview in Freeplay. * Defaults to 0 seconds in. diff --git a/source/funkin/data/stickers/CHANGELOG.md b/source/funkin/data/stickers/CHANGELOG.md index d278a5f9c..041ecf2f4 100644 --- a/source/funkin/data/stickers/CHANGELOG.md +++ b/source/funkin/data/stickers/CHANGELOG.md @@ -1,4 +1,4 @@ -# Sticker Set Data Schema Changelog +# Sticker Pack Data Schema Changelog All notable changes to this project will be documented in this file. @@ -6,4 +6,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [1.0.0] -Initial release. +Initial release. Sticker packs have been restructured compared to previous versions to be less complicated. diff --git a/source/funkin/data/stickers/StickerData.hx b/source/funkin/data/stickers/StickerData.hx index e0d82d323..c23511e82 100644 --- a/source/funkin/data/stickers/StickerData.hx +++ b/source/funkin/data/stickers/StickerData.hx @@ -1,8 +1,8 @@ package funkin.data.stickers; /** - * A type definition for a sticker set. - * It includes things like its name, the artist, and the stickers. + * A type definition for a sticker pack. + * It includes things like its name, the artist, and what sticker graphics to use. * @see https://lib.haxe.org/p/json2object/ */ typedef StickerData = @@ -17,11 +17,6 @@ typedef StickerData = */ public var name:String; - /** - * The asset key for this sticker set. - */ - public var assetPath:String; - /** * The artist of the sticker set. */ @@ -29,11 +24,7 @@ typedef StickerData = /** * The stickers in the set. + * This is simply a list of asset files to use. */ - public var stickers:Map>; - - /** - * The sticker packs in this set. - */ - public var stickerPacks:Map>; + public var stickers:Array; } diff --git a/source/funkin/data/stickers/StickerRegistry.hx b/source/funkin/data/stickers/StickerRegistry.hx index 08ec6194b..08c2b9669 100644 --- a/source/funkin/data/stickers/StickerRegistry.hx +++ b/source/funkin/data/stickers/StickerRegistry.hx @@ -1,13 +1,13 @@ package funkin.data.stickers; -import funkin.data.stickers.StickerSet; import funkin.data.stickers.StickerData; -import funkin.ui.transition.ScriptedStickerSet; +import funkin.ui.transition.stickers.StickerPack; +import funkin.ui.transition.stickers.ScriptedStickerPack; -class StickerRegistry extends BaseRegistry +class StickerRegistry extends BaseRegistry { /** - * The current version string for the sticker set data format. + * The current version string for the sticker pack data format. * Handle breaking changes by incrementing this value * and adding migration to the `migrateStickerData()` function. */ @@ -19,7 +19,12 @@ class StickerRegistry extends BaseRegistry public function new() { - super('STICKER', 'stickersets', STICKER_DATA_VERSION_RULE); + super('STICKER', 'stickerpacks', STICKER_DATA_VERSION_RULE); + } + + public function fetchDefault():StickerPack + { + return fetchEntry(Constants.DEFAULT_STICKER_PACK); } /** @@ -72,13 +77,13 @@ class StickerRegistry extends BaseRegistry return parser.value; } - function createScriptedEntry(clsName:String):StickerSet + function createScriptedEntry(clsName:String):StickerPack { - return ScriptedStickerSet.init(clsName, 'unknown'); + return ScriptedStickerPack.init(clsName, 'unknown'); } function getScriptedClassNames():Array { - return ScriptedStickerSet.listScriptClasses(); + return ScriptedStickerPack.listScriptClasses(); } } diff --git a/source/funkin/data/stickers/StickerSet.hx b/source/funkin/data/stickers/StickerSet.hx deleted file mode 100644 index 1cea8293a..000000000 --- a/source/funkin/data/stickers/StickerSet.hx +++ /dev/null @@ -1,109 +0,0 @@ -package funkin.data.stickers; - -import funkin.data.stickers.StickerData; -import funkin.data.stickers.StickerRegistry; -import funkin.data.IRegistryEntry; -import funkin.graphics.FunkinSprite; - -/** - * A class representing the data for a sticker set as displayed in the Sticker SubState. - */ -class StickerSet implements IRegistryEntry -{ - /** - * The internal ID for this sticker set. - */ - public final id:String; - - /** - * The full data for this sticker set. - */ - public final _data:StickerData; - - public function new(id:String) - { - this.id = id; - this._data = _fetchData(id); - - if (_data == null) - { - throw 'Could not parse sticker set data for id: $id'; - } - } - - /** - * Return the name of the sticker set. - * @return The name of the sticker set - */ - public function getStickerSetName():String - { - return _data.name; - } - - /** - * Return the artist of the sticker set. - * @return The list of artists - */ - public function getStickerSetArtist():String - { - return _data.artist; - } - - /** - * Get the asset key for the album art. - * @return The asset key - */ - public function getStickerSetAssetKey():String - { - return _data.assetPath; - } - - /** - * Gets the stickers for a given sticker name. - * @param stickerName The name of the sticker to get. - * @return The sticker. - */ - public function getStickers(stickerName:String):Array - { - return _data.stickers[stickerName]; - } - - /** - * Gets the sticker pack for a given pack name. - * @param packName The name of the pack to get. - * @return The sticker pack. - */ - public function getPack(packName:String):Array - { - return _data.stickerPacks[packName]; - } - - public function toString():String - { - return 'StickerSet($id)'; - } - - public function destroy():Void {} - - static function _fetchData(id:String):Null - { - return StickerRegistry.instance.parseEntryDataWithMigration(id, StickerRegistry.instance.fetchEntryVersion(id)); - } -} - -class StickerSprite extends FunkinSprite -{ - public var timing:Float = 0; - - public function new(x:Float, y:Float, filePath:String):Void - { - super(x, y); - if (!Assets.exists(Paths.image(filePath))) - { - throw 'File path does not exist! ($filePath)'; - } - loadTexture(filePath); - updateHitbox(); - scrollFactor.set(); - } -} diff --git a/source/funkin/play/GameOverSubState.hx b/source/funkin/play/GameOverSubState.hx index 6752a8009..c84a47f51 100644 --- a/source/funkin/play/GameOverSubState.hx +++ b/source/funkin/play/GameOverSubState.hx @@ -291,7 +291,7 @@ class GameOverSubState extends MusicBeatSubState } else { - var targetState:funkin.ui.transition.StickerSubState->FlxState = (PlayStatePlaylist.isStoryMode) ? (sticker) -> + var targetState:funkin.ui.transition.stickers.StickerSubState->FlxState = (PlayStatePlaylist.isStoryMode) ? (sticker) -> new StoryMenuState(sticker) : (sticker) -> FreeplayState.build(sticker); if (PlayStatePlaylist.isStoryMode) @@ -299,16 +299,20 @@ class GameOverSubState extends MusicBeatSubState PlayStatePlaylist.reset(); } - var playerCharacterId = PlayerRegistry.instance.getCharacterOwnerId(PlayState.instance.currentChart.characters.player); - var stickerSet = (playerCharacterId == "pico") ? "stickers-set-2" : "stickers-set-1"; - var stickerPack = switch (PlayState.instance.currentChart.song.id) - { - case "tutorial": "tutorial"; - case "darnell" | "lit-up" | "2hot": "weekend"; - default: "all"; - }; + var stickerPackId:Null = PlayState.instance.currentChart.stickerPack; - openSubState(new funkin.ui.transition.StickerSubState({targetState: targetState, stickerSet: stickerSet, stickerPack: stickerPack})); + if (stickerPackId == null) + { + var playerCharacterId = PlayerRegistry.instance.getCharacterOwnerId(PlayState.instance.currentChart.characters.player); + var playerCharacter = PlayerRegistry.instance.fetchEntry(playerCharacterId ?? Constants.DEFAULT_CHARACTER); + + if (playerCharacter != null) + { + stickerPackId = playerCharacter.getStickerPackID(); + } + } + + openSubState(new funkin.ui.transition.stickers.StickerSubState({targetState: targetState, stickerPack: stickerPackId})); } } diff --git a/source/funkin/play/PauseSubState.hx b/source/funkin/play/PauseSubState.hx index 325d96fef..977a7808a 100644 --- a/source/funkin/play/PauseSubState.hx +++ b/source/funkin/play/PauseSubState.hx @@ -737,8 +737,8 @@ class PauseSubState extends MusicBeatSubState FlxTransitionableState.skipNextTransIn = true; FlxTransitionableState.skipNextTransOut = true; - var targetState:funkin.ui.transition.StickerSubState->FlxState = (PlayStatePlaylist.isStoryMode) ? (sticker) -> new StoryMenuState(sticker) : (sticker) -> - FreeplayState.build(sticker); + var targetState:funkin.ui.transition.stickers.StickerSubState->FlxState = (PlayStatePlaylist.isStoryMode) ? (sticker) -> + new StoryMenuState(sticker) : (sticker) -> FreeplayState.build(sticker); // Do this AFTER because this resets the value of isStoryMode! if (PlayStatePlaylist.isStoryMode) @@ -746,16 +746,20 @@ class PauseSubState extends MusicBeatSubState PlayStatePlaylist.reset(); } - var playerCharacterId = PlayerRegistry.instance.getCharacterOwnerId(PlayState.instance.currentChart.characters.player); - var stickerSet = (playerCharacterId == "pico") ? "stickers-set-2" : "stickers-set-1"; - var stickerPack = switch (PlayState.instance.currentChart.song.id) - { - case "tutorial": "tutorial"; - case "darnell" | "lit-up" | "2hot": "weekend"; - default: "all"; - }; + var stickerPackId:Null = PlayState.instance.currentChart.stickerPack; - state.openSubState(new funkin.ui.transition.StickerSubState({targetState: targetState, stickerSet: stickerSet, stickerPack: stickerPack})); + if (stickerPackId == null) + { + var playerCharacterId = PlayerRegistry.instance.getCharacterOwnerId(PlayState.instance.currentChart.characters.player); + var playerCharacter = PlayerRegistry.instance.fetchEntry(playerCharacterId ?? Constants.DEFAULT_CHARACTER); + + if (playerCharacter != null) + { + stickerPackId = playerCharacter.getStickerPackID(); + } + } + + state.openSubState(new funkin.ui.transition.stickers.StickerSubState({targetState: targetState, stickerPack: stickerPackId})); } /** diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index a05c220d7..efd118be6 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -2946,7 +2946,7 @@ class PlayState extends MusicBeatSubState Highscore.talliesLevel = Highscore.combineTallies(Highscore.tallies, Highscore.talliesLevel); #if FEATURE_NEWGROUNDS - Leaderboards.submitSongScore(currentSong.id, suffixedDifficulty, data.score); + Leaderboards.submitSongScore(currentSong.id, suffixedDifficulty, songScore); #end if (!isPracticeMode && !isBotPlayMode) @@ -3304,6 +3304,7 @@ class PlayState extends MusicBeatSubState storyMode: PlayStatePlaylist.isStoryMode, songId: currentChart.song.id, difficultyId: currentDifficulty, + variationId: currentVariation, characterId: currentChart.characters.player, title: PlayStatePlaylist.isStoryMode ? ('${PlayStatePlaylist.campaignTitle}') : ('${currentChart.songName} by ${currentChart.songArtist}'), prevScoreData: prevScoreData, diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx index 3cec331e3..591bbef49 100644 --- a/source/funkin/play/ResultState.hx +++ b/source/funkin/play/ResultState.hx @@ -1,34 +1,36 @@ package funkin.play; -import funkin.ui.story.StoryMenuState; -import funkin.graphics.adobeanimate.FlxAtlasSprite; +import flixel.addons.display.FlxBackdrop; +import flixel.effects.FlxFlicker; import flixel.FlxSprite; import flixel.FlxSubState; -import funkin.graphics.FunkinSprite; -import flixel.effects.FlxFlicker; import flixel.graphics.frames.FlxBitmapFont; import flixel.group.FlxGroup.FlxTypedGroup; import flixel.math.FlxPoint; -import funkin.ui.MusicBeatSubState; import flixel.math.FlxRect; import flixel.text.FlxBitmapText; -import funkin.data.freeplay.player.PlayerRegistry; -import funkin.data.freeplay.player.PlayerData.PlayerResultsAnimationData; -import funkin.ui.freeplay.charselect.PlayableCharacter; -import flixel.util.FlxColor; import flixel.tweens.FlxEase; -import funkin.graphics.FunkinCamera; -import funkin.ui.freeplay.FreeplayState; import flixel.tweens.FlxTween; -import flixel.addons.display.FlxBackdrop; -import funkin.audio.FunkinSound; +import flixel.util.FlxColor; import flixel.util.FlxGradient; import flixel.util.FlxTimer; -import funkin.play.scoring.Scoring; -import funkin.save.Save.SaveScoreData; +import funkin.audio.FunkinSound; +import funkin.data.freeplay.player.PlayerData.PlayerResultsAnimationData; +import funkin.data.freeplay.player.PlayerRegistry; +import funkin.graphics.adobeanimate.FlxAtlasSprite; +import funkin.graphics.FunkinCamera; +import funkin.graphics.FunkinSprite; import funkin.graphics.shaders.LeftMaskShader; -import funkin.play.components.TallyCounter; import funkin.play.components.ClearPercentCounter; +import funkin.play.components.TallyCounter; +import funkin.play.scoring.Scoring; +import funkin.play.song.Song; +import funkin.data.song.SongRegistry; +import funkin.save.Save.SaveScoreData; +import funkin.ui.freeplay.charselect.PlayableCharacter; +import funkin.ui.freeplay.FreeplayState; +import funkin.ui.MusicBeatSubState; +import funkin.ui.story.StoryMenuState; #if FEATURE_NEWGROUNDS import funkin.api.newgrounds.Medals; #end @@ -72,9 +74,10 @@ class ResultState extends MusicBeatSubState delay:Float }> = []; - var playerCharacterId:Null; + var playerCharacterId:Null = null; + var playerCharacter:Null = null; - var introMusicAudio:Null; + var introMusicAudio:Null = null; var rankBg:FunkinSprite; final cameraBG:FunkinCamera; @@ -171,7 +174,7 @@ class ResultState extends MusicBeatSubState // Fetch playable character data. Default to BF on the results screen if we can't find it. playerCharacterId = PlayerRegistry.instance.getCharacterOwnerId(params.characterId); - var playerCharacter:Null = PlayerRegistry.instance.fetchEntry(playerCharacterId ?? 'bf'); + playerCharacter = PlayerRegistry.instance.fetchEntry(playerCharacterId ?? 'bf'); trace('Got playable character: ${playerCharacter?.getName()}'); // Query JSON data based on the rank, then use that to build the animation(s) the player sees. @@ -267,7 +270,7 @@ class ResultState extends MusicBeatSubState } } - var diffSpr:String = 'diff_${params?.difficultyId ?? 'Normal'}'; + var diffSpr:String = 'diff_${params?.difficultyId ?? Constants.DEFAULT_DIFFICULTY}'; difficulty.loadGraphic(Paths.image("resultScreen/" + diffSpr)); add(difficulty); @@ -698,7 +701,6 @@ class ResultState extends MusicBeatSubState override function update(elapsed:Float):Void { - // maskShaderSongName.swagSprX = songName.x; maskShaderDifficulty.swagSprX = difficulty.x; if (movingSongStuff) @@ -774,13 +776,18 @@ class ResultState extends MusicBeatSubState var shouldTween = false; var shouldUseSubstate = false; - var stickerSet = (playerCharacterId == "pico") ? "stickers-set-2" : "stickers-set-1"; - var stickerPack = switch (PlayState.instance?.currentChart?.song?.id) + var stickerPackId:Null = null; + + var song:Null = params.songId == null ? null : SongRegistry.instance.fetchEntry(params.songId); + + if (song != null) { - case "tutorial": "tutorial"; - case "darnell" | "lit-up" | "2hot": "weekend"; - default: "all"; - }; + stickerPackId = song.getStickerPackId(params?.difficultyId ?? Constants.DEFAULT_DIFFICULTY, params?.variationId ?? Constants.DEFAULT_VARIATION); + } + if (stickerPackId == null && playerCharacter != null) + { + stickerPackId = playerCharacter.getStickerPackID(); + } if (params.storyMode) { @@ -803,11 +810,10 @@ class ResultState extends MusicBeatSubState // No new characters. shouldTween = false; shouldUseSubstate = true; - targetState = new funkin.ui.transition.StickerSubState( + targetState = new funkin.ui.transition.stickers.StickerSubState( { targetState: (sticker) -> new StoryMenuState(sticker), - stickerSet: stickerSet, - stickerPack: stickerPack + stickerPack: stickerPackId }); } } @@ -841,11 +847,10 @@ class ResultState extends MusicBeatSubState { shouldTween = false; shouldUseSubstate = true; - targetState = new funkin.ui.transition.StickerSubState( + targetState = new funkin.ui.transition.stickers.StickerSubState( { targetState: (sticker) -> FreeplayState.build(null, sticker), - stickerSet: stickerSet, - stickerPack: stickerPack + stickerPack: stickerPackId }); } } @@ -925,10 +930,16 @@ typedef ResultsStateParams = /** * The difficulty ID of the song/week we just played. - * @default Normal + * @default `Constants.DEFAULT_DIFFICULTY` */ var ?difficultyId:String; + /** + * The variation ID of the song/week we just played. + * @default `Constants.DEFAULT_VARIATION` + */ + var ?variationId:String; + /** * The score, accuracy, and judgements. */ diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index 17585973f..39445e27e 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -289,6 +289,14 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry + { + var diff:Null = getDifficulty(diffId, variation); + if (diff == null) return null; + + return diff.stickerPack; + } + /** * Populate the difficulty data from the provided metadata. * Does not load chart data (that is triggered later when we want to play the song). @@ -331,6 +339,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry = null; + public var stickerPack:Null = null; public function new(song:Song, diffId:String, variation:String) { diff --git a/source/funkin/ui/debug/DebugMenuSubState.hx b/source/funkin/ui/debug/DebugMenuSubState.hx index 158aa8b48..1b7d990c6 100644 --- a/source/funkin/ui/debug/DebugMenuSubState.hx +++ b/source/funkin/ui/debug/DebugMenuSubState.hx @@ -123,7 +123,7 @@ class DebugMenuSubState extends MusicBeatSubState function testStickers() { - openSubState(new funkin.ui.transition.StickerSubState({})); + openSubState(new funkin.ui.transition.stickers.StickerSubState({})); trace('opened stickers'); } diff --git a/source/funkin/ui/debug/results/ResultsDebugSubState.hx b/source/funkin/ui/debug/results/ResultsDebugSubState.hx index 81628f30f..1d4245258 100644 --- a/source/funkin/ui/debug/results/ResultsDebugSubState.hx +++ b/source/funkin/ui/debug/results/ResultsDebugSubState.hx @@ -125,6 +125,7 @@ class ResultsDebugSubState extends MusicBeatSubState songId: "cum", characterId: "bf", difficultyId: "nightmare", + variationId: "erect", isNewHighscore: true, isPracticeMode: true, // Invalidates achievements/scores. isBotPlayMode: true, // Invalidates achievements/scores. diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index b02dbc7c7..677ef32ba 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -40,7 +40,7 @@ import funkin.ui.mainmenu.MainMenuState; import funkin.ui.MusicBeatSubState; import funkin.ui.story.Level; import funkin.ui.transition.LoadingState; -import funkin.ui.transition.StickerSubState; +import funkin.ui.transition.stickers.StickerSubState; import funkin.util.MathUtil; import funkin.util.SortUtil; import openfl.display.BlendMode; diff --git a/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx b/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx index 408a91672..681c11910 100644 --- a/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx +++ b/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx @@ -79,6 +79,11 @@ class PlayableCharacter implements IRegistryEntry return false; } + public function getStickerPackID():String + { + return _data?.stickerPack ?? Constants.DEFAULT_STICKER_PACK; + } + public function getFreeplayStyleID():String { return _data?.freeplayStyle ?? Constants.DEFAULT_FREEPLAY_STYLE; diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx index 3685e0f10..c0fbeef79 100644 --- a/source/funkin/ui/story/StoryMenuState.hx +++ b/source/funkin/ui/story/StoryMenuState.hx @@ -21,7 +21,7 @@ import funkin.save.Save.SaveScoreData; import funkin.ui.mainmenu.MainMenuState; import funkin.ui.MusicBeatState; import funkin.ui.transition.LoadingState; -import funkin.ui.transition.StickerSubState; +import funkin.ui.transition.stickers.StickerSubState; import funkin.util.MathUtil; import openfl.utils.Assets; #if FEATURE_DISCORD_RPC diff --git a/source/funkin/ui/transition/ScriptedStickerSet.hx b/source/funkin/ui/transition/ScriptedStickerSet.hx deleted file mode 100644 index aaada82db..000000000 --- a/source/funkin/ui/transition/ScriptedStickerSet.hx +++ /dev/null @@ -1,8 +0,0 @@ -package funkin.ui.transition; - -/** - * A script that can be tied to a StickerSet. - * Create a scripted class that extends StickerSet to use this. - */ -@:hscriptClass -class ScriptedStickerSet extends funkin.data.stickers.StickerSet implements polymod.hscript.HScriptedClass {} diff --git a/source/funkin/ui/transition/stickers/ScriptedStickerPack.hx b/source/funkin/ui/transition/stickers/ScriptedStickerPack.hx new file mode 100644 index 000000000..34a0efd2c --- /dev/null +++ b/source/funkin/ui/transition/stickers/ScriptedStickerPack.hx @@ -0,0 +1,8 @@ +package funkin.ui.transition.stickers; + +/** + * A script that can be tied to a StickerPack. + * Create a scripted class that extends StickerPack to use this. + */ +@:hscriptClass +class ScriptedStickerPack extends funkin.ui.transition.stickers.StickerPack implements polymod.hscript.HScriptedClass {} diff --git a/source/funkin/ui/transition/stickers/StickerPack.hx b/source/funkin/ui/transition/stickers/StickerPack.hx new file mode 100644 index 000000000..ecd79bf37 --- /dev/null +++ b/source/funkin/ui/transition/stickers/StickerPack.hx @@ -0,0 +1,82 @@ +package funkin.ui.transition.stickers; + +import funkin.data.stickers.StickerData; +import funkin.data.stickers.StickerRegistry; +import funkin.data.IRegistryEntry; +import funkin.graphics.FunkinSprite; + +/** + * A class representing the data for a sticker pack as displayed in the Sticker SubState. + */ +class StickerPack implements IRegistryEntry +{ + /** + * The internal ID for this sticker pack. + */ + public final id:String; + + /** + * The full data for this sticker pack. + */ + public final _data:StickerData; + + public function new(id:String) + { + this.id = id; + this._data = _fetchData(id); + + if (_data == null) + { + throw 'Could not parse sticker pack data for id: $id'; + } + } + + /** + * Return the name of the sticker pack. + * @return The name of the sticker pack + */ + public function getStickerPackName():String + { + return _data.name; + } + + /** + * Return the artist of the sticker pack. + * @return The list of artists + */ + public function getStickerPackArtist():String + { + return _data.artist; + } + + /** + * Gets a list of all the sticker assets available in the pack. + * @return The list of stickers as raw strings. + */ + public function getStickers():Array + { + return _data.stickers; + } + + /** + * Retrieve a random sticker from the pack. + * @param last Whether this will be the last sticker to be placed on the screen. + * @return An asset path to a sticker to display. + */ + public function getRandomStickerPath(last:Bool):String + { + return FlxG.random.getObject(getStickers()); + } + + public function toString():String + { + return 'StickerPack($id)'; + } + + public function destroy():Void {} + + static function _fetchData(id:String):Null + { + return StickerRegistry.instance.parseEntryDataWithMigration(id, StickerRegistry.instance.fetchEntryVersion(id)); + } +} diff --git a/source/funkin/ui/transition/stickers/StickerSprite.hx b/source/funkin/ui/transition/stickers/StickerSprite.hx new file mode 100644 index 000000000..b993587bc --- /dev/null +++ b/source/funkin/ui/transition/stickers/StickerSprite.hx @@ -0,0 +1,16 @@ +package funkin.ui.transition.stickers; + +import funkin.graphics.FunkinSprite; + +class StickerSprite extends FunkinSprite +{ + public var timing:Float = 0; + + public function new(x:Float, y:Float, filePath:String):Void + { + super(x, y); + loadTexture(filePath); + updateHitbox(); + scrollFactor.set(); + } +} diff --git a/source/funkin/ui/transition/StickerSubState.hx b/source/funkin/ui/transition/stickers/StickerSubState.hx similarity index 72% rename from source/funkin/ui/transition/StickerSubState.hx rename to source/funkin/ui/transition/stickers/StickerSubState.hx index 5df961690..9061cb214 100644 --- a/source/funkin/ui/transition/StickerSubState.hx +++ b/source/funkin/ui/transition/stickers/StickerSubState.hx @@ -1,34 +1,38 @@ -package funkin.ui.transition; +package funkin.ui.transition.stickers; -import haxe.Json; -import funkin.graphics.FunkinSprite; -// import flxtyped group -import funkin.ui.MusicBeatSubState; -import flixel.group.FlxGroup.FlxTypedGroup; -import flixel.util.FlxTimer; +import flixel.addons.transition.FlxTransitionableState; import flixel.FlxG; +import flixel.FlxState; +import flixel.group.FlxGroup.FlxTypedGroup; import flixel.math.FlxMath; import flixel.util.FlxSort; -import funkin.ui.mainmenu.MainMenuState; -import flixel.addons.transition.FlxTransitionableState; -import openfl.display.BitmapData; -import funkin.data.stickers.StickerRegistry; -import funkin.data.stickers.StickerSet; -import funkin.ui.freeplay.FreeplayState; -import openfl.geom.Matrix; +import flixel.util.FlxTimer; import funkin.audio.FunkinSound; -import flixel.FlxState; +import funkin.data.stickers.StickerRegistry; +import funkin.graphics.FunkinSprite; +import funkin.ui.freeplay.FreeplayState; +import funkin.ui.MusicBeatSubState; +import funkin.ui.transition.stickers.StickerPack; using Lambda; using StringTools; typedef StickerSubStateParams = { + /* + * The state to transition into. + */ ?targetState:StickerSubState->FlxState, - ?stickerSet:String, + /** + * The sticker pack to retrieve and use. + * @default `Constants.DEFAULT_STICKER_PACK` + */ ?stickerPack:String, + /** + * An existing set of stickers to transition out with. + */ ?oldStickers:Array, } @@ -37,9 +41,6 @@ class StickerSubState extends MusicBeatSubState { public var grpStickers:FlxTypedGroup; - // yes... a damn OpenFL sprite!!! - // public var dipshit:Sprite; - /** * The state to switch to after the stickers are done. * This is a FUNCTION so we can pass it directly to `FlxG.switchState()`, @@ -47,9 +48,8 @@ class StickerSubState extends MusicBeatSubState */ var targetState:StickerSubState->FlxState; - // what stickers to use - var stickerSet:String; - var stickerPack:String; + var stickerPackId:String; + var stickerPack:StickerPack; // what "folders" to potentially load from (as of writing only "keys" exist) var soundSelections:Array = []; @@ -61,21 +61,20 @@ class StickerSubState extends MusicBeatSubState { super(); - this.stickerSet = params.stickerSet ?? 'stickers-set-1'; - this.stickerPack = params.stickerPack ?? 'all'; + // Define the target state, with a default fallback. + this.targetState = params?.targetState ?? (sticker) -> FreeplayState.build(null, sticker); - this.targetState = params.targetState ?? (sticker) -> new MainMenuState(); + this.stickerPackId = params.stickerPack ?? Constants.DEFAULT_STICKER_PACK; - // todo still - // make sure that ONLY plays mp3/ogg files - // if there's no mp3/ogg file, then it regenerates/reloads the random folder + var targetStickerPack = StickerRegistry.instance.fetchEntry(this.stickerPackId); + this.stickerPack = targetStickerPack ?? StickerRegistry.instance.fetchDefault(); + + // TODO: Make this tied to the sticker pack more closely. var assetsInList = Assets.list(); - var soundFilterFunc = function(a:String) { return a.startsWith('assets/shared/sounds/stickersounds/'); }; - soundSelections = assetsInList.filter(soundFilterFunc); soundSelections = soundSelections.map(function(a:String) { return a.replace('assets/shared/sounds/stickersounds/', '').split('/')[0]; @@ -94,12 +93,10 @@ class StickerSubState extends MusicBeatSubState soundSelections.push(i); } - trace(soundSelections); - soundSelection = FlxG.random.getObject(soundSelections); var filterFunc = function(a:String) { - return a.startsWith('assets/shared/sounds/stickersounds/' + this.soundSelection + '/'); + return a.startsWith('assets/shared/sounds/stickersounds/' + soundSelection + '/'); }; var assetsInList3 = Assets.list(); sounds = assetsInList3.filter(filterFunc); @@ -109,8 +106,6 @@ class StickerSubState extends MusicBeatSubState sounds[i] = sounds[i].substring(0, sounds[i].lastIndexOf('.')); } - trace(sounds); - // makes the stickers on the most recent camera, which is more often than not... a UI camera!! // grpStickers.cameras = [FlxG.cameras.list[FlxG.cameras.list.length - 1]]; grpStickers.cameras = FlxG.cameras.list; @@ -125,7 +120,9 @@ class StickerSubState extends MusicBeatSubState degenStickers(); } else + { regenStickers(); + } } public function degenStickers():Void @@ -170,26 +167,14 @@ class StickerSubState extends MusicBeatSubState grpStickers.clear(); } - var stickerSets:Array = StickerRegistry.instance.listEntryIds(); - var stickers:Map> = new Map>(); - - for (stickerSets in stickerInfo.getPack(this.stickerPack)) - for (stickerSets in stickerInfo.getPack("all")) - var assetKey:String = stickerSet.getStickerSetAssetKey(); - for (sticker in stickerSet.getPack("all")) - // add the asset key at the beginning of each sticker name because it's just easier lol - var stickerPack:Array = stickerSet.getStickers(sticker).map(s -> '${assetKey}/${s}'); - stickers.set(sticker, stickerPack); - } - } - + // Initialize stickers at each point on the screen, then shuffle up the order they will get placed. + // This ensures stickers consistently cover the screen. var xPos:Float = -100; var yPos:Float = -100; while (xPos <= FlxG.width) { - var stickerSet:String = FlxG.random.getObject(stickers.keyValues()); - var sticker:String = FlxG.random.getObject(stickers.get(stickerSet)); - var sticky:StickerSprite = new StickerSprite(0, 0, sticker); + var stickerPath:String = stickerPack.getRandomStickerPath(false); + var sticky:StickerSprite = new StickerSprite(0, 0, stickerPath); sticky.visible = false; sticky.x = xPos; @@ -211,32 +196,14 @@ class StickerSubState extends MusicBeatSubState FlxG.random.shuffle(grpStickers.members); - // var stickerCount:Int = 0; - - // for (w in 0...6) - // { - // var xPos:Float = FlxG.width * (w / 6); - // for (h in 0...6) - // { - // var yPos:Float = FlxG.height * (h / 6); - // var sticker = grpStickers.members[stickerCount]; - // xPos -= sticker.width / 2; - // yPos -= sticker.height * 0.9; - // sticker.x = xPos; - // sticker.y = yPos; - - // stickerCount++; - // } - // } - - // for (ind => sticker in grpStickers.members) - // { - // sticker.x = (ind % 8) * sticker.width; - // var yShit:Int = Math.floor(ind / 8); - // sticker.y += yShit * sticker.height; - // // scales it juuuust a smidge - // sticker.y += 20 * yShit; - // } + // Creates a new sticker for the very center. + var lastStickerPath:String = stickerPack.getRandomStickerPath(true); + var lastSticker:StickerSprite = new StickerSprite(0, 0, lastStickerPath); + lastSticker.visible = false; + lastSticker.updateHitbox(); + lastSticker.angle = 0; + lastSticker.screenCenter(); + grpStickers.add(lastSticker); // another damn for loop... apologies!!! for (ind => sticker in grpStickers.members) @@ -298,12 +265,6 @@ class StickerSubState extends MusicBeatSubState grpStickers.sort((ord, a, b) -> { return FlxSort.byValues(ord, a.timing, b.timing); }); - - // centers the very last sticker - var lastOne:StickerSprite = grpStickers.members[grpStickers.members.length - 1]; - lastOne.updateHitbox(); - lastOne.angle = 0; - lastOne.screenCenter(); } override public function update(elapsed:Float):Void diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index cb5ab10c5..19105a1ff 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -238,6 +238,11 @@ class Constants */ public static final DEFAULT_VARIATION_LIST:Array = ['default', 'erect', 'pico', 'bf']; + /** + * Default sticker pack for transitions + */ + public static final DEFAULT_STICKER_PACK:String = 'default'; + /** * The default intensity multiplier for camera bops. * Prolly needs to be tuned bc it's a multiplier now. From 9f3711bf43e1d35779e2e95a0664ee50a5097022 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Wed, 19 Mar 2025 23:24:01 -0400 Subject: [PATCH 11/40] Scoring rebalance: Misses are now worth -250 points (up from -10). --- source/funkin/play/PlayState.hx | 2 +- source/funkin/play/scoring/Scoring.hx | 21 ++++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index efd118be6..b35da02a7 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -2606,7 +2606,7 @@ class PlayState extends MusicBeatSubState } vocals.playerVolume = 0; - applyScore(-10, 'miss', healthChange, true); + applyScore(Scoring.getMissScore(), 'miss', healthChange, true); if (playSound) { diff --git a/source/funkin/play/scoring/Scoring.hx b/source/funkin/play/scoring/Scoring.hx index 5bd6efc25..f495ceede 100644 --- a/source/funkin/play/scoring/Scoring.hx +++ b/source/funkin/play/scoring/Scoring.hx @@ -69,6 +69,19 @@ class Scoring } } + public static function getMissScore(scoringSystem:ScoringSystem = PBOT1):Int + { + return switch (scoringSystem) + { + case LEGACY: LEGACY_MISS_SCORE; + case WEEK7: WEEK7_MISS_SCORE; + case PBOT1: PBOT1_MISS_SCORE; + default: + FlxG.log.error('Unknown scoring system: ${scoringSystem}'); + 0; + } + } + /** * The maximum score a note can receive. */ @@ -92,7 +105,7 @@ class Scoring /** * The score a note receives when it is missed. */ - public static final PBOT1_MISS_SCORE:Int = 0; + public static final PBOT1_MISS_SCORE:Int = -250; /** * The threshold at which a note hit is considered perfect and always given the max score. @@ -228,6 +241,11 @@ class Scoring */ public static final LEGACY_SHIT_SCORE:Int = 50; + /** + * The score a note receives when missed. + */ + public static final LEGACY_MISS_SCORE:Int = -10; + static function scoreNoteLEGACY(msTiming:Float):Int { var absTiming:Float = Math.abs(msTiming); @@ -276,6 +294,7 @@ class Scoring public static final WEEK7_BAD_THRESHOLD:Float = 0.8; // 80% of the hit window, or ~125ms public static final WEEK7_GOOD_THRESHOLD:Float = 0.55; // 55% of the hit window, or ~91ms public static final WEEK7_SICK_THRESHOLD:Float = 0.2; // 20% of the hit window, or ~33ms + public static final WEEK7_MISS_SCORE:Int = -10; public static final WEEK7_SHIT_SCORE:Int = 50; public static final WEEK7_BAD_SCORE:Int = 100; public static final WEEK7_GOOD_SCORE:Int = 200; From da686ed0c497528e0ab6dce0ceec53a7a3730627 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Wed, 19 Mar 2025 23:26:34 -0400 Subject: [PATCH 12/40] Scoring rebalance: Misses now reduce your final grade, which impacts your rank --- source/funkin/play/scoring/Scoring.hx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/source/funkin/play/scoring/Scoring.hx b/source/funkin/play/scoring/Scoring.hx index f495ceede..f73d4b0df 100644 --- a/source/funkin/play/scoring/Scoring.hx +++ b/source/funkin/play/scoring/Scoring.hx @@ -382,10 +382,9 @@ class Scoring // Else, use the standard grades - // Grade % (only good and sick), 1.00 is a full combo + // Final Grade = (Sick + Good - Miss) / (Total Notes) + var grade = (scoreData.tallies.sick + scoreData.tallies.good) / scoreData.tallies.totalNotes; - // Clear % (including bad and shit). 1.00 is a full clear but not a full combo - var clear = (scoreData.tallies.totalNotesHit) / scoreData.tallies.totalNotes; if (grade == Constants.RANK_PERFECT_THRESHOLD) { From 550bf95c762eefc434a97c8cfb9813ace9a2c8ad Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Wed, 19 Mar 2025 23:57:21 -0400 Subject: [PATCH 13/40] Split miss penalty and ghost miss penalty into two variables (but make them equal for now). --- source/funkin/play/PlayState.hx | 4 ++-- source/funkin/util/Constants.hx | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index b35da02a7..83ff162f0 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -2339,7 +2339,7 @@ class PlayState extends MusicBeatSubState { // Call an event to allow canceling the note miss. // NOTE: This is what handles the character animations! - var event:NoteScriptEvent = new NoteScriptEvent(NOTE_MISS, note, -Constants.HEALTH_MISS_PENALTY, 0, true); + var event:NoteScriptEvent = new NoteScriptEvent(NOTE_MISS, note, Constants.HEALTH_MISS_PENALTY, 0, true); dispatchEvent(event); // Calling event.cancelEvent() skips all the other logic! Neat! @@ -2627,7 +2627,7 @@ class PlayState extends MusicBeatSubState { var event:GhostMissNoteScriptEvent = new GhostMissNoteScriptEvent(direction, // Direction missed in. hasPossibleNotes, // Whether there was a note you could have hit. - - 1 * Constants.HEALTH_MISS_PENALTY, // How much health to add (negative). + Constants.HEALTH_GHOST_MISS_PENALTY, // How much health to add (negative). - 10 // Amount of score to add (negative). ); dispatchEvent(event); diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index 19105a1ff..015476255 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -468,12 +468,12 @@ class Constants /** * The amount of health the player loses upon missing a note. */ - public static final HEALTH_MISS_PENALTY:Float = 4.0 / 100.0 * HEALTH_MAX; // 4.0% + public static final HEALTH_MISS_PENALTY:Float = -4.0 / 100.0 * HEALTH_MAX; // 4.0% /** * The amount of health the player loses upon pressing a key when no note is there. */ - public static final HEALTH_GHOST_MISS_PENALTY:Float = 2.0 / 100.0 * HEALTH_MAX; // 2.0% + public static final HEALTH_GHOST_MISS_PENALTY:Float = -4.0 / 100.0 * HEALTH_MAX; // 2.0% /** * The amount of health the player loses upon letting go of a hold note while it is still going. @@ -483,7 +483,7 @@ class Constants /** * The amount of health the player loses upon hitting a mine. */ - public static final HEALTH_MINE_PENALTY:Float = 15.0 / 100.0 * HEALTH_MAX; // 15.0% + public static final HEALTH_MINE_PENALTY:Float = -15.0 / 100.0 * HEALTH_MAX; // 15.0% /** * SCORE VALUES From 429f6f9bc0b85cb35bb96e9b548677c723b4770c Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 20 Mar 2025 00:39:52 -0400 Subject: [PATCH 14/40] Remove the unused input splitter. --- source/funkin/play/PlayState.hx | 62 --------------------------------- 1 file changed, 62 deletions(-) diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 83ff162f0..0aa82b8df 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -837,8 +837,6 @@ class PlayState extends MusicBeatSubState startingSong = true; isPlayerDying = false; - inputSpitter = []; - // Reset music properly. if (FlxG.sound.music != null) { @@ -2582,27 +2580,6 @@ class PlayState extends MusicBeatSubState { if (pressArray[i]) indices.push(i); } - if (indices.length > 0) - { - for (i in 0...indices.length) - { - inputSpitter.push( - { - t: Std.int(Conductor.instance.songPosition), - d: indices[i], - l: 20 - }); - } - } - else - { - inputSpitter.push( - { - t: Std.int(Conductor.instance.songPosition), - d: -1, - l: 20 - }); - } } vocals.playerVolume = 0; @@ -2652,15 +2629,6 @@ class PlayState extends MusicBeatSubState { if (pressArray[i]) indices.push(i); } - for (i in 0...indices.length) - { - inputSpitter.push( - { - t: Std.int(Conductor.instance.songPosition), - d: indices[i], - l: 20 - }); - } } if (event.playSound) @@ -2734,8 +2702,6 @@ class PlayState extends MusicBeatSubState // SHIFT+PAGEDOWN: Skip backward twenty sections. if (FlxG.keys.justPressed.PAGEDOWN) changeSection(FlxG.keys.pressed.SHIFT ? -20 : -2); #end - - if (FlxG.keys.justPressed.B) trace(inputSpitter.join('\n')); } /** @@ -2801,27 +2767,6 @@ class PlayState extends MusicBeatSubState { if (pressArray[i]) indices.push(i); } - if (indices.length > 0) - { - for (i in 0...indices.length) - { - inputSpitter.push( - { - t: Std.int(Conductor.instance.songPosition), - d: indices[i], - l: 20 - }); - } - } - else - { - inputSpitter.push( - { - t: Std.int(Conductor.instance.songPosition), - d: -1, - l: 20 - }); - } } comboPopUps.displayRating(daRating); if (combo >= 10) comboPopUps.displayCombo(combo); @@ -2905,13 +2850,6 @@ class PlayState extends MusicBeatSubState dispatchEvent(event); if (event.eventCanceled) return; - #if sys - // spitter for ravy, teehee!! - var writer = new json2object.JsonWriter>(); - var output = writer.write(inputSpitter, ' '); - sys.io.File.saveContent("./scores.json", output); - #end - deathCounter = 0; // TODO: This line of code makes me sad, but you can't really fix it without a breaking migration. From 72e9fb641ee63cb92516a3b91e0341316e0969af Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 20 Mar 2025 00:41:00 -0400 Subject: [PATCH 15/40] Scoring rework: Reduce health given by hold notes by 20%. --- source/funkin/util/Constants.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index 015476255..dad6a706c 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -463,7 +463,7 @@ class Constants /** * The amount of health the player gains, while holding a hold note, per second. */ - public static final HEALTH_HOLD_BONUS_PER_SECOND:Float = 7.5 / 100.0 * HEALTH_MAX; // +7.5% / second + public static final HEALTH_HOLD_BONUS_PER_SECOND:Float = 6.0 / 100.0 * HEALTH_MAX; // +6.0% / second /** * The amount of health the player loses upon missing a note. From 577421a7829cf423976f800ab77015990b9d0c12 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 20 Mar 2025 03:19:29 -0400 Subject: [PATCH 16/40] Scoring rework: Penalize the player (deducting health and score) for dropping a hold note before it's finished. --- source/funkin/modding/IScriptedClass.hx | 5 +++ source/funkin/modding/events/ScriptEvent.hx | 43 ++++++++++++++++++ .../modding/events/ScriptEventDispatcher.hx | 3 ++ .../funkin/modding/events/ScriptEventType.hx | 8 ++++ source/funkin/modding/module/Module.hx | 2 + source/funkin/play/PlayState.hx | 39 +++++++++++++--- source/funkin/play/character/BaseCharacter.hx | 29 ++++++++++-- source/funkin/play/notes/notekind/NoteKind.hx | 2 + .../play/notes/notekind/NoteKindManager.hx | 2 +- source/funkin/play/song/Song.hx | 44 ++++++++++--------- source/funkin/play/stage/Bopper.hx | 2 + source/funkin/play/stage/Stage.hx | 2 + .../ui/haxeui/components/CharacterPlayer.hx | 11 +++++ source/funkin/util/Constants.hx | 21 +++++++-- 14 files changed, 179 insertions(+), 34 deletions(-) diff --git a/source/funkin/modding/IScriptedClass.hx b/source/funkin/modding/IScriptedClass.hx index 11f1d5d66..f739f59b7 100644 --- a/source/funkin/modding/IScriptedClass.hx +++ b/source/funkin/modding/IScriptedClass.hx @@ -74,6 +74,11 @@ interface INoteScriptedClass extends IScriptedClass * Called when EITHER player (usually the player) misses a note. */ public function onNoteMiss(event:NoteScriptEvent):Void; + + /** + * Called when EITHER player (usually the player) drops a hold note. + */ + public function onNoteHoldDrop(event:HoldNoteScriptEvent):Void; } /** diff --git a/source/funkin/modding/events/ScriptEvent.hx b/source/funkin/modding/events/ScriptEvent.hx index 43f8fc28b..6f4742a29 100644 --- a/source/funkin/modding/events/ScriptEvent.hx +++ b/source/funkin/modding/events/ScriptEvent.hx @@ -5,6 +5,7 @@ import funkin.data.song.SongData.SongEventData; import flixel.FlxState; import flixel.FlxSubState; import funkin.play.notes.NoteSprite; +import funkin.play.notes.SustainTrail; import funkin.play.cutscene.dialogue.Conversation; import funkin.play.Countdown.CountdownStep; import funkin.play.notes.NoteDirection; @@ -226,6 +227,48 @@ class GhostMissNoteScriptEvent extends ScriptEvent } } +class HoldNoteScriptEvent extends NoteScriptEvent +{ + /** + * The hold note that was hit (or dropped). + */ + public var holdNote:SustainTrail; + + /** + * The score the player received for hitting the note. + */ + public var score:Int; + + /** + * If the hit causes a combo break. + */ + public var isComboBreak:Bool = false; + + /** + * The time difference when the player hit the note + */ + public var hitDiff:Float = 0; + + /** + * Whether this note hit causes a note splash to display. + * Defaults to true only on "sick" notes. + */ + public var doesNotesplash:Bool = false; + + public function new(type:ScriptEventType, holdNote:SustainTrail, healthChange:Float, score:Int, isComboBreak:Bool, cancelable:Bool = false):Void + { + super(type, null, healthChange, comboCount, true); + this.holdNote = holdNote; + this.score = score; + this.isComboBreak = isComboBreak; + } + + public override function toString():String + { + return 'HoldNoteScriptEvent(type=$type, holdNote=$holdNote, healthChange=$healthChange, score=$score, isComboBreak=$isComboBreak, cancelable=$cancelable)'; + } +} + /** * An event that is fired when the song reaches an event. */ diff --git a/source/funkin/modding/events/ScriptEventDispatcher.hx b/source/funkin/modding/events/ScriptEventDispatcher.hx index 15d4dfaf6..7d6e3992f 100644 --- a/source/funkin/modding/events/ScriptEventDispatcher.hx +++ b/source/funkin/modding/events/ScriptEventDispatcher.hx @@ -90,6 +90,9 @@ class ScriptEventDispatcher case NOTE_MISS: t.onNoteMiss(cast event); return; + case NOTE_HOLD_DROP: + t.onNoteHoldDrop(cast event); + return; default: // Continue; } } diff --git a/source/funkin/modding/events/ScriptEventType.hx b/source/funkin/modding/events/ScriptEventType.hx index e44780dcf..02de0dc1a 100644 --- a/source/funkin/modding/events/ScriptEventType.hx +++ b/source/funkin/modding/events/ScriptEventType.hx @@ -88,6 +88,14 @@ enum abstract ScriptEventType(String) from String to String */ var NOTE_MISS = 'NOTE_MISS'; + /** + * Called when a character lets go of a hold note. + * Important information such as note data, player/opponent, etc. are all provided. + * + * This event is not cancelable. + */ + var NOTE_HOLD_DROP = 'NOTE_HOLD_DROP'; + /** * Called when a character presses a note when there was none there, causing them to lose health. * Important information such as direction pressed, etc. are all provided. diff --git a/source/funkin/modding/module/Module.hx b/source/funkin/modding/module/Module.hx index 012b9869f..d53639f09 100644 --- a/source/funkin/modding/module/Module.hx +++ b/source/funkin/modding/module/Module.hx @@ -122,6 +122,8 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte */ public function onNoteMiss(event:NoteScriptEvent) {} + public function onNoteHoldDrop(event:HoldNoteScriptEvent) {} + /** * Called when the player presses a key without any notes present. */ diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 0aa82b8df..d1e3b5919 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -2384,18 +2384,45 @@ class PlayState extends MusicBeatSubState // The player dropped a hold note. holdNote.handledMiss = true; - // Mute vocals and play miss animation, but don't penalize. + // Mute vocals and play miss animation. // vocals.playerVolume = 0; // if (currentStage != null && currentStage.getBoyfriend() != null) currentStage.getBoyfriend().playSingAnimation(holdNote.noteData.getDirection(), true); + + if (!isBotPlayMode) + { + if (holdNote.sustainLength > Constants.HOLD_DROP_PENALTY_THRESHOLD_MS) + { + // Penalize the player for letting go of a hold note too early. + trace('Player dropped a hold note, penalizing... (has hit: ${holdNote.hitNote})'); + + // Different penalty based on whether the note itself was missed, + // or the note was hit and then the hold was dropped. + var remainingLengthSec = holdNote.sustainLength / Constants.MS_PER_SEC; + var healthChangeUncapped = remainingLengthSec * Constants.HEALTH_HOLD_DROP_PENALTY_PER_SECOND; + // If the base note of the hold was missed, don't penalize them more on top of that. + var healthChangeMax = Constants.HEALTH_HOLD_DROP_PENALTY_MAX - (holdNote.hitNote ? -Constants.HEALTH_MISS_PENALTY : 0); + var healthChange = Math.max(healthChangeUncapped, healthChangeMax); + var scoreChange = Std.int(Constants.SCORE_HOLD_DROP_PENALTY_PER_SECOND * remainingLengthSec); + + var event:HoldNoteScriptEvent = new HoldNoteScriptEvent(NOTE_HOLD_DROP, holdNote, healthChange, scoreChange, true); + dispatchEvent(event); + + trace('Penalizing score by ${event.score} and health by ${event.healthChange} for dropping hold note (is combo break: ${event.isComboBreak})!'); + applyScore(event.score, 'miss', event.healthChange, event.isComboBreak); + + // Play the miss sound. + vocals.playerVolume = 0; + FunkinSound.playOnce(Paths.soundRandom('missnote', 1, 3), FlxG.random.float(0.5, 0.6)); + } + else + { + trace('Hold note too short, not penalizing...'); + } + } } } } - /** - * Spitting out the input for ravy 🙇‍♂️!! - */ - var inputSpitter:Array = []; - function handleSkippedNotes():Void { for (note in playerStrumline.notes.members) diff --git a/source/funkin/play/character/BaseCharacter.hx b/source/funkin/play/character/BaseCharacter.hx index a85b76688..edcf231e8 100644 --- a/source/funkin/play/character/BaseCharacter.hx +++ b/source/funkin/play/character/BaseCharacter.hx @@ -553,17 +553,40 @@ class BaseCharacter extends Bopper if (event.note.noteData.getMustHitNote() && characterType == BF) { - // If the note is from the same strumline, play the sing animation. + // If the note is from the same strumline, play the miss animation. this.playSingAnimation(event.note.noteData.getDirection(), true); } else if (!event.note.noteData.getMustHitNote() && characterType == DAD) { - // If the note is from the same strumline, play the sing animation. + // If the note is from the same strumline, play the miss animation. this.playSingAnimation(event.note.noteData.getDirection(), true); } else if (event.note.noteData.getMustHitNote() && characterType == GF) { - playComboDropAnimation(Highscore.tallies.combo); + playComboDropAnimation(event.comboCount); + } + } + + public override function onNoteHoldDrop(event:HoldNoteScriptEvent) + { + super.onNoteHoldDrop(event); + + // If another script cancelled the event, don't do anything. + if (event.eventCanceled) return; + + if (event.holdNote.noteData.getMustHitNote() && characterType == BF) + { + // If the note is from the same strumline, play the miss animation. + this.playSingAnimation(event.holdNote.noteData.getDirection(), true); + } + else if (!event.holdNote.noteData.getMustHitNote() && characterType == DAD) + { + // If the note is from the same strumline, play the miss animation. + this.playSingAnimation(event.holdNote.noteData.getDirection(), true); + } + else if (event.holdNote.noteData.getMustHitNote() && event.isComboBreak && characterType == GF) + { + playComboDropAnimation(event.comboCount); } } diff --git a/source/funkin/play/notes/notekind/NoteKind.hx b/source/funkin/play/notes/notekind/NoteKind.hx index 6f0ecb7ad..9cc125a31 100644 --- a/source/funkin/play/notes/notekind/NoteKind.hx +++ b/source/funkin/play/notes/notekind/NoteKind.hx @@ -66,6 +66,8 @@ class NoteKind implements INoteScriptedClass public function onNoteHit(event:HitNoteScriptEvent):Void {} public function onNoteMiss(event:NoteScriptEvent):Void {} + + public function onNoteHoldDrop(event:HoldNoteScriptEvent) {} } /** diff --git a/source/funkin/play/notes/notekind/NoteKindManager.hx b/source/funkin/play/notes/notekind/NoteKindManager.hx index d698ba5d0..f1f0c11fe 100644 --- a/source/funkin/play/notes/notekind/NoteKindManager.hx +++ b/source/funkin/play/notes/notekind/NoteKindManager.hx @@ -49,7 +49,7 @@ class NoteKindManager { var noteEvent:NoteScriptEvent = cast(event, NoteScriptEvent); - var noteKind:NoteKind = noteKinds.get(noteEvent.note.kind); + var noteKind:NoteKind = noteKinds.get(noteEvent?.note?.kind); if (noteKind != null) { diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index 39445e27e..8cf613160 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -640,47 +640,49 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry { diff --git a/source/funkin/play/stage/Bopper.hx b/source/funkin/play/stage/Bopper.hx index 76c8fc810..b539da77f 100644 --- a/source/funkin/play/stage/Bopper.hx +++ b/source/funkin/play/stage/Bopper.hx @@ -371,6 +371,8 @@ class Bopper extends StageProp implements IPlayStateScriptedClass public function onNoteHit(event:HitNoteScriptEvent) {} + public function onNoteHoldDrop(event:HoldNoteScriptEvent) {} + public function onNoteMiss(event:NoteScriptEvent) {} public function onSongEvent(event:SongEventScriptEvent) {} diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx index 4513bde1c..618fb5667 100644 --- a/source/funkin/play/stage/Stage.hx +++ b/source/funkin/play/stage/Stage.hx @@ -901,6 +901,8 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements public function onNoteMiss(event:NoteScriptEvent) {} + public function onNoteHoldDrop(event:HoldNoteScriptEvent) {} + public function onSongEvent(event:SongEventScriptEvent) {} public function onNoteGhostMiss(event:GhostMissNoteScriptEvent) {} diff --git a/source/funkin/ui/haxeui/components/CharacterPlayer.hx b/source/funkin/ui/haxeui/components/CharacterPlayer.hx index 95b81bfcf..e2c7a0957 100644 --- a/source/funkin/ui/haxeui/components/CharacterPlayer.hx +++ b/source/funkin/ui/haxeui/components/CharacterPlayer.hx @@ -2,6 +2,7 @@ package funkin.ui.haxeui.components; import funkin.modding.events.ScriptEvent.GhostMissNoteScriptEvent; import funkin.modding.events.ScriptEvent.NoteScriptEvent; +import funkin.modding.events.ScriptEvent.HoldNoteScriptEvent; import funkin.modding.events.ScriptEvent.HitNoteScriptEvent; import funkin.modding.events.ScriptEvent.SongTimeScriptEvent; import funkin.modding.events.ScriptEvent.UpdateScriptEvent; @@ -237,6 +238,16 @@ class CharacterPlayer extends Box if (character != null) character.onNoteMiss(event); } + /** + * Called when a hold note is dropped in the song + * Used to play character animations. + * @param event The event. + */ + public function onNoteHoldDrop(event:HoldNoteScriptEvent):Void + { + if (character != null) character.onNoteHoldDrop(event); + } + /** * Called when a key is pressed but no note is hit in the song * Used to play character animations. diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index dad6a706c..387ee7656 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -476,9 +476,14 @@ class Constants public static final HEALTH_GHOST_MISS_PENALTY:Float = -4.0 / 100.0 * HEALTH_MAX; // 2.0% /** - * The amount of health the player loses upon letting go of a hold note while it is still going. + * The amount of health the player loses upon letting go of a hold note, per second remaining. */ - public static final HEALTH_HOLD_DROP_PENALTY:Float = 0.0; // 0.0% + public static final HEALTH_HOLD_DROP_PENALTY_PER_SECOND:Float = -4.5 / 100.0 * HEALTH_MAX; // -4.5% / second + + /** + * The maximum amount of health the player can lose upon letting go of a hold note. + */ + public static final HEALTH_HOLD_DROP_PENALTY_MAX:Float = -10.0 / 100.0 * HEALTH_MAX; // -10.0% /** * The amount of health the player loses upon hitting a mine. @@ -491,11 +496,21 @@ class Constants // ============================== /** - * The amount of score the player gains for every send they hold a hold note. + * The amount of score the player gains for every second they hold a hold note. * A fraction of this value is granted every frame. */ public static final SCORE_HOLD_BONUS_PER_SECOND:Float = 250.0; + /** + * The amount of score the player loses upon letting go of a hold note, per second remaining. + */ + public static final SCORE_HOLD_DROP_PENALTY_PER_SECOND:Float = -125.0; + + /** + * The minimum amount of the hold note, in milliseconds, before the player gets penalized for letting go of it early. + */ + public static final HOLD_DROP_PENALTY_THRESHOLD_MS:Float = 250.0; + public static final JUDGEMENT_KILLER_COMBO_BREAK:Bool = false; public static final JUDGEMENT_SICK_COMBO_BREAK:Bool = false; public static final JUDGEMENT_GOOD_COMBO_BREAK:Bool = false; From b00589367cd317556db8443783749469230a0f10 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 20 Mar 2025 03:25:24 -0400 Subject: [PATCH 17/40] Make hold drop penalty threshold match the hit window. --- source/funkin/util/Constants.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index 387ee7656..2611d7b8e 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -509,7 +509,7 @@ class Constants /** * The minimum amount of the hold note, in milliseconds, before the player gets penalized for letting go of it early. */ - public static final HOLD_DROP_PENALTY_THRESHOLD_MS:Float = 250.0; + public static final HOLD_DROP_PENALTY_THRESHOLD_MS:Float = 160.0; public static final JUDGEMENT_KILLER_COMBO_BREAK:Bool = false; public static final JUDGEMENT_SICK_COMBO_BREAK:Bool = false; From 4d9638f0acc217079eb12f4099965dbcf3e7d25b Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 20 Mar 2025 03:35:23 -0400 Subject: [PATCH 18/40] Fix the clamping. --- source/funkin/play/PlayState.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index d1e3b5919..e0d47ebc6 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -2401,7 +2401,7 @@ class PlayState extends MusicBeatSubState var healthChangeUncapped = remainingLengthSec * Constants.HEALTH_HOLD_DROP_PENALTY_PER_SECOND; // If the base note of the hold was missed, don't penalize them more on top of that. var healthChangeMax = Constants.HEALTH_HOLD_DROP_PENALTY_MAX - (holdNote.hitNote ? -Constants.HEALTH_MISS_PENALTY : 0); - var healthChange = Math.max(healthChangeUncapped, healthChangeMax); + var healthChange = healthChangeUncapped.clamp(healthChangeMax, 0); var scoreChange = Std.int(Constants.SCORE_HOLD_DROP_PENALTY_PER_SECOND * remainingLengthSec); var event:HoldNoteScriptEvent = new HoldNoteScriptEvent(NOTE_HOLD_DROP, holdNote, healthChange, scoreChange, true); From 01bf3bee11420769f58393db503a9fb6bbf33c88 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Wed, 26 Mar 2025 02:50:26 -0400 Subject: [PATCH 19/40] Dropping hold notes should no longer cost health by default. --- source/funkin/util/Constants.hx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index 2611d7b8e..eab31fef7 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -478,12 +478,12 @@ class Constants /** * The amount of health the player loses upon letting go of a hold note, per second remaining. */ - public static final HEALTH_HOLD_DROP_PENALTY_PER_SECOND:Float = -4.5 / 100.0 * HEALTH_MAX; // -4.5% / second + public static final HEALTH_HOLD_DROP_PENALTY_PER_SECOND:Float = 0 / 100.0 * HEALTH_MAX; // -4.5% / second /** * The maximum amount of health the player can lose upon letting go of a hold note. */ - public static final HEALTH_HOLD_DROP_PENALTY_MAX:Float = -10.0 / 100.0 * HEALTH_MAX; // -10.0% + public static final HEALTH_HOLD_DROP_PENALTY_MAX:Float = 0 / 100.0 * HEALTH_MAX; // -10.0% /** * The amount of health the player loses upon hitting a mine. From 20c3228d8cf9a6b9e44bb049898a522055585bd5 Mon Sep 17 00:00:00 2001 From: Eric Date: Sat, 5 Apr 2025 03:44:53 -0400 Subject: [PATCH 20/40] Don't discount the score so much on miss, but still keep it meaningful. --- source/funkin/play/scoring/Scoring.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/funkin/play/scoring/Scoring.hx b/source/funkin/play/scoring/Scoring.hx index f73d4b0df..cbb947ab6 100644 --- a/source/funkin/play/scoring/Scoring.hx +++ b/source/funkin/play/scoring/Scoring.hx @@ -105,7 +105,7 @@ class Scoring /** * The score a note receives when it is missed. */ - public static final PBOT1_MISS_SCORE:Int = -250; + public static final PBOT1_MISS_SCORE:Int = -100; /** * The threshold at which a note hit is considered perfect and always given the max score. From 1ee8246b483ef60ebed76af8c3b93fcbdc6ef892 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 20 Feb 2025 17:49:35 -0500 Subject: [PATCH 21/40] Added a preference to enable VSync. --- hmm.json | 2 +- source/funkin/Preferences.hx | 23 ++++++++++++ source/funkin/save/Save.hx | 8 +++++ source/funkin/ui/options/PreferencesMenu.hx | 36 ++++++++++++++----- .../ui/options/items/EnumPreferenceItem.hx | 32 +++++++++-------- source/funkin/util/WindowUtil.hx | 12 +++++++ 6 files changed, 90 insertions(+), 23 deletions(-) diff --git a/hmm.json b/hmm.json index 73136485a..a51d594c3 100644 --- a/hmm.json +++ b/hmm.json @@ -143,7 +143,7 @@ "name": "lime", "type": "git", "dir": null, - "ref": "d1322e60f97b5c6e977f9e3e8a04f22b5190e7d9", + "ref": "2fe5846312a2133a5ef75fc5c88d93799514a5de", "url": "https://github.com/FunkinCrew/lime" }, { diff --git a/source/funkin/Preferences.hx b/source/funkin/Preferences.hx index aa040231e..6c079315f 100644 --- a/source/funkin/Preferences.hx +++ b/source/funkin/Preferences.hx @@ -1,6 +1,7 @@ package funkin; import funkin.save.Save; +import funkin.util.WindowUtil; /** * A core class which provides a store of user-configurable, globally relevant values. @@ -176,6 +177,27 @@ class Preferences return value; } + /** + * If enabled, the game will utilize VSync (or adaptive VSync) on startup. + * @default `OFF` + */ + public static var vsyncMode(get, set):lime.ui.WindowVSyncMode; + + static function get_vsyncMode():lime.ui.WindowVSyncMode + { + return Save?.instance?.options?.vsyncMode ?? lime.ui.WindowVSyncMode.OFF; + } + + static function set_vsyncMode(value:lime.ui.WindowVSyncMode):lime.ui.WindowVSyncMode + { + WindowUtil.setVSyncMode(value); + + var save:Save = Save.instance; + save.options.vsyncMode = value; + save.flush(); + return value; + } + public static var unlockedFramerate(get, set):Bool; static function get_unlockedFramerate():Bool @@ -340,6 +362,7 @@ class Preferences { // Apply the autoPause setting (enables automatic pausing on focus lost). FlxG.autoPause = Preferences.autoPause; + // WindowUtil.setVSyncMode(Preferences.vsyncMode); // Apply the debugDisplay setting (enables the FPS and RAM display). toggleDebugDisplay(Preferences.debugDisplay); #if web diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx index ec527122e..16572bb85 100644 --- a/source/funkin/save/Save.hx +++ b/source/funkin/save/Save.hx @@ -115,6 +115,7 @@ class Save zoomCamera: true, debugDisplay: false, autoPause: true, + vsyncMode: lime.ui.WindowVSyncMode.OFF, strumlineBackgroundOpacity: 0, autoFullscreen: false, inputOffset: 0, @@ -1128,6 +1129,7 @@ class Save * @param slot The slot number to check. * @return Whether the slot is not empty. */ + @:haxe.warning("-WDeprecated") static function querySlot(slot:Int):Bool { var targetSaveData = new FlxSave(); @@ -1397,6 +1399,12 @@ typedef SaveDataOptions = */ var autoPause:Bool; + /** + * If enabled, the game will utilize VSync (or adaptive VSync) on startup. + * @default `OFF` + */ + var vsyncMode:lime.ui.WindowVSyncMode; + /** * If >0, the game will display a semi-opaque background under the notes. * `0` for no background, `100` for solid black if you're freaky like that diff --git a/source/funkin/ui/options/PreferencesMenu.hx b/source/funkin/ui/options/PreferencesMenu.hx index ff5ae1f13..c16f8a783 100644 --- a/source/funkin/ui/options/PreferencesMenu.hx +++ b/source/funkin/ui/options/PreferencesMenu.hx @@ -14,6 +14,7 @@ import funkin.ui.TextMenuList.TextMenuItem; import funkin.ui.options.items.CheckboxPreferenceItem; import funkin.ui.options.items.NumberPreferenceItem; import funkin.ui.options.items.EnumPreferenceItem; +import lime.ui.WindowVSyncMode; class PreferencesMenu extends Page { @@ -118,6 +119,19 @@ class PreferencesMenu extends Page createPrefItemCheckbox('Launch in Fullscreen', 'Automatically launch the game in fullscreen on startup', function(value:Bool):Void { Preferences.autoFullscreen = value; }, Preferences.autoFullscreen); + createPrefItemEnum('VSync', 'If enabled, game will attempt to match framerate with your monitor.', [ + "Off" => WindowVSyncMode.OFF, + "On" => WindowVSyncMode.ON, + "Adaptive" => WindowVSyncMode.ADAPTIVE, + ], function(key:String, value:WindowVSyncMode):Void { + trace("Setting vsync mode to " + key); + Preferences.vsyncMode = value; + }, switch (Preferences.vsyncMode) + { + case WindowVSyncMode.OFF: "Off"; + case WindowVSyncMode.ON: "On"; + case WindowVSyncMode.ADAPTIVE: "Adaptive"; + }); #if web createPrefItemCheckbox('Unlocked Framerate', 'If enabled, the framerate will be unlocked.', function(value:Bool):Void { @@ -155,13 +169,19 @@ class PreferencesMenu extends Page var thyOffset:Int = 0; // Initializing thy text width (if thou text present) var thyTextWidth:Int = 0; - if (Std.isOfType(daItem, EnumPreferenceItem)) thyTextWidth = cast(daItem, EnumPreferenceItem).lefthandText.getWidth(); - else if (Std.isOfType(daItem, NumberPreferenceItem)) thyTextWidth = cast(daItem, NumberPreferenceItem).lefthandText.getWidth(); - - if (thyTextWidth != 0) + switch (Type.typeof(daItem)) { - // Magic number because of the weird offset thats being added by default - thyOffset += thyTextWidth - 75; + case TClass(CheckboxPreferenceItem): + thyTextWidth = 0; + thyOffset = 0; + case TClass(EnumPreferenceItem): + thyTextWidth = cast(daItem, EnumPreferenceItem).lefthandText.getWidth(); + thyOffset = 0 + thyTextWidth - 75; + case TClass(NumberPreferenceItem): + thyTextWidth = cast(daItem, NumberPreferenceItem).lefthandText.getWidth(); + thyOffset = 0 + thyTextWidth - 75; + default: + // Huh? } if (items.selectedItem == daItem) @@ -245,9 +265,9 @@ class PreferencesMenu extends Page * @param onChange Gets called every time the player changes the value; use this to apply the value * @param defaultValue The value that is loaded in when the pref item is created (usually your Preferences.settingVariable) */ - function createPrefItemEnum(prefName:String, prefDesc:String, values:Map, onChange:String->Void, defaultValue:String):Void + function createPrefItemEnum(prefName:String, prefDesc:String, values:Map, onChange:String->T->Void, defaultKey:String):Void { - var item = new EnumPreferenceItem(0, (120 * items.length) + 30, prefName, values, defaultValue, onChange); + var item = new EnumPreferenceItem(0, (120 * items.length) + 30, prefName, values, defaultKey, onChange); items.addItem(prefName, item); preferenceItems.add(item.lefthandText); preferenceDesc.push(prefDesc); diff --git a/source/funkin/ui/options/items/EnumPreferenceItem.hx b/source/funkin/ui/options/items/EnumPreferenceItem.hx index 6e7a7ab39..4ec35c5e9 100644 --- a/source/funkin/ui/options/items/EnumPreferenceItem.hx +++ b/source/funkin/ui/options/items/EnumPreferenceItem.hx @@ -7,7 +7,7 @@ import funkin.input.Controls; /** * Preference item that allows the player to pick a value from an enum (list of values) */ -class EnumPreferenceItem extends TextMenuItem +class EnumPreferenceItem extends TextMenuItem { function controls():Controls { @@ -16,34 +16,37 @@ class EnumPreferenceItem extends TextMenuItem public var lefthandText:AtlasText; - public var currentValue:String; - public var onChangeCallback:NullVoid>; - public var map:Map; + public var currentKey:String; + public var onChangeCallback:NullT->Void>; + public var map:Map; public var keys:Array = []; var index = 0; - public function new(x:Float, y:Float, name:String, map:Map, defaultValue:String, ?callback:String->Void) + public function new(x:Float, y:Float, name:String, map:Map, defaultKey:String, ?callback:String->T->Void) { super(x, y, name, function() { - callback(this.currentValue); + var value = map.get(this.currentKey); + callback(this.currentKey, value); }); updateHitbox(); this.map = map; - this.currentValue = defaultValue; + this.currentKey = defaultKey; this.onChangeCallback = callback; var i:Int = 0; for (key in map.keys()) { + var value:T = map[key]; + this.keys.push(key); - if (this.currentValue == key) index = i; + if (this.currentKey == key) index = i; i += 1; } - lefthandText = new AtlasText(15, y, formatted(defaultValue), AtlasFont.DEFAULT); + lefthandText = new AtlasText(15, y, formatted(defaultKey), AtlasFont.DEFAULT); this.fireInstantly = true; } @@ -64,21 +67,22 @@ class EnumPreferenceItem extends TextMenuItem if (index > keys.length - 1) index = 0; if (index < 0) index = keys.length - 1; - currentValue = keys[index]; + currentKey = keys[index]; if (onChangeCallback != null && (shouldIncrease || shouldDecrease)) { - onChangeCallback(currentValue); + var value = map.get(currentKey); + onChangeCallback(currentKey, value); } } - lefthandText.text = formatted(currentValue); + lefthandText.text = formatted(currentKey); } - function formatted(value:String):String + function formatted(key:String):String { // FIXME: Can't add arrows around the text because the font doesn't support < > // var leftArrow:String = selected ? '<' : ''; // var rightArrow:String = selected ? '>' : ''; - return '${map.get(value) ?? value}'; + return '${key}'; } } diff --git a/source/funkin/util/WindowUtil.hx b/source/funkin/util/WindowUtil.hx index e42cb59b0..1b7a4a806 100644 --- a/source/funkin/util/WindowUtil.hx +++ b/source/funkin/util/WindowUtil.hx @@ -157,4 +157,16 @@ class WindowUtil { lime.app.Application.current.window.title = value; } + + public static function setVSyncMode(value:lime.ui.WindowVSyncMode):Void + { + // Apply the new value. + var success = FlxG.stage.application.window.setVSyncMode(value); + + if (!success) + { + // Maybe throw an error? + FlxG.stage.application.window.setVSyncMode(lime.ui.WindowVSyncMode.OFF); + } + } } From 6e2a077d6b377c4d562b83ecc12f2fd6b2c869f4 Mon Sep 17 00:00:00 2001 From: kade-github <26305836+Kade-github@users.noreply.github.com> Date: Mon, 14 Apr 2025 09:43:02 -0700 Subject: [PATCH 22/40] Fixes from merge --- source/funkin/ui/options/PreferencesMenu.hx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/source/funkin/ui/options/PreferencesMenu.hx b/source/funkin/ui/options/PreferencesMenu.hx index c16f8a783..bf78ca7f1 100644 --- a/source/funkin/ui/options/PreferencesMenu.hx +++ b/source/funkin/ui/options/PreferencesMenu.hx @@ -152,7 +152,8 @@ class PreferencesMenu extends Page createPrefItemCheckbox('Preview on save', 'If enabled, the preview will be shown only after a screenshot is saved.', function(value:Bool):Void { Preferences.previewOnSave = value; }, Preferences.previewOnSave); - createPrefItemEnum('Save Format', 'Save screenshots to this format.', ['PNG' => 'PNG', 'JPEG' => 'JPEG'], function(value:String):Void { + // TODO: having oValue is weird, probably change this later? was done to accomodate VSync changes. + createPrefItemEnum('Save Format', 'Save screenshots to this format.', ['PNG' => 'PNG', 'JPEG' => 'JPEG'], function(value:String, oValue:String):Void { Preferences.saveFormat = value; }, Preferences.saveFormat); createPrefItemNumber('JPEG Quality', 'The quality of JPEG screenshots.', function(value:Float) { From db00feff938277dff28516e896fe48a167f8dbfa Mon Sep 17 00:00:00 2001 From: kade-github <26305836+Kade-github@users.noreply.github.com> Date: Mon, 14 Apr 2025 09:43:08 -0700 Subject: [PATCH 23/40] Vsync does work now --- source/funkin/util/WindowUtil.hx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/source/funkin/util/WindowUtil.hx b/source/funkin/util/WindowUtil.hx index 1b7a4a806..ef35a69e7 100644 --- a/source/funkin/util/WindowUtil.hx +++ b/source/funkin/util/WindowUtil.hx @@ -160,13 +160,8 @@ class WindowUtil public static function setVSyncMode(value:lime.ui.WindowVSyncMode):Void { - // Apply the new value. - var success = FlxG.stage.application.window.setVSyncMode(value); + FlxG.stage.application.window.setVSyncMode(value); - if (!success) - { - // Maybe throw an error? - FlxG.stage.application.window.setVSyncMode(lime.ui.WindowVSyncMode.OFF); - } + // TODO: Add an error check here, simply doing !result (as 0 is success), doesn't work for the check. } } From 3944816021fd82f158d1c0087dad67b55c4b5685 Mon Sep 17 00:00:00 2001 From: kade-github <26305836+Kade-github@users.noreply.github.com> Date: Mon, 14 Apr 2025 10:07:22 -0700 Subject: [PATCH 24/40] Error handling from wiki --- source/funkin/util/WindowUtil.hx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/source/funkin/util/WindowUtil.hx b/source/funkin/util/WindowUtil.hx index ef35a69e7..78d5274ae 100644 --- a/source/funkin/util/WindowUtil.hx +++ b/source/funkin/util/WindowUtil.hx @@ -160,8 +160,14 @@ class WindowUtil public static function setVSyncMode(value:lime.ui.WindowVSyncMode):Void { - FlxG.stage.application.window.setVSyncMode(value); + var res:Bool = FlxG.stage.application.window.setVSyncMode(value); - // TODO: Add an error check here, simply doing !result (as 0 is success), doesn't work for the check. + // SDL_GL_SetSwapInterval returns the value we assigned on success, https://wiki.libsdl.org/SDL2/SDL_GL_GetSwapInterval#return-value. + // In lime, we can compare this to the original value to get a boolean. + if (!res) + { + trace('Failed to set VSync mode to ' + value); + FlxG.stage.application.window.setVSyncMode(lime.ui.WindowVSyncMode.OFF); + } } } From c7c2620217a8319b0938628e8a190df7078e935d Mon Sep 17 00:00:00 2001 From: kade-github <26305836+Kade-github@users.noreply.github.com> Date: Mon, 14 Apr 2025 10:13:41 -0700 Subject: [PATCH 25/40] Set vsync on launch (might need to be set elsewhere too) --- source/Main.hx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/source/Main.hx b/source/Main.hx index c0a85d315..8ffbe724f 100644 --- a/source/Main.hx +++ b/source/Main.hx @@ -13,6 +13,7 @@ import openfl.events.Event; import openfl.Lib; import openfl.media.Video; import openfl.net.NetStream; +import funkin.util.WindowUtil; /** * The main class which initializes HaxeFlixel and starts the game in its initial state. @@ -67,6 +68,8 @@ class Main extends Sprite funkin.Preferences.lockedFramerateFunction = untyped js.Syntax.code("window.requestAnimationFrame"); #end + WindowUtil.setVSyncMode(funkin.Preferences.vsyncMode); + if (hasEventListener(Event.ADDED_TO_STAGE)) { removeEventListener(Event.ADDED_TO_STAGE, init); From 7e904234950c05187833bda0da11398c376b40f1 Mon Sep 17 00:00:00 2001 From: kade-github <26305836+Kade-github@users.noreply.github.com> Date: Mon, 14 Apr 2025 20:14:38 -0700 Subject: [PATCH 26/40] Pre-Processor to disable VSync on MacOS --- source/funkin/ui/options/PreferencesMenu.hx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/source/funkin/ui/options/PreferencesMenu.hx b/source/funkin/ui/options/PreferencesMenu.hx index bf78ca7f1..240eade8b 100644 --- a/source/funkin/ui/options/PreferencesMenu.hx +++ b/source/funkin/ui/options/PreferencesMenu.hx @@ -119,6 +119,9 @@ class PreferencesMenu extends Page createPrefItemCheckbox('Launch in Fullscreen', 'Automatically launch the game in fullscreen on startup', function(value:Bool):Void { Preferences.autoFullscreen = value; }, Preferences.autoFullscreen); + + // disabled on macos due to "error: Late swap tearing currently unsupported" + #if !mac createPrefItemEnum('VSync', 'If enabled, game will attempt to match framerate with your monitor.', [ "Off" => WindowVSyncMode.OFF, "On" => WindowVSyncMode.ON, @@ -132,7 +135,7 @@ class PreferencesMenu extends Page case WindowVSyncMode.ON: "On"; case WindowVSyncMode.ADAPTIVE: "Adaptive"; }); - + #end #if web createPrefItemCheckbox('Unlocked Framerate', 'If enabled, the framerate will be unlocked.', function(value:Bool):Void { Preferences.unlockedFramerate = value; From 0d5b7d4ed40f89dc3078bad20bdf1c84a4c4ef29 Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Mon, 14 Apr 2025 23:19:07 -0400 Subject: [PATCH 27/40] also conditional for WindowUtils.setVSyncMode() for mac --- source/funkin/util/WindowUtil.hx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/source/funkin/util/WindowUtil.hx b/source/funkin/util/WindowUtil.hx index 78d5274ae..fc590444c 100644 --- a/source/funkin/util/WindowUtil.hx +++ b/source/funkin/util/WindowUtil.hx @@ -160,6 +160,8 @@ class WindowUtil public static function setVSyncMode(value:lime.ui.WindowVSyncMode):Void { + // vsync crap dont worky on mac rn derp + #if !mac var res:Bool = FlxG.stage.application.window.setVSyncMode(value); // SDL_GL_SetSwapInterval returns the value we assigned on success, https://wiki.libsdl.org/SDL2/SDL_GL_GetSwapInterval#return-value. @@ -169,5 +171,6 @@ class WindowUtil trace('Failed to set VSync mode to ' + value); FlxG.stage.application.window.setVSyncMode(lime.ui.WindowVSyncMode.OFF); } + #end } } From e672c0978f653e4576b2677dc21443e2e19fe48e Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Mon, 14 Apr 2025 23:35:42 -0400 Subject: [PATCH 28/40] hmm lime bump --- hmm.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hmm.json b/hmm.json index a51d594c3..71e7b490f 100644 --- a/hmm.json +++ b/hmm.json @@ -143,7 +143,7 @@ "name": "lime", "type": "git", "dir": null, - "ref": "2fe5846312a2133a5ef75fc5c88d93799514a5de", + "ref": "e41e3d428ccdd13a1197b20c353f7c40acb45e96", "url": "https://github.com/FunkinCrew/lime" }, { @@ -210,4 +210,4 @@ "url": "https://github.com/fponticelli/thx.semver" } ] -} +} \ No newline at end of file From 3ad14bac32096e45259997ab9957bec5fecf296f Mon Sep 17 00:00:00 2001 From: Hyper_ <40342021+NotHyper-474@users.noreply.github.com> Date: Mon, 14 Apr 2025 23:20:14 -0300 Subject: [PATCH 29/40] fix: Double check if events are the same when doing indexOf This is basically copy-pasted from SongNoteDataArrayTools lol --- source/funkin/util/tools/SongEventDataArrayTools.hx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/source/funkin/util/tools/SongEventDataArrayTools.hx b/source/funkin/util/tools/SongEventDataArrayTools.hx index dd2e3137e..38e298dde 100644 --- a/source/funkin/util/tools/SongEventDataArrayTools.hx +++ b/source/funkin/util/tools/SongEventDataArrayTools.hx @@ -46,10 +46,15 @@ class SongEventDataArrayTools // Search the lower half of the range. highIndex = midIndex - 1; } + // Found it? Make a more thorough check. + else if (midNote == note) + { + return midIndex; + } else { - // Found it! - return midIndex; + // We may be close, so constrain the range (but only a little) and try again. + highIndex -= 1; } } return -1; From 14975212a918adb1a5d96a349ec9e8e63c9fc64b Mon Sep 17 00:00:00 2001 From: lemz1 Date: Sun, 13 Apr 2025 00:16:04 +0200 Subject: [PATCH 30/40] macro registry macro Merge branch 'lemz1/registry-entry-data' into lemz1/registry-macro-stufff --- source/funkin/data/BaseRegistry.hx | 1 + source/funkin/data/DefaultRegistryImpl.hx | 9 + source/funkin/data/IRegistryEntry.hx | 1 + .../conversation/ConversationRegistry.hx | 70 +--- .../dialoguebox/DialogueBoxRegistry.hx | 70 +--- .../data/dialogue/speaker/SpeakerRegistry.hx | 70 +--- .../data/freeplay/album/AlbumRegistry.hx | 66 +--- .../data/freeplay/player/PlayerRegistry.hx | 72 +--- .../freeplay/style/FreeplayStyleRegistry.hx | 66 +--- .../data/notestyle/NoteStyleRegistry.hx | 70 +--- source/funkin/data/song/SongRegistry.hx | 57 +--- source/funkin/data/stage/StageRegistry.hx | 90 +---- .../funkin/data/story/level/LevelRegistry.hx | 83 +---- .../play/cutscene/dialogue/Conversation.hx | 21 -- .../play/cutscene/dialogue/DialogueBox.hx | 14 - .../funkin/play/cutscene/dialogue/Speaker.hx | 20 -- .../funkin/play/notes/notestyle/NoteStyle.hx | 5 - source/funkin/play/song/Song.hx | 62 ++-- source/funkin/play/stage/Stage.hx | 14 - source/funkin/ui/freeplay/Album.hx | 22 -- .../freeplay/charselect/PlayableCharacter.hx | 31 -- source/funkin/ui/story/Level.hx | 31 -- source/funkin/util/macro/DataMacro.hx | 59 ++++ source/funkin/util/macro/MacroUtil.hx | 75 +++- source/funkin/util/macro/RegistryMacro.hx | 320 ++++++++++++++++++ source/funkin/util/macro/SingletonMacro.hx | 46 +++ source/funkin/util/tools/ISingleton.hx | 7 + 27 files changed, 567 insertions(+), 885 deletions(-) create mode 100644 source/funkin/data/DefaultRegistryImpl.hx create mode 100644 source/funkin/util/macro/DataMacro.hx create mode 100644 source/funkin/util/macro/RegistryMacro.hx create mode 100644 source/funkin/util/macro/SingletonMacro.hx create mode 100644 source/funkin/util/tools/ISingleton.hx diff --git a/source/funkin/data/BaseRegistry.hx b/source/funkin/data/BaseRegistry.hx index 83413ad00..882ba9695 100644 --- a/source/funkin/data/BaseRegistry.hx +++ b/source/funkin/data/BaseRegistry.hx @@ -16,6 +16,7 @@ typedef EntryConstructorFunction = String->Void; * @param J The type of the JSON data used when constructing. */ @:generic +@:autoBuild(funkin.util.macro.DataRegistryMacro.buildRegistry()) abstract class BaseRegistry & Constructible), J> { /** diff --git a/source/funkin/data/DefaultRegistryImpl.hx b/source/funkin/data/DefaultRegistryImpl.hx new file mode 100644 index 000000000..f7c137fbb --- /dev/null +++ b/source/funkin/data/DefaultRegistryImpl.hx @@ -0,0 +1,9 @@ +package funkin.data; + +/** + * An interface which automatically implements essential fields for a class extending `BaseRegistry`. + * + * @see `funkin.data.BaseRegistry` + */ +@:autoBuild(funkin.util.macro.RegistryMacro.buildRegistry()) +interface DefaultRegistryImpl {} diff --git a/source/funkin/data/IRegistryEntry.hx b/source/funkin/data/IRegistryEntry.hx index ff506767d..70f464f86 100644 --- a/source/funkin/data/IRegistryEntry.hx +++ b/source/funkin/data/IRegistryEntry.hx @@ -5,6 +5,7 @@ package funkin.data; * A `String->Void` constructor is also mandatory, but enforced elsewhere. * @param T The JSON data type of the registry entry. */ +@:autoBuild(funkin.util.macro.RegistryMacro.buildEntry()) interface IRegistryEntry { public final id:String; diff --git a/source/funkin/data/dialogue/conversation/ConversationRegistry.hx b/source/funkin/data/dialogue/conversation/ConversationRegistry.hx index fad1e43ad..2f79cf973 100644 --- a/source/funkin/data/dialogue/conversation/ConversationRegistry.hx +++ b/source/funkin/data/dialogue/conversation/ConversationRegistry.hx @@ -2,8 +2,10 @@ package funkin.data.dialogue.conversation; import funkin.play.cutscene.dialogue.Conversation; import funkin.play.cutscene.dialogue.ScriptedConversation; +import funkin.util.tools.ISingleton; +import funkin.data.DefaultRegistryImpl; -class ConversationRegistry extends BaseRegistry +class ConversationRegistry extends BaseRegistry implements ISingleton implements DefaultRegistryImpl { /** * The current version string for the dialogue box data format. @@ -14,74 +16,8 @@ class ConversationRegistry extends BaseRegistry public static final CONVERSATION_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x"; - public static var instance(get, never):ConversationRegistry; - static var _instance:Null = null; - - static function get_instance():ConversationRegistry - { - if (_instance == null) _instance = new ConversationRegistry(); - return _instance; - } - public function new() { super('CONVERSATION', 'dialogue/conversations', CONVERSATION_DATA_VERSION_RULE); } - - /** - * Read, parse, and validate the JSON data and produce the corresponding data object. - */ - public function parseEntryData(id:String):Null - { - // JsonParser does not take type parameters, - // otherwise this function would be in BaseRegistry. - var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - - switch (loadEntryFile(id)) - { - case {fileName: fileName, contents: contents}: - parser.fromJson(contents, fileName); - default: - return null; - } - - if (parser.errors.length > 0) - { - printErrors(parser.errors, id); - return null; - } - return parser.value; - } - - /** - * Parse and validate the JSON data and produce the corresponding data object. - * - * NOTE: Must be implemented on the implementation class. - * @param contents The JSON as a string. - * @param fileName An optional file name for error reporting. - */ - public function parseEntryDataRaw(contents:String, ?fileName:String):Null - { - var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - parser.fromJson(contents, fileName); - - if (parser.errors.length > 0) - { - printErrors(parser.errors, fileName); - return null; - } - return parser.value; - } - - function createScriptedEntry(clsName:String):Conversation - { - return ScriptedConversation.init(clsName, "unknown"); - } - - function getScriptedClassNames():Array - { - return ScriptedConversation.listScriptClasses(); - } } diff --git a/source/funkin/data/dialogue/dialoguebox/DialogueBoxRegistry.hx b/source/funkin/data/dialogue/dialoguebox/DialogueBoxRegistry.hx index 5c15a72ef..681f8566f 100644 --- a/source/funkin/data/dialogue/dialoguebox/DialogueBoxRegistry.hx +++ b/source/funkin/data/dialogue/dialoguebox/DialogueBoxRegistry.hx @@ -3,8 +3,10 @@ package funkin.data.dialogue.dialoguebox; import funkin.play.cutscene.dialogue.DialogueBox; import funkin.data.dialogue.dialoguebox.DialogueBoxData; import funkin.play.cutscene.dialogue.ScriptedDialogueBox; +import funkin.util.tools.ISingleton; +import funkin.data.DefaultRegistryImpl; -class DialogueBoxRegistry extends BaseRegistry +class DialogueBoxRegistry extends BaseRegistry implements ISingleton implements DefaultRegistryImpl { /** * The current version string for the dialogue box data format. @@ -15,74 +17,8 @@ class DialogueBoxRegistry extends BaseRegistry public static final DIALOGUEBOX_DATA_VERSION_RULE:thx.semver.VersionRule = ">=1.0.0 <1.2.0"; - public static var instance(get, never):DialogueBoxRegistry; - static var _instance:Null = null; - - static function get_instance():DialogueBoxRegistry - { - if (_instance == null) _instance = new DialogueBoxRegistry(); - return _instance; - } - public function new() { super('DIALOGUEBOX', 'dialogue/boxes', DIALOGUEBOX_DATA_VERSION_RULE); } - - /** - * Read, parse, and validate the JSON data and produce the corresponding data object. - */ - public function parseEntryData(id:String):Null - { - // JsonParser does not take type parameters, - // otherwise this function would be in BaseRegistry. - var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - - switch (loadEntryFile(id)) - { - case {fileName: fileName, contents: contents}: - parser.fromJson(contents, fileName); - default: - return null; - } - - if (parser.errors.length > 0) - { - printErrors(parser.errors, id); - return null; - } - return parser.value; - } - - /** - * Parse and validate the JSON data and produce the corresponding data object. - * - * NOTE: Must be implemented on the implementation class. - * @param contents The JSON as a string. - * @param fileName An optional file name for error reporting. - */ - public function parseEntryDataRaw(contents:String, ?fileName:String):Null - { - var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - parser.fromJson(contents, fileName); - - if (parser.errors.length > 0) - { - printErrors(parser.errors, fileName); - return null; - } - return parser.value; - } - - function createScriptedEntry(clsName:String):DialogueBox - { - return ScriptedDialogueBox.init(clsName, "unknown"); - } - - function getScriptedClassNames():Array - { - return ScriptedDialogueBox.listScriptClasses(); - } } diff --git a/source/funkin/data/dialogue/speaker/SpeakerRegistry.hx b/source/funkin/data/dialogue/speaker/SpeakerRegistry.hx index 1692ee0a1..96e85aca2 100644 --- a/source/funkin/data/dialogue/speaker/SpeakerRegistry.hx +++ b/source/funkin/data/dialogue/speaker/SpeakerRegistry.hx @@ -2,8 +2,10 @@ package funkin.data.dialogue.speaker; import funkin.play.cutscene.dialogue.Speaker; import funkin.play.cutscene.dialogue.ScriptedSpeaker; +import funkin.util.tools.ISingleton; +import funkin.data.DefaultRegistryImpl; -class SpeakerRegistry extends BaseRegistry +class SpeakerRegistry extends BaseRegistry implements ISingleton implements DefaultRegistryImpl { /** * The current version string for the speaker data format. @@ -14,74 +16,8 @@ class SpeakerRegistry extends BaseRegistry public static final SPEAKER_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x"; - public static var instance(get, never):SpeakerRegistry; - static var _instance:Null = null; - - static function get_instance():SpeakerRegistry - { - if (_instance == null) _instance = new SpeakerRegistry(); - return _instance; - } - public function new() { super('SPEAKER', 'dialogue/speakers', SPEAKER_DATA_VERSION_RULE); } - - /** - * Read, parse, and validate the JSON data and produce the corresponding data object. - */ - public function parseEntryData(id:String):Null - { - // JsonParser does not take type parameters, - // otherwise this function would be in BaseRegistry. - var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - - switch (loadEntryFile(id)) - { - case {fileName: fileName, contents: contents}: - parser.fromJson(contents, fileName); - default: - return null; - } - - if (parser.errors.length > 0) - { - printErrors(parser.errors, id); - return null; - } - return parser.value; - } - - /** - * Parse and validate the JSON data and produce the corresponding data object. - * - * NOTE: Must be implemented on the implementation class. - * @param contents The JSON as a string. - * @param fileName An optional file name for error reporting. - */ - public function parseEntryDataRaw(contents:String, ?fileName:String):Null - { - var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - parser.fromJson(contents, fileName); - - if (parser.errors.length > 0) - { - printErrors(parser.errors, fileName); - return null; - } - return parser.value; - } - - function createScriptedEntry(clsName:String):Speaker - { - return ScriptedSpeaker.init(clsName, "unknown"); - } - - function getScriptedClassNames():Array - { - return ScriptedSpeaker.listScriptClasses(); - } } diff --git a/source/funkin/data/freeplay/album/AlbumRegistry.hx b/source/funkin/data/freeplay/album/AlbumRegistry.hx index c04464fd2..a7c3d3990 100644 --- a/source/funkin/data/freeplay/album/AlbumRegistry.hx +++ b/source/funkin/data/freeplay/album/AlbumRegistry.hx @@ -3,8 +3,10 @@ package funkin.data.freeplay.album; import funkin.ui.freeplay.Album; import funkin.data.freeplay.album.AlbumData; import funkin.ui.freeplay.ScriptedAlbum; +import funkin.util.tools.ISingleton; +import funkin.data.DefaultRegistryImpl; -class AlbumRegistry extends BaseRegistry +class AlbumRegistry extends BaseRegistry implements ISingleton implements DefaultRegistryImpl { /** * The current version string for the album data format. @@ -15,70 +17,8 @@ class AlbumRegistry extends BaseRegistry public static final ALBUM_DATA_VERSION_RULE:thx.semver.VersionRule = '1.0.x'; - public static final instance:AlbumRegistry = new AlbumRegistry(); - public function new() { super('ALBUM', 'ui/freeplay/albums', ALBUM_DATA_VERSION_RULE); } - - /** - * Read, parse, and validate the JSON data and produce the corresponding data object. - * @param id The ID of the entry to load. - * @return The parsed data object. - */ - public function parseEntryData(id:String):Null - { - // JsonParser does not take type parameters, - // otherwise this function would be in BaseRegistry. - var parser:json2object.JsonParser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - - switch (loadEntryFile(id)) - { - case {fileName: fileName, contents: contents}: - parser.fromJson(contents, fileName); - default: - return null; - } - - if (parser.errors.length > 0) - { - printErrors(parser.errors, id); - return null; - } - return parser.value; - } - - /** - * Parse and validate the JSON data and produce the corresponding data object. - * - * NOTE: Must be implemented on the implementation class. - * @param contents The JSON as a string. - * @param fileName An optional file name for error reporting. - * @return The parsed data object. - */ - public function parseEntryDataRaw(contents:String, ?fileName:String):Null - { - var parser:json2object.JsonParser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - parser.fromJson(contents, fileName); - - if (parser.errors.length > 0) - { - printErrors(parser.errors, fileName); - return null; - } - return parser.value; - } - - function createScriptedEntry(clsName:String):Album - { - return ScriptedAlbum.init(clsName, 'unknown'); - } - - function getScriptedClassNames():Array - { - return ScriptedAlbum.listScriptClasses(); - } } diff --git a/source/funkin/data/freeplay/player/PlayerRegistry.hx b/source/funkin/data/freeplay/player/PlayerRegistry.hx index 76b1c25c1..b6c3af152 100644 --- a/source/funkin/data/freeplay/player/PlayerRegistry.hx +++ b/source/funkin/data/freeplay/player/PlayerRegistry.hx @@ -4,8 +4,10 @@ import funkin.data.freeplay.player.PlayerData; import funkin.ui.freeplay.charselect.PlayableCharacter; import funkin.ui.freeplay.charselect.ScriptedPlayableCharacter; import funkin.save.Save; +import funkin.util.tools.ISingleton; +import funkin.data.DefaultRegistryImpl; -class PlayerRegistry extends BaseRegistry +class PlayerRegistry extends BaseRegistry implements ISingleton implements DefaultRegistryImpl { /** * The current version string for the stage data format. @@ -16,14 +18,7 @@ class PlayerRegistry extends BaseRegistry public static final PLAYER_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x"; - public static var instance(get, never):PlayerRegistry; - static var _instance:Null = null; - - static function get_instance():PlayerRegistry - { - if (_instance == null) _instance = new PlayerRegistry(); - return _instance; - } + static final baseGamePlayerIds:Array = funkin.util.macro.DataMacro.listBaseGamePlayerIds(); /** * A mapping between stage character IDs and Freeplay playable character IDs. @@ -131,69 +126,12 @@ class PlayerRegistry extends BaseRegistry return ownedCharacterIds.exists(characterId); } - /** - * Read, parse, and validate the JSON data and produce the corresponding data object. - */ - public function parseEntryData(id:String):Null - { - // JsonParser does not take type parameters, - // otherwise this function would be in BaseRegistry. - var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - - switch (loadEntryFile(id)) - { - case {fileName: fileName, contents: contents}: - parser.fromJson(contents, fileName); - default: - return null; - } - - if (parser.errors.length > 0) - { - printErrors(parser.errors, id); - return null; - } - return parser.value; - } - - /** - * Parse and validate the JSON data and produce the corresponding data object. - * - * NOTE: Must be implemented on the implementation class. - * @param contents The JSON as a string. - * @param fileName An optional file name for error reporting. - */ - public function parseEntryDataRaw(contents:String, ?fileName:String):Null - { - var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - parser.fromJson(contents, fileName); - - if (parser.errors.length > 0) - { - printErrors(parser.errors, fileName); - return null; - } - return parser.value; - } - - function createScriptedEntry(clsName:String):PlayableCharacter - { - return ScriptedPlayableCharacter.init(clsName, "unknown"); - } - - function getScriptedClassNames():Array - { - return ScriptedPlayableCharacter.listScriptClasses(); - } - /** * A list of all the playable characters from the base game, in order. */ public function listBaseGamePlayerIds():Array { - return ["bf", "pico"]; + return baseGamePlayerIds; } /** diff --git a/source/funkin/data/freeplay/style/FreeplayStyleRegistry.hx b/source/funkin/data/freeplay/style/FreeplayStyleRegistry.hx index 626e2ac39..f6d75a86a 100644 --- a/source/funkin/data/freeplay/style/FreeplayStyleRegistry.hx +++ b/source/funkin/data/freeplay/style/FreeplayStyleRegistry.hx @@ -3,8 +3,10 @@ package funkin.data.freeplay.style; import funkin.ui.freeplay.FreeplayStyle; import funkin.data.freeplay.style.FreeplayStyleData; import funkin.ui.freeplay.ScriptedFreeplayStyle; +import funkin.util.tools.ISingleton; +import funkin.data.DefaultRegistryImpl; -class FreeplayStyleRegistry extends BaseRegistry +class FreeplayStyleRegistry extends BaseRegistry implements ISingleton implements DefaultRegistryImpl { /** * The current version string for the style data format. @@ -15,70 +17,8 @@ class FreeplayStyleRegistry extends BaseRegistry - { - // JsonParser does not take type parameters, - // otherwise this function would be in BaseRegistry. - var parser:json2object.JsonParser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - - switch (loadEntryFile(id)) - { - case {fileName: fileName, contents: contents}: - parser.fromJson(contents, fileName); - default: - return null; - } - - if (parser.errors.length > 0) - { - printErrors(parser.errors, id); - return null; - } - return parser.value; - } - - /** - * Parse and validate the JSON data and produce the corresponding data object. - * - * NOTE: Must be implemented on the implementation class. - * @param contents The JSON as a string. - * @param fileName An optional file name for error reporting. - * @return The parsed data object. - */ - public function parseEntryDataRaw(contents:String, ?fileName:String):Null - { - var parser:json2object.JsonParser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - parser.fromJson(contents, fileName); - - if (parser.errors.length > 0) - { - printErrors(parser.errors, fileName); - return null; - } - return parser.value; - } - - function createScriptedEntry(clsName:String):FreeplayStyle - { - return ScriptedFreeplayStyle.init(clsName, 'unknown'); - } - - function getScriptedClassNames():Array - { - return ScriptedFreeplayStyle.listScriptClasses(); - } } diff --git a/source/funkin/data/notestyle/NoteStyleRegistry.hx b/source/funkin/data/notestyle/NoteStyleRegistry.hx index 04bdd9190..c88aba6d5 100644 --- a/source/funkin/data/notestyle/NoteStyleRegistry.hx +++ b/source/funkin/data/notestyle/NoteStyleRegistry.hx @@ -3,8 +3,10 @@ package funkin.data.notestyle; import funkin.play.notes.notestyle.NoteStyle; import funkin.play.notes.notestyle.ScriptedNoteStyle; import funkin.data.notestyle.NoteStyleData; +import funkin.util.tools.ISingleton; +import funkin.data.DefaultRegistryImpl; -class NoteStyleRegistry extends BaseRegistry +class NoteStyleRegistry extends BaseRegistry implements ISingleton implements DefaultRegistryImpl { /** * The current version string for the note style data format. @@ -15,15 +17,6 @@ class NoteStyleRegistry extends BaseRegistry public static final NOTE_STYLE_DATA_VERSION_RULE:thx.semver.VersionRule = ">=1.0.0 <1.2.0"; - public static var instance(get, never):NoteStyleRegistry; - static var _instance:Null = null; - - static function get_instance():NoteStyleRegistry - { - if (_instance == null) _instance = new NoteStyleRegistry(); - return _instance; - } - public function new() { super('NOTESTYLE', 'notestyles', NOTE_STYLE_DATA_VERSION_RULE); @@ -33,61 +26,4 @@ class NoteStyleRegistry extends BaseRegistry { return fetchEntry(Constants.DEFAULT_NOTE_STYLE); } - - /** - * Read, parse, and validate the JSON data and produce the corresponding data object. - */ - public function parseEntryData(id:String):Null - { - // JsonParser does not take type parameters, - // otherwise this function would be in BaseRegistry. - var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - - switch (loadEntryFile(id)) - { - case {fileName: fileName, contents: contents}: - parser.fromJson(contents, fileName); - default: - return null; - } - - if (parser.errors.length > 0) - { - printErrors(parser.errors, id); - return null; - } - return parser.value; - } - - /** - * Parse and validate the JSON data and produce the corresponding data object. - * - * NOTE: Must be implemented on the implementation class. - * @param contents The JSON as a string. - * @param fileName An optional file name for error reporting. - */ - public function parseEntryDataRaw(contents:String, ?fileName:String):Null - { - var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - parser.fromJson(contents, fileName); - - if (parser.errors.length > 0) - { - printErrors(parser.errors, fileName); - return null; - } - return parser.value; - } - - function createScriptedEntry(clsName:String):NoteStyle - { - return ScriptedNoteStyle.init(clsName, "unknown"); - } - - function getScriptedClassNames():Array - { - return ScriptedNoteStyle.listScriptClasses(); - } } diff --git a/source/funkin/data/song/SongRegistry.hx b/source/funkin/data/song/SongRegistry.hx index 75fbb637f..b1a5c0333 100644 --- a/source/funkin/data/song/SongRegistry.hx +++ b/source/funkin/data/song/SongRegistry.hx @@ -10,11 +10,13 @@ import funkin.play.song.ScriptedSong; import funkin.play.song.Song; import funkin.util.assets.DataAssets; import funkin.util.VersionUtil; +import funkin.util.tools.ISingleton; +import funkin.data.DefaultRegistryImpl; using funkin.data.song.migrator.SongDataMigrator; @:nullSafety -class SongRegistry extends BaseRegistry +class SongRegistry extends BaseRegistry implements ISingleton implements DefaultRegistryImpl { /** * The current version string for the stage data format. @@ -40,18 +42,7 @@ class SongRegistry extends BaseRegistry return '${Constants.TITLE} - ${Constants.VERSION}'; } - /** - * TODO: What if there was a Singleton macro which automatically created the property for us? - */ - public static var instance(get, never):SongRegistry; - - static var _instance:Null = null; - - static function get_instance():SongRegistry - { - if (_instance == null) _instance = new SongRegistry(); - return _instance; - } + static final baseGameSongIds:Array = funkin.util.macro.DataMacro.listBaseGameSongIds(); public function new() { @@ -419,16 +410,6 @@ class SongRegistry extends BaseRegistry } } - function createScriptedEntry(clsName:String):Song - { - return ScriptedSong.init(clsName, "unknown"); - } - - function getScriptedClassNames():Array - { - return ScriptedSong.listScriptClasses(); - } - function loadEntryMetadataFile(id:String, ?variation:String):Null { variation = variation == null ? Constants.DEFAULT_VARIATION : variation; @@ -505,38 +486,10 @@ class SongRegistry extends BaseRegistry /** * A list of all the story weeks from the base game, in order. - * TODO: Should this be hardcoded? */ public function listBaseGameSongIds():Array { - return [ - "tutorial", - "bopeebo", - "fresh", - "dadbattle", - "spookeez", - "south", - "monster", - "pico", - "philly-nice", - "blammed", - "satin-panties", - "high", - "milf", - "cocoa", - "eggnog", - "winter-horrorland", - "senpai", - "roses", - "thorns", - "ugh", - "guns", - "stress", - "darnell", - "lit-up", - "2hot", - "blazin" - ]; + return baseGameSongIds; } /** diff --git a/source/funkin/data/stage/StageRegistry.hx b/source/funkin/data/stage/StageRegistry.hx index 0cdb21f41..7ff63efde 100644 --- a/source/funkin/data/stage/StageRegistry.hx +++ b/source/funkin/data/stage/StageRegistry.hx @@ -2,8 +2,10 @@ package funkin.data.stage; import funkin.play.stage.Stage; import funkin.play.stage.ScriptedStage; +import funkin.util.tools.ISingleton; +import funkin.data.DefaultRegistryImpl; -class StageRegistry extends BaseRegistry +class StageRegistry extends BaseRegistry implements ISingleton implements DefaultRegistryImpl { /** * The current version string for the stage data format. @@ -14,101 +16,19 @@ class StageRegistry extends BaseRegistry public static final STAGE_DATA_VERSION_RULE:thx.semver.VersionRule = ">=1.0.0 <1.1.0"; - public static var instance(get, never):StageRegistry; - static var _instance:Null = null; - - static function get_instance():StageRegistry - { - if (_instance == null) _instance = new StageRegistry(); - return _instance; - } + static final baseGameStageIds:Array = funkin.util.macro.DataMacro.listBaseGameStageIds(); public function new() { super('STAGE', 'stages', STAGE_DATA_VERSION_RULE); } - /** - * Read, parse, and validate the JSON data and produce the corresponding data object. - */ - public function parseEntryData(id:String):Null - { - // JsonParser does not take type parameters, - // otherwise this function would be in BaseRegistry. - var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - - switch (loadEntryFile(id)) - { - case {fileName: fileName, contents: contents}: - parser.fromJson(contents, fileName); - default: - return null; - } - - if (parser.errors.length > 0) - { - printErrors(parser.errors, id); - return null; - } - return parser.value; - } - - /** - * Parse and validate the JSON data and produce the corresponding data object. - * - * NOTE: Must be implemented on the implementation class. - * @param contents The JSON as a string. - * @param fileName An optional file name for error reporting. - */ - public function parseEntryDataRaw(contents:String, ?fileName:String):Null - { - var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - parser.fromJson(contents, fileName); - - if (parser.errors.length > 0) - { - printErrors(parser.errors, fileName); - return null; - } - return parser.value; - } - - function createScriptedEntry(clsName:String):Stage - { - return ScriptedStage.init(clsName, "unknown"); - } - - function getScriptedClassNames():Array - { - return ScriptedStage.listScriptClasses(); - } - /** * A list of all the stages from the base game, in order. - * TODO: Should this be hardcoded? */ public function listBaseGameStageIds():Array { - return [ - "mainStage", - "mainStageErect", - "spookyMansion", - "phillyTrain", - "phillyTrainErect", - "limoRide", - "limoRideErect", - "mallXmas", - "mallXmasErect", - "mallEvil", - "school", - "schoolEvil", - "tankmanBattlefield", - "phillyStreets", - "phillyStreetsErect", - "phillyBlazin", - ]; + return baseGameStageIds; } /** diff --git a/source/funkin/data/story/level/LevelRegistry.hx b/source/funkin/data/story/level/LevelRegistry.hx index dba8ce7f0..07e18f9a5 100644 --- a/source/funkin/data/story/level/LevelRegistry.hx +++ b/source/funkin/data/story/level/LevelRegistry.hx @@ -3,8 +3,10 @@ package funkin.data.story.level; import funkin.util.SortUtil; import funkin.ui.story.Level; import funkin.ui.story.ScriptedLevel; +import funkin.util.tools.ISingleton; +import funkin.data.DefaultRegistryImpl; -class LevelRegistry extends BaseRegistry +class LevelRegistry extends BaseRegistry implements ISingleton implements DefaultRegistryImpl { /** * The current version string for the level data format. @@ -15,94 +17,19 @@ class LevelRegistry extends BaseRegistry public static final LEVEL_DATA_VERSION_RULE:thx.semver.VersionRule = ">=1.0.0 <1.1.0"; - public static var instance(get, never):LevelRegistry; - static var _instance:Null = null; - - static function get_instance():LevelRegistry - { - if (_instance == null) _instance = new LevelRegistry(); - return _instance; - } + static final baseGameLevelIds:Array = funkin.util.macro.DataMacro.listBaseGameLevelIds(); public function new() { super('LEVEL', 'levels', LEVEL_DATA_VERSION_RULE); } - /** - * Read, parse, and validate the JSON data and produce the corresponding data object. - */ - public function parseEntryData(id:String):Null - { - // JsonParser does not take type parameters, - // otherwise this function would be in BaseRegistry. - var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - - switch (loadEntryFile(id)) - { - case {fileName: fileName, contents: contents}: - parser.fromJson(contents, fileName); - default: - return null; - } - - if (parser.errors.length > 0) - { - printErrors(parser.errors, id); - return null; - } - return parser.value; - } - - /** - * Parse and validate the JSON data and produce the corresponding data object. - * - * NOTE: Must be implemented on the implementation class. - * @param contents The JSON as a string. - * @param fileName An optional file name for error reporting. - */ - public function parseEntryDataRaw(contents:String, ?fileName:String):Null - { - var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - parser.fromJson(contents, fileName); - - if (parser.errors.length > 0) - { - printErrors(parser.errors, fileName); - return null; - } - return parser.value; - } - - function createScriptedEntry(clsName:String):Level - { - return ScriptedLevel.init(clsName, "unknown"); - } - - function getScriptedClassNames():Array - { - return ScriptedLevel.listScriptClasses(); - } - /** * A list of all the story weeks from the base game, in order. - * TODO: Should this be hardcoded? */ public function listBaseGameLevelIds():Array { - return [ - "tutorial", - "week1", - "week2", - "week3", - "week4", - "week5", - "week6", - "week7", - "weekend1" - ]; + return baseGameLevelIds; } public function listSortedLevelIds():Array diff --git a/source/funkin/play/cutscene/dialogue/Conversation.hx b/source/funkin/play/cutscene/dialogue/Conversation.hx index fd7504725..a94de8f94 100644 --- a/source/funkin/play/cutscene/dialogue/Conversation.hx +++ b/source/funkin/play/cutscene/dialogue/Conversation.hx @@ -28,22 +28,11 @@ import funkin.util.EaseUtil; @:nullSafety class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass implements IRegistryEntry { - /** - * The ID of the conversation. - */ - public final id:String; - /** * The current state of the conversation. */ var state:ConversationState = ConversationState.Start; - /** - * Conversation data as parsed from the JSON file. - * `null` if the data could not be parsed or loaded. - */ - public final _data:Null; - /** * The current entry in the dialogue. */ @@ -659,16 +648,6 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl outroTween = null; } } - - public override function toString():String - { - return 'Conversation($id)'; - } - - static function _fetchData(id:String):Null - { - return ConversationRegistry.instance.parseEntryDataWithMigration(id, ConversationRegistry.instance.fetchEntryVersion(id)); - } } // Managing things with a single enum is a lot easier than a multitude of flags. diff --git a/source/funkin/play/cutscene/dialogue/DialogueBox.hx b/source/funkin/play/cutscene/dialogue/DialogueBox.hx index 47acad4cc..f4372d71c 100644 --- a/source/funkin/play/cutscene/dialogue/DialogueBox.hx +++ b/source/funkin/play/cutscene/dialogue/DialogueBox.hx @@ -16,8 +16,6 @@ import funkin.data.dialogue.dialoguebox.DialogueBoxRegistry; class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass implements IRegistryEntry { - public final id:String; - public var dialogueBoxName(get, never):String; function get_dialogueBoxName():String @@ -25,8 +23,6 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass imple return _data.name ?? 'UNKNOWN'; } - public final _data:DialogueBoxData; - /** * Offset the speaker's sprite by this much when playing each animation. */ @@ -419,14 +415,4 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass imple } public function onScriptEvent(event:ScriptEvent):Void {} - - public override function toString():String - { - return 'DialogueBox($id)'; - } - - static function _fetchData(id:String):Null - { - return DialogueBoxRegistry.instance.parseEntryDataWithMigration(id, DialogueBoxRegistry.instance.fetchEntryVersion(id)); - } } diff --git a/source/funkin/play/cutscene/dialogue/Speaker.hx b/source/funkin/play/cutscene/dialogue/Speaker.hx index 7203ddb28..02efcb99d 100644 --- a/source/funkin/play/cutscene/dialogue/Speaker.hx +++ b/source/funkin/play/cutscene/dialogue/Speaker.hx @@ -16,16 +16,6 @@ import funkin.data.dialogue.speaker.SpeakerRegistry; */ class Speaker extends FlxSprite implements IDialogueScriptedClass implements IRegistryEntry { - /** - * The internal ID for this speaker. - */ - public final id:String; - - /** - * The full data for a speaker. - */ - public final _data:SpeakerData; - /** * A readable name for this speaker. */ @@ -298,14 +288,4 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass implements IRe } public function onScriptEvent(event:ScriptEvent):Void {} - - public override function toString():String - { - return 'Speaker($id)'; - } - - static function _fetchData(id:String):Null - { - return SpeakerRegistry.instance.parseEntryDataWithMigration(id, SpeakerRegistry.instance.fetchEntryVersion(id)); - } } diff --git a/source/funkin/play/notes/notestyle/NoteStyle.hx b/source/funkin/play/notes/notestyle/NoteStyle.hx index f497a58c2..bce2bcd45 100644 --- a/source/funkin/play/notes/notestyle/NoteStyle.hx +++ b/source/funkin/play/notes/notestyle/NoteStyle.hx @@ -19,11 +19,6 @@ using funkin.data.animation.AnimationData.AnimationDataUtil; @:nullSafety class NoteStyle implements IRegistryEntry { - /** - * The ID of the note style. - */ - public final id:String; - /** * Note style data as parsed from the JSON file. */ diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index 8cf613160..4d6b21ccb 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -65,19 +65,6 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry; - // key = variation id, value = metadata final _metadata:Map; @@ -633,56 +620,49 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry { diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx index 618fb5667..cf432a52a 100644 --- a/source/funkin/play/stage/Stage.hx +++ b/source/funkin/play/stage/Stage.hx @@ -30,10 +30,6 @@ typedef StagePropGroup = FlxTypedSpriteGroup; */ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements IRegistryEntry { - public final id:String; - - public final _data:StageData; - public var stageName(get, never):String; function get_stageName():String @@ -859,16 +855,6 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements } } - public override function toString():String - { - return 'Stage($id)'; - } - - static function _fetchData(id:String):Null - { - return StageRegistry.instance.parseEntryDataWithMigration(id, StageRegistry.instance.fetchEntryVersion(id)); - } - public function onScriptEvent(event:ScriptEvent) { // Ensure all custom events get broadcast to the elements of the stage. diff --git a/source/funkin/ui/freeplay/Album.hx b/source/funkin/ui/freeplay/Album.hx index 154013ea3..264094d6e 100644 --- a/source/funkin/ui/freeplay/Album.hx +++ b/source/funkin/ui/freeplay/Album.hx @@ -11,16 +11,6 @@ import flixel.graphics.FlxGraphic; */ class Album implements IRegistryEntry { - /** - * The internal ID for this album. - */ - public final id:String; - - /** - * The full data for an album. - */ - public final _data:AlbumData; - public function new(id:String) { this.id = id; @@ -93,16 +83,4 @@ class Album implements IRegistryEntry { return _data.albumTitleAnimations; } - - public function toString():String - { - return 'Album($id)'; - } - - public function destroy():Void {} - - static function _fetchData(id:String):Null - { - return AlbumRegistry.instance.parseEntryDataWithMigration(id, AlbumRegistry.instance.fetchEntryVersion(id)); - } } diff --git a/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx b/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx index 681c11910..fe5b8b9e1 100644 --- a/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx +++ b/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx @@ -12,16 +12,6 @@ import funkin.play.scoring.Scoring.ScoringRank; @:nullSafety class PlayableCharacter implements IRegistryEntry { - /** - * The ID of the playable character. - */ - public final id:String; - - /** - * Playable character data as parsed from the JSON file. - */ - public final _data:Null; - /** * @param id The ID of the JSON file to parse. */ @@ -161,25 +151,4 @@ class PlayableCharacter implements IRegistryEntry { return _data?.unlocked ?? true; } - - /** - * Called when the character is destroyed. - * TODO: Document when this gets called - */ - public function destroy():Void {} - - public function toString():String - { - return 'PlayableCharacter($id)'; - } - - /** - * Retrieve and parse the JSON data for a playable character by ID. - * @param id The ID of the character - * @return The parsed player data, or null if not found or invalid - */ - static function _fetchData(id:String):Null - { - return PlayerRegistry.instance.parseEntryDataWithMigration(id, PlayerRegistry.instance.fetchEntryVersion(id)); - } } diff --git a/source/funkin/ui/story/Level.hx b/source/funkin/ui/story/Level.hx index 683e94815..2d7a697f1 100644 --- a/source/funkin/ui/story/Level.hx +++ b/source/funkin/ui/story/Level.hx @@ -15,16 +15,6 @@ import funkin.data.story.level.LevelData; */ class Level implements IRegistryEntry { - /** - * The ID of the story mode level. - */ - public final id:String; - - /** - * Level data as parsed from the JSON file. - */ - public final _data:LevelData; - /** * @param id The ID of the JSON file to parse. */ @@ -249,25 +239,4 @@ class Level implements IRegistryEntry return props; } - - /** - * Called when the level is destroyed. - * TODO: Document when this gets called - */ - public function destroy():Void {} - - public function toString():String - { - return 'Level($id)'; - } - - /** - * Retrieve and parse the JSON data for a level by ID. - * @param id The ID of the level - * @return The parsed level data, or null if not found or invalid - */ - static function _fetchData(id:String):Null - { - return LevelRegistry.instance.parseEntryDataWithMigration(id, LevelRegistry.instance.fetchEntryVersion(id)); - } } diff --git a/source/funkin/util/macro/DataMacro.hx b/source/funkin/util/macro/DataMacro.hx new file mode 100644 index 000000000..c5898044b --- /dev/null +++ b/source/funkin/util/macro/DataMacro.hx @@ -0,0 +1,59 @@ +package funkin.util.macro; + +import haxe.macro.Expr; +#if macro +import sys.FileSystem; +#end + +using StringTools; + +class DataMacro +{ + public static macro function listBaseGameStageIds():Expr + { + var folder = "assets/preload/data/stages/"; + var files = FileSystem.readDirectory(folder); + var stages:Array = []; + + for (file in files) + stages.push(macro $v{file.replace(".json", "")}); + + return macro $a{stages}; + } + + public static macro function listBaseGameSongIds():Expr + { + var folder = "assets/songs/"; + var files = FileSystem.readDirectory(folder); + var songs:Array = []; + + for (file in files) + songs.push(macro $v{file}); + + return macro $a{songs}; + } + + public static macro function listBaseGameLevelIds():Expr + { + var folder = "assets/preload/data/levels/"; + var files = FileSystem.readDirectory(folder); + var levels:Array = []; + + for (file in files) + levels.push(macro $v{file.replace(".json", "")}); + + return macro $a{levels}; + } + + public static macro function listBaseGamePlayerIds():Expr + { + var folder = "assets/preload/data/players/"; + var files = FileSystem.readDirectory(folder); + var players:Array = []; + + for (file in files) + players.push(macro $v{file.replace(".json", "")}); + + return macro $a{players}; + } +} diff --git a/source/funkin/util/macro/MacroUtil.hx b/source/funkin/util/macro/MacroUtil.hx index 2e2c73279..bbe6e71af 100644 --- a/source/funkin/util/macro/MacroUtil.hx +++ b/source/funkin/util/macro/MacroUtil.hx @@ -4,6 +4,9 @@ import haxe.macro.Context; import haxe.macro.Expr; import haxe.macro.Type; +/** + * A collection of utility functions for Haxe macros. + */ class MacroUtil { /** @@ -13,9 +16,9 @@ class MacroUtil * @param defaultValue The value to return if the define is not set. * @return An expression containing the value of the define. */ - public static macro function getDefine(key:String, defaultValue:String = null):haxe.macro.Expr + public static macro function getDefine(key:String, ?defaultValue:String):haxe.macro.Expr { - var value = haxe.macro.Context.definedValue(key); + var value:Null = haxe.macro.Context.definedValue(key); if (value == null) value = defaultValue; return macro $v{value}; } @@ -26,13 +29,13 @@ class MacroUtil */ public static macro function getDate():ExprOf { - var date = Date.now(); - var year = toExpr(date.getFullYear()); - var month = toExpr(date.getMonth()); - var day = toExpr(date.getDate()); - var hours = toExpr(date.getHours()); - var mins = toExpr(date.getMinutes()); - var secs = toExpr(date.getSeconds()); + var date:Date = Date.now(); + var year:Expr = toExpr(date.getFullYear()); + var month:Expr = toExpr(date.getMonth()); + var day:Expr = toExpr(date.getDate()); + var hours:Expr = toExpr(date.getHours()); + var mins:Expr = toExpr(date.getMinutes()); + var secs:Expr = toExpr(date.getSeconds()); return macro new Date($year, $month, $day, $hours, $mins, $secs); } @@ -44,6 +47,8 @@ class MacroUtil /** * Convert an ExprOf> to a ClassType. * @see https://github.com/jasononeil/compiletime/blob/master/src/CompileTime.hx#L201 + * @param e The expression to convert. + * @return The `ClassType` */ public static function getClassTypeFromExpr(e:Expr):ClassType { @@ -99,6 +104,11 @@ class MacroUtil return null; } + /** + * Determine whether a field is static. + * @param field The field to check. + * @return Whether the field is static. + */ public static function isFieldStatic(field:haxe.macro.Expr.Field):Bool { return field.access.contains(AStatic); @@ -107,11 +117,17 @@ class MacroUtil /** * Converts a value to an equivalent macro expression. */ - public static function toExpr(value:Dynamic):ExprOf + public static function toExpr(value:Any):ExprOf { return Context.makeExpr(value, Context.currentPos()); } + /** + * Determine whether two classes are equal. + * @param class1 The first class to compare. + * @param class2 The second class to compare. + * @return Whether the two classes are equivalent. + */ public static function areClassesEqual(class1:ClassType, class2:ClassType):Bool { return class1.pack.join('.') == class2.pack.join('.') && class1.name == class2.name; @@ -119,6 +135,8 @@ class MacroUtil /** * Retrieve a ClassType from a string name. + * @param name The name of the class to retrieve. + * @return The `ClassType` */ public static function getClassType(name:String):ClassType { @@ -131,6 +149,43 @@ class MacroUtil } } + /** + * If we are in a build macro, return whether a field already exists on the current class. + * @param name The name of the field to check for. + * @return Whether the field already exists. + */ + public static function fieldAlreadyExists(name:String):Bool + { + for (field in Context.getBuildFields()) + { + if (field.name == name && !((field.access ?? []).contains(Access.AAbstract))) + { + return true; + } + } + + function fieldAlreadyExistsSuper(name:String, superClass:Null) + { + if (superClass == null) + { + return false; + } + + for (field in superClass.fields.get()) + { + if (field.name == name && !field.isAbstract) + { + return true; + } + } + + // recursively check superclasses + return fieldAlreadyExistsSuper(name, superClass.superClass?.t.get()); + } + + return fieldAlreadyExistsSuper(name, Context.getLocalClass().get().superClass?.t.get()); + } + /** * Determine whether a given ClassType is a subclass of a given superclass. * @param classType The class to check. diff --git a/source/funkin/util/macro/RegistryMacro.hx b/source/funkin/util/macro/RegistryMacro.hx new file mode 100644 index 000000000..e11687001 --- /dev/null +++ b/source/funkin/util/macro/RegistryMacro.hx @@ -0,0 +1,320 @@ +package funkin.util.macro; + +import haxe.macro.Context; +import haxe.macro.Expr; +import haxe.macro.Expr.ComplexType; +import haxe.macro.Expr.Field; +import haxe.macro.Expr.TypeDefKind; +import haxe.macro.Expr.MetadataEntry; +import haxe.macro.Type; +import haxe.macro.Type.ClassType; + +using Lambda; +using haxe.macro.ExprTools; +using haxe.macro.TypeTools; +using StringTools; + +/** + * The type parameters for a class extending `BaseRegistry`. + */ +typedef RegistryTypeParams = +{ + /** + * The class type of the entry. Must implement `IRegistryEntry`. + */ + var entryType:ClassType; + + /** + * The type for the data of an entry. This is usually a typedef of a struct. + */ + var dataType:Any; // DefType or ClassType + +} + +/** + * A set of build macros to be applied to `Registry` classes in the `funkin.data` package. + * + * @see `funkin.data.BaseRegistry` + */ +class RegistryMacro +{ + /** + * Builds the registry class. + * + * @return The modified list of fields for the target class. + */ + public static macro function buildRegistry():Array + { + var cls:ClassType = Context.getLocalClass().get(); + var fields:Array = Context.getBuildFields(); + + // Classes with the `@:funkinBase` meta or `@:funkinProcessed` meta should be ignored. + var baseMeta:Null = cls.meta.get().find(function(m) return m.name == ':funkinBase'); + if (baseMeta != null || alreadyProcessed(cls)) return fields; + + var typeParams:RegistryTypeParams = getTypeParams(cls); + + // Build an internal class with static functions that allow the Entry class to call functions on the Registry class. + buildEntryImpl(typeParams.entryType, cls); + + fields = fields.concat(buildRegistryMethods(cls, fields, typeParams.entryType, typeParams.dataType)); + + // Indicate that the class has been processed so we don't process twice. + cls.meta.add(":funkinProcessed", [], cls.pos); + + return fields; + } + + /** + * Builds the registry entry class. + * + * @return The modified list of fields for the target class. + */ + public static macro function buildEntry():Array + { + var cls:ClassType = Context.getLocalClass().get(); + var fields:Array = Context.getBuildFields(); + + // Classes with the `@:funkinProcessed` meta should be ignored. + if (alreadyProcessed(cls)) return fields; + + // Get the type of the JSON data for an entry. + var entryData:Any = getEntryData(cls); + + // Build variables and methods for the entry. + fields = fields.concat(buildEntryVariables(cls, entryData)); + fields = fields.concat(buildEntryMethods(cls)); + + // Indicate that the class has been processed so we don't process twice. + cls.meta.add(":funkinProcessed", [], cls.pos); + + return fields; + } + + #if macro + /** + * Retrieve the type parameters for a class extending `BaseRegistry`. + * @param cls The class to retrieve the type parameters for. + * @return The type parameters for the class. + */ + static function getTypeParams(cls:ClassType):RegistryTypeParams + { + switch (cls.superClass.t.get().kind) + { + case KGenericInstance(_, params): + var typeParams:Array = []; + for (param in params) + { + switch (param) + { + case TInst(t, _): + typeParams.push(t.get()); + case TType(t, _): + typeParams.push(t.get()); + default: + throw 'Not a class'; + } + } + return {entryType: typeParams[0], dataType: typeParams[1]}; + default: + throw '${cls.name}: Could not interpret type parameters of Registry class.'; + } + } + + /** + * Builds new static and instance methods for a registry class. + * + * @param cls The registry class to build the methods for. + * @param fields The fields of the registry class. + * @param entryType The class type of entries in the registry. + * @param dataType The type of the data for entries in the registry. + * @return The modified list of fields for the target class. + */ + static function buildRegistryMethods(cls:ClassType, fields:Array, entryType:ClassType, dataType:Dynamic):Array + { + var scriptedEntryClsName:String = entryType.pack.join('.') + '.Scripted' + entryType.name; + + var getScriptedClassName:String = '${scriptedEntryClsName}'; + + var createScriptedEntry:String = '${scriptedEntryClsName}.init(clsName, "unknown")'; + + var newJsonParser:String = 'new json2object.JsonParser<${dataType.module}.${dataType.name}>()'; + return (macro class TempClass + { + function getScriptedClassNames() + { + return ${Context.parse(getScriptedClassName, Context.currentPos())}.listScriptClasses(); + } + + function createScriptedEntry(clsName:String) + { + return ${Context.parse(createScriptedEntry, Context.currentPos())}; + } + + public function parseEntryData(id:String) + { + var parser = ${Context.parse(newJsonParser, Context.currentPos())}; + parser.ignoreUnknownVariables = false; + + @:privateAccess + switch (this.loadEntryFile(id)) + { + case {fileName: fileName, contents: contents}: + parser.fromJson(contents, fileName); + default: + return null; + } + + if (parser.errors.length > 0) + { + @:privateAccess + this.printErrors(parser.errors, id); + return null; + } + return parser.value; + } + + public function parseEntryDataRaw(contents:String, ?fileName:String) + { + var parser = ${Context.parse(newJsonParser, Context.currentPos())}; + parser.ignoreUnknownVariables = false; + parser.fromJson(contents, fileName); + + if (parser.errors.length > 0) + { + @:privateAccess + this.printErrors(parser.errors, fileName); + return null; + } + return parser.value; + } + }).fields.filter((field) -> return !MacroUtil.fieldAlreadyExists(field.name)); + } + + /** + * Retrieve the type of the JSON data for an entry. + * @param cls The entry class to retrieve the type of the JSON data for. + * @return Will be either a `DefType` or a `ClassType`. + */ + static function getEntryData(cls:ClassType):Any // DefType or ClassType + { + try + { + switch (cls.interfaces[0].params[0]) + { + case Type.TInst(t, _): + return t.get(); + case Type.TType(t, _): + return t.get(); + default: + throw '${cls.name}: Type parameter for Entry must be a Class or typedef'; + } + } + catch (e) + { + throw '${cls.name}: IRegistryEntry must be the last implemented interface'; + } + } + + /** + * Add fields to the entry class. + * @param cls The entry class to add fields to. + * @param entryData The type of the data for the entry. + * @return The modified list of fields for the target class. + */ + static function buildEntryVariables(cls:ClassType, entryData:Dynamic):Array + { + var entryDataType:ComplexType = Context.getType('${entryData.module}.${entryData.name}').toComplexType(); + + return (macro class TempClass + { + public final id:String; + + public final _data:Null<$entryDataType>; + }).fields.filter((field) -> return !MacroUtil.fieldAlreadyExists(field.name)); + } + + /** + * Add methods to the entry class. + * @param cls The entry class to add methods to. + * @return The modified list of fields for the target class. + */ + static function buildEntryMethods(cls:ClassType):Array + { + // The internal class built by `buildEntryImpl`. + var impl:String = 'funkin.macro.impl._${cls.name}_Impl'; + + return (macro class TempClass + { + public function _fetchData(id:String) + { + return ${Context.parse(impl, Context.currentPos())}._fetchData(this, id); + } + + public function toString() + { + return ${Context.parse(impl, Context.currentPos())}.toString(this); + } + + public function destroy() + { + ${Context.parse(impl, Context.currentPos())}.destroy(this); + } + }).fields.filter((field) -> return !MacroUtil.fieldAlreadyExists(field.name)); + } + + /** + * Build an internal class that calls functions for an associated registry class. + * @param cls The entry class to build the internal class for. + * @param registryCls The registry class that the entry class is associated with. + */ + static function buildEntryImpl(cls:ClassType, registryCls:ClassType):Void + { + var clsType:ComplexType = Context.getType('${cls.module}.${cls.name}').toComplexType(); + + var registry:String = '${registryCls.module}.${registryCls.name}'; + + Context.defineType( + { + pos: Context.currentPos(), + pack: ['funkin', 'macro', 'impl'], + name: '_${cls.name}_Impl', + kind: TypeDefKind.TDClass(null, [], false, false, false), + fields: (macro class TempClass + { + public static inline function _fetchData(me:$clsType, id:String) + { + return $ + { + Context.parse(registry, Context.currentPos()) + }.instance.parseEntryDataWithMigration(id, ${Context.parse(registry, Context.currentPos())}.instance.fetchEntryVersion(id)); + } + + public static inline function toString(me:$clsType) + { + return $v{cls.name} + '(' + me.id + ')'; + } + + public static inline function destroy(me:$clsType) {} + }).fields + }); + } + + /** + * Check whether this class has already been processed by the RegistryMacro, + * as indicated by the `@:funkinProcessed` meta. + * @param cls The class to check. + * @return `true` if the class has already been processed, `false` otherwise. + */ + static function alreadyProcessed(cls:ClassType):Bool + { + // Check for the `@:funkinProcessed` meta. + var processedMeta:MetadataEntry = cls.meta.get().find(function(m) return m.name == ':funkinProcessed'); + if (processedMeta != null) return true; + + // If it's not found, check the superclass. + if (cls.superClass != null) return alreadyProcessed(cls.superClass.t.get()); + return false; + } + #end +} diff --git a/source/funkin/util/macro/SingletonMacro.hx b/source/funkin/util/macro/SingletonMacro.hx new file mode 100644 index 000000000..4148c7c12 --- /dev/null +++ b/source/funkin/util/macro/SingletonMacro.hx @@ -0,0 +1,46 @@ +package funkin.util.macro; + +import haxe.macro.Context; +import haxe.macro.Type.ClassType; +import haxe.macro.Expr.Field; +import haxe.macro.Expr.ComplexType; + +using haxe.macro.Tools; + +/** + * A macro which automatically creates a Singleton `instance` property for a class. + * Add `implements funkin.util.tools.ISingleton` to your class to use. + */ +class SingletonMacro +{ + /** + * Applies an `instance` static field to the target class. + * @return The modified list of fields for the target class. + */ + public static macro function build():Array + { + var cls:ClassType = Context.getLocalClass().get(); + + var fields:Array = Context.getBuildFields(); + + var clsType:ComplexType = Context.getType('${cls.module}.${cls.name}').toComplexType(); + var newExpr:String = 'new ${cls.module}.${cls.name}()'; + + fields = fields.concat((macro class TempClass + { + static var _instance:Null<$clsType>; + public static var instance(get, never):$clsType; + + static function get_instance():$clsType + { + if (_instance == null) + { + _instance = ${Context.parse(newExpr, Context.currentPos())}; + } + return _instance; + } + }).fields); + + return fields; + } +} diff --git a/source/funkin/util/tools/ISingleton.hx b/source/funkin/util/tools/ISingleton.hx new file mode 100644 index 000000000..642ec617f --- /dev/null +++ b/source/funkin/util/tools/ISingleton.hx @@ -0,0 +1,7 @@ +package funkin.util.tools; + +/** + * An interface which applies a macro to add a Singleton `instance` property to the class. + */ +@:autoBuild(funkin.util.macro.SingletonMacro.build()) +interface ISingleton {} From 963e2ecd630fae24dae4206d315618cf4c4be6db Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Sun, 13 Apr 2025 03:39:31 -0400 Subject: [PATCH 31/40] Edit RegistryMacro to automatically populate baseGameEntryIds list. --- source/funkin/data/BaseRegistry.hx | 13 ++++ .../data/freeplay/player/PlayerRegistry.hx | 20 ------ source/funkin/data/song/SongRegistry.hx | 20 ------ source/funkin/data/stage/StageRegistry.hx | 20 ------ .../funkin/data/story/level/LevelRegistry.hx | 38 +++++----- .../ui/debug/results/ResultsDebugSubState.hx | 2 +- .../stageeditor/components/WelcomeDialog.hx | 2 +- source/funkin/util/macro/DataMacro.hx | 59 --------------- source/funkin/util/macro/RegistryMacro.hx | 72 +++++++++++++++++++ 9 files changed, 109 insertions(+), 137 deletions(-) delete mode 100644 source/funkin/util/macro/DataMacro.hx diff --git a/source/funkin/data/BaseRegistry.hx b/source/funkin/data/BaseRegistry.hx index 882ba9695..dad238478 100644 --- a/source/funkin/data/BaseRegistry.hx +++ b/source/funkin/data/BaseRegistry.hx @@ -190,6 +190,19 @@ abstract class BaseRegistry & Constructible + */ + // public function listBaseGameEntryIds():Array {} + + /** + * A list of all entries that are not included in the base game. + * @return Array + */ + // public function listModdedEntryIds():Array {} + public function toString():String { return 'Registry(' + registryId + ', ${countEntries()} entries)'; diff --git a/source/funkin/data/freeplay/player/PlayerRegistry.hx b/source/funkin/data/freeplay/player/PlayerRegistry.hx index b6c3af152..d584398e1 100644 --- a/source/funkin/data/freeplay/player/PlayerRegistry.hx +++ b/source/funkin/data/freeplay/player/PlayerRegistry.hx @@ -18,8 +18,6 @@ class PlayerRegistry extends BaseRegistry impleme public static final PLAYER_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x"; - static final baseGamePlayerIds:Array = funkin.util.macro.DataMacro.listBaseGamePlayerIds(); - /** * A mapping between stage character IDs and Freeplay playable character IDs. */ @@ -125,22 +123,4 @@ class PlayerRegistry extends BaseRegistry impleme { return ownedCharacterIds.exists(characterId); } - - /** - * A list of all the playable characters from the base game, in order. - */ - public function listBaseGamePlayerIds():Array - { - return baseGamePlayerIds; - } - - /** - * A list of all installed playable characters that are not from the base game. - */ - public function listModdedPlayerIds():Array - { - return listEntryIds().filter(function(id:String):Bool { - return listBaseGamePlayerIds().indexOf(id) == -1; - }); - } } diff --git a/source/funkin/data/song/SongRegistry.hx b/source/funkin/data/song/SongRegistry.hx index b1a5c0333..fcdabc91f 100644 --- a/source/funkin/data/song/SongRegistry.hx +++ b/source/funkin/data/song/SongRegistry.hx @@ -42,8 +42,6 @@ class SongRegistry extends BaseRegistry implements ISingleto return '${Constants.TITLE} - ${Constants.VERSION}'; } - static final baseGameSongIds:Array = funkin.util.macro.DataMacro.listBaseGameSongIds(); - public function new() { super('SONG', 'songs', SONG_METADATA_VERSION_RULE); @@ -484,24 +482,6 @@ class SongRegistry extends BaseRegistry implements ISingleto return chartData; } - /** - * A list of all the story weeks from the base game, in order. - */ - public function listBaseGameSongIds():Array - { - return baseGameSongIds; - } - - /** - * A list of all installed story weeks that are not from the base game. - */ - public function listModdedSongIds():Array - { - return listEntryIds().filter(function(id:String):Bool { - return listBaseGameSongIds().indexOf(id) == -1; - }); - } - /** * A list of all difficulties for a specific character. */ diff --git a/source/funkin/data/stage/StageRegistry.hx b/source/funkin/data/stage/StageRegistry.hx index 7ff63efde..72c8cb002 100644 --- a/source/funkin/data/stage/StageRegistry.hx +++ b/source/funkin/data/stage/StageRegistry.hx @@ -16,28 +16,8 @@ class StageRegistry extends BaseRegistry implements ISingleton public static final STAGE_DATA_VERSION_RULE:thx.semver.VersionRule = ">=1.0.0 <1.1.0"; - static final baseGameStageIds:Array = funkin.util.macro.DataMacro.listBaseGameStageIds(); - public function new() { super('STAGE', 'stages', STAGE_DATA_VERSION_RULE); } - - /** - * A list of all the stages from the base game, in order. - */ - public function listBaseGameStageIds():Array - { - return baseGameStageIds; - } - - /** - * A list of all installed story weeks that are not from the base game. - */ - public function listModdedStageIds():Array - { - return listEntryIds().filter(function(id:String):Bool { - return listBaseGameStageIds().indexOf(id) == -1; - }); - } } diff --git a/source/funkin/data/story/level/LevelRegistry.hx b/source/funkin/data/story/level/LevelRegistry.hx index 07e18f9a5..4e4f66019 100644 --- a/source/funkin/data/story/level/LevelRegistry.hx +++ b/source/funkin/data/story/level/LevelRegistry.hx @@ -17,8 +17,6 @@ class LevelRegistry extends BaseRegistry implements ISingleton public static final LEVEL_DATA_VERSION_RULE:thx.semver.VersionRule = ">=1.0.0 <1.1.0"; - static final baseGameLevelIds:Array = funkin.util.macro.DataMacro.listBaseGameLevelIds(); - public function new() { super('LEVEL', 'levels', LEVEL_DATA_VERSION_RULE); @@ -26,26 +24,34 @@ class LevelRegistry extends BaseRegistry implements ISingleton /** * A list of all the story weeks from the base game, in order. + * @return Array */ - public function listBaseGameLevelIds():Array + public function listBaseGameEntryIds():Array { - return baseGameLevelIds; - } - - public function listSortedLevelIds():Array - { - var result = listEntryIds(); - result.sort(SortUtil.defaultsThenAlphabetically.bind(listBaseGameLevelIds())); - return result; + // This MUST be hard-coded (overriding the auto-generated method) + // because the auto-generated method spits out values in alphabetical order. + return [ + 'tutorial', + 'week1', + 'week2', + 'week3', + 'week4', + 'week5', + 'week6', + 'week7', + 'weekend1' + ]; } /** - * A list of all installed story weeks that are not from the base game. + * A list of all the story weeks in the game, in order. + * Modded levels are in alphabetical order at the end of the list. + * @return Array */ - public function listModdedLevelIds():Array + public function listSortedLevelIds():Array { - return listEntryIds().filter(function(id:String):Bool { - return listBaseGameLevelIds().indexOf(id) == -1; - }); + var result:Array = listEntryIds(); + result.sort(SortUtil.defaultsThenAlphabetically.bind(listBaseGameEntryIds())); + return result; } } diff --git a/source/funkin/ui/debug/results/ResultsDebugSubState.hx b/source/funkin/ui/debug/results/ResultsDebugSubState.hx index 1d4245258..39dd19d55 100644 --- a/source/funkin/ui/debug/results/ResultsDebugSubState.hx +++ b/source/funkin/ui/debug/results/ResultsDebugSubState.hx @@ -54,7 +54,7 @@ class ResultsDebugSubState extends MusicBeatSubState FlxG.switchState(() -> new funkin.play.ResultState(resultsParams)); }); }); - createToggleListItem("Character", PlayerRegistry.instance.listBaseGamePlayerIds(), function(result:String) { + createToggleListItem("Character", PlayerRegistry.instance.listBaseGameEntryIds(), function(result:String) { resultsParams.characterId = result; }); createToggleListItem("Results Mode", ["Debug", "Story", "Freeplay"], function(result:String) { diff --git a/source/funkin/ui/debug/stageeditor/components/WelcomeDialog.hx b/source/funkin/ui/debug/stageeditor/components/WelcomeDialog.hx index 2794da465..5cc07fbe9 100644 --- a/source/funkin/ui/debug/stageeditor/components/WelcomeDialog.hx +++ b/source/funkin/ui/debug/stageeditor/components/WelcomeDialog.hx @@ -56,7 +56,7 @@ class WelcomeDialog extends Dialog boxDrag.onClick = function(_) FileUtil.browseForSaveFile([FileUtil.FILE_FILTER_FNFS], loadFromFilePath, null, null, "Open Stage Data"); - var defaultStages = StageRegistry.instance.listBaseGameStageIds(); + var defaultStages:Array = StageRegistry.instance.listBaseGameEntryIds(); defaultStages.sort(funkin.util.SortUtil.alphabetically); for (stage in defaultStages) diff --git a/source/funkin/util/macro/DataMacro.hx b/source/funkin/util/macro/DataMacro.hx deleted file mode 100644 index c5898044b..000000000 --- a/source/funkin/util/macro/DataMacro.hx +++ /dev/null @@ -1,59 +0,0 @@ -package funkin.util.macro; - -import haxe.macro.Expr; -#if macro -import sys.FileSystem; -#end - -using StringTools; - -class DataMacro -{ - public static macro function listBaseGameStageIds():Expr - { - var folder = "assets/preload/data/stages/"; - var files = FileSystem.readDirectory(folder); - var stages:Array = []; - - for (file in files) - stages.push(macro $v{file.replace(".json", "")}); - - return macro $a{stages}; - } - - public static macro function listBaseGameSongIds():Expr - { - var folder = "assets/songs/"; - var files = FileSystem.readDirectory(folder); - var songs:Array = []; - - for (file in files) - songs.push(macro $v{file}); - - return macro $a{songs}; - } - - public static macro function listBaseGameLevelIds():Expr - { - var folder = "assets/preload/data/levels/"; - var files = FileSystem.readDirectory(folder); - var levels:Array = []; - - for (file in files) - levels.push(macro $v{file.replace(".json", "")}); - - return macro $a{levels}; - } - - public static macro function listBaseGamePlayerIds():Expr - { - var folder = "assets/preload/data/players/"; - var files = FileSystem.readDirectory(folder); - var players:Array = []; - - for (file in files) - players.push(macro $v{file.replace(".json", "")}); - - return macro $a{players}; - } -} diff --git a/source/funkin/util/macro/RegistryMacro.hx b/source/funkin/util/macro/RegistryMacro.hx index e11687001..bc16e92be 100644 --- a/source/funkin/util/macro/RegistryMacro.hx +++ b/source/funkin/util/macro/RegistryMacro.hx @@ -8,6 +8,7 @@ import haxe.macro.Expr.TypeDefKind; import haxe.macro.Expr.MetadataEntry; import haxe.macro.Type; import haxe.macro.Type.ClassType; +import sys.FileSystem; using Lambda; using haxe.macro.ExprTools; @@ -38,6 +39,8 @@ typedef RegistryTypeParams = */ class RegistryMacro { + static final DATA_FILE_BASE_PATH:String = 'assets/preload/data'; + /** * Builds the registry class. * @@ -139,8 +142,24 @@ class RegistryMacro var createScriptedEntry:String = '${scriptedEntryClsName}.init(clsName, "unknown")'; var newJsonParser:String = 'new json2object.JsonParser<${dataType.module}.${dataType.name}>()'; + + var dataFilePath:String = getRegistryDataFilePath(cls, fields); + var baseGameEntryIds:Array = listBaseGameEntryIds('${DATA_FILE_BASE_PATH}/${dataFilePath}/'); + return (macro class TempClass { + public function listBaseGameEntryIds():Array + { + return $a{baseGameEntryIds}; + } + + public function listModdedEntryIds():Array + { + return listEntryIds().filter(function(id:String):Bool { + return listBaseGameEntryIds().indexOf(id) == -1; + }); + } + function getScriptedClassNames() { return ${Context.parse(getScriptedClassName, Context.currentPos())}.listScriptClasses(); @@ -300,6 +319,59 @@ class RegistryMacro }); } + static function getRegistryDataFilePath(cls:ClassType, fields:Array):String + { + for (field in fields) + { + if (field.name == 'new') + { + // We found a field called `new`, it's probably the constructor. + switch (field.kind) + { + case FFun(f): + // Inside the function. + switch (f.expr.expr) + { + // Inside the block. + case EBlock(exprs): + var superCall:Expr = exprs[0]; + switch (superCall.expr) + { + // Inside the super() call. + case ECall(_, args): + // var registryId:String = args[0].toString(); + var dataPath:String = args[1].toString().replace('"', '').replace("'", ''); + // var versionRule = args[2].toString(); + + return dataPath; + default: + Context.error('${cls.name}.new: RegistryMacro expected super call', field.pos); + } + default: + Context.error('${cls.name}.new: RegistryMacro expected super call', field.pos); + } + default: + // Continue looking for the actual constructor. + } + } + } + + return ''; + } + + static function listBaseGameEntryIds(dataFilePath:String):Array + { + var result:Array = []; + var files:Array = FileSystem.readDirectory(dataFilePath); + + for (file in files) + { + result.push(macro $v{file.replace('.json', '')}); + } + + return result; + } + /** * Check whether this class has already been processed by the RegistryMacro, * as indicated by the `@:funkinProcessed` meta. From 855deb58280e850c0ad1581807c70f05d6b6a2cb Mon Sep 17 00:00:00 2001 From: Kolo <67389779+JustKolosaki@users.noreply.github.com> Date: Sat, 12 Apr 2025 20:59:51 +0200 Subject: [PATCH 32/40] set enabled when prompt gone --- source/funkin/ui/options/OptionsState.hx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/source/funkin/ui/options/OptionsState.hx b/source/funkin/ui/options/OptionsState.hx index 683088700..4337495ed 100644 --- a/source/funkin/ui/options/OptionsState.hx +++ b/source/funkin/ui/options/OptionsState.hx @@ -145,6 +145,12 @@ class OptionsMenu extends Page return item; } + override function update(elapsed:Float) + { + enabled = (prompt == null); + super.update(elapsed); + } + override function set_enabled(value:Bool) { items.enabled = value; From be5d6227a7b40bb169dd9bac6831d43489db0f03 Mon Sep 17 00:00:00 2001 From: kade-github <26305836+Kade-github@users.noreply.github.com> Date: Thu, 17 Apr 2025 11:38:14 -0700 Subject: [PATCH 33/40] erect nearest neighbor select --- source/funkin/ui/freeplay/FreeplayState.hx | 46 ++++++++++++++++++++-- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index 677ef32ba..6b817f91c 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -1654,6 +1654,36 @@ class FreeplayState extends MusicBeatSubState FlxG.cameras.remove(funnyCam); } + /** + * findClosestDiff will find the closest difficulty to the given diff. + * It will return the index of the closest song in the grpCapsules.members array. + * @param diff + * @return Int + */ + function findClosestDiff(characterVariations:Array, diff:String):Int + { + var closestIndex:Int = 0; + var closest:Int = curSelected; + + for (index in 0...grpCapsules.members.length) + { + var song:Null = grpCapsules.members[index].freeplayData; + if (song == null) continue; + var songDiff:Null = song.data.getDifficulty(diff, null, characterVariations)?.difficulty; + // if the difference between the current index and this index is the smallest so far, + // take this one as the closest index. (By comparing with the closest variable) + var c:Int = curSelected - index; + if (songDiff == diff && (Math.abs(c) < Math.abs(closestIndex - curSelected) || closestIndex == 0)) + { + // trace('Found closest diff: ${songDiff} at index ${index} (current: ${curSelected})'); + closestIndex = index; + closest = c; + } + } + + return closestIndex; + } + /** * changeDiff is the root of both difficulty and variation changes/management. * It will check the difficulty of the current variation, all available variations, and all available difficulties per variation. @@ -1669,10 +1699,10 @@ class FreeplayState extends MusicBeatSubState // Available variations for current character. We get this since bf is usually `default` variation, and `pico` is `pico` // but sometimes pico can be the default variation (weekend 1 songs), and bf can be `bf` variation (darnell) var characterVariations:Array = grpCapsules.members[curSelected].freeplayData?.data.getVariationsByCharacter(currentCharacter) ?? Constants.DEFAULT_VARIATION_LIST; - + var difficultiesAvailable:Array = allDifficulties; // Gets all available difficulties for our character, via our available variations - var difficultiesAvailable:Array = grpCapsules.members[curSelected].freeplayData?.data.listDifficulties(null, - characterVariations) ?? allDifficulties; + var songDifficulties:Array = grpCapsules.members[curSelected].freeplayData?.data.listDifficulties(null, + characterVariations) ?? Constants.DEFAULT_DIFFICULTY_LIST; var currentDifficultyIndex:Int = difficultiesAvailable.indexOf(currentDifficulty); @@ -1682,9 +1712,17 @@ class FreeplayState extends MusicBeatSubState if (currentDifficultyIndex < 0) currentDifficultyIndex = Std.int(difficultiesAvailable.length - 1); if (currentDifficultyIndex >= difficultiesAvailable.length) currentDifficultyIndex = 0; - // Update the current difficulty currentDifficulty = difficultiesAvailable[currentDifficultyIndex]; + // For when we change the difficulty, but the song doesn't have that difficulty! + if (!songDifficulties.contains(difficultiesAvailable[currentDifficultyIndex])) + { + // If the current difficulty is not available, find the closest one that is. + characterVariations.push("erect"); + curSelected = findClosestDiff(characterVariations, difficultiesAvailable[currentDifficultyIndex]); + rememberedSongId = grpCapsules.members[curSelected].freeplayData?.data.id ?? rememberedSongId; + } + for (variation in characterVariations) { if (grpCapsules.members[curSelected].freeplayData?.data.hasDifficulty(currentDifficulty, variation) ?? false) From 09fdd0c91ef6e5eabb92b13fcd3e18fd3524c64e Mon Sep 17 00:00:00 2001 From: kade-github <26305836+Kade-github@users.noreply.github.com> Date: Thu, 17 Apr 2025 13:37:14 -0700 Subject: [PATCH 34/40] going to erect with the erect takes you to the closest erect --- source/funkin/ui/freeplay/FreeplayState.hx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index 6b817f91c..39663ca05 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -768,6 +768,7 @@ class FreeplayState extends MusicBeatSubState rememberSelection(); changeSelection(); refreshCapsuleDisplays(); + } /** @@ -1669,7 +1670,8 @@ class FreeplayState extends MusicBeatSubState { var song:Null = grpCapsules.members[index].freeplayData; if (song == null) continue; - var songDiff:Null = song.data.getDifficulty(diff, null, characterVariations)?.difficulty; + var characterVar = song.data.getVariationsByCharacter(currentCharacter); + var songDiff:Null = song.data.getDifficulty(diff, null, characterVar)?.difficulty; // if the difference between the current index and this index is the smallest so far, // take this one as the closest index. (By comparing with the closest variable) var c:Int = curSelected - index; @@ -1717,10 +1719,8 @@ class FreeplayState extends MusicBeatSubState // For when we change the difficulty, but the song doesn't have that difficulty! if (!songDifficulties.contains(difficultiesAvailable[currentDifficultyIndex])) { - // If the current difficulty is not available, find the closest one that is. - characterVariations.push("erect"); curSelected = findClosestDiff(characterVariations, difficultiesAvailable[currentDifficultyIndex]); - rememberedSongId = grpCapsules.members[curSelected].freeplayData?.data.id ?? rememberedSongId; + rememberedSongId = grpCapsules.members[curSelected].freeplayData?.data.id; } for (variation in characterVariations) @@ -2107,7 +2107,7 @@ class FreeplayState extends MusicBeatSubState } #end instSuffix = (instSuffix != '') ? '-$instSuffix' : ''; - trace('Attempting to play partial preview: ${previewSong.id}:${instSuffix}'); + // trace('Attempting to play partial preview: ${previewSong.id}:${instSuffix}'); FunkinSound.playMusic(previewSong.id, { startingVolume: 0.0, From 095bc9006d6cb093dec7c7a63320b7054a05565f Mon Sep 17 00:00:00 2001 From: kade-github <26305836+Kade-github@users.noreply.github.com> Date: Thu, 17 Apr 2025 14:54:54 -0700 Subject: [PATCH 35/40] animation on all diffs --- source/funkin/ui/freeplay/FreeplayState.hx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index 39663ca05..5fc844734 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -735,7 +735,9 @@ class FreeplayState extends MusicBeatSubState if (fromCharSelect) randomCapsule.forcePosition(); else + { randomCapsule.initJumpIn(0, force); + } var hsvShader:HSVShader = new HSVShader(); randomCapsule.hsvShader = hsvShader; @@ -1557,13 +1559,14 @@ class FreeplayState extends MusicBeatSubState { if (dj != null) dj.resetAFKTimer(); changeDiff(-1); - generateSongList(currentFilter, true); + generateSongList(currentFilter, true, false); } if (controls.UI_RIGHT_P) { if (dj != null) dj.resetAFKTimer(); changeDiff(1); - generateSongList(currentFilter, true); + generateSongList(currentFilter, true, false); + } if (controls.BACK && !busy) From 750779a3fb914459575ab3bbaf0a1a74e0abdd36 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 17 Apr 2025 18:20:48 -0400 Subject: [PATCH 36/40] Disable the pop-in animation on the Random capsule for consistency. --- source/funkin/ui/freeplay/FreeplayState.hx | 33 ++++++++++++++++------ source/funkin/ui/freeplay/SongMenuItem.hx | 21 ++++++++++++-- 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index 5fc844734..2ec105ea0 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -703,8 +703,20 @@ class FreeplayState extends MusicBeatSubState if (onlyIfChanged) { - // == performs equality by reference - if (tempSongs.isEqualUnordered(currentFilteredSongs)) return; + if (tempSongs.isEqualUnordered(currentFilteredSongs)) + { + // If the song list is the same, we don't need to generate a new list. + + // Instead, we just apply the jump-in animation to the existing capsules. + for (capsule in grpCapsules.members) + { + capsule.initPosition(FlxG.width, 0); + capsule.initJumpIn(0, force); + } + + // Stop processing. + return; + } } // Only now do we know that the filter is actually changing. @@ -721,7 +733,8 @@ class FreeplayState extends MusicBeatSubState // Initialize the random capsule, with empty/blank info (which we display once bf/pico does his hand) var randomCapsule:SongMenuItem = grpCapsules.recycle(SongMenuItem); - randomCapsule.init(FlxG.width, 0, null, styleData); + randomCapsule.initPosition(FlxG.width, 0); + randomCapsule.initData(null, styleData); randomCapsule.y = randomCapsule.intendedY(0) + 10; randomCapsule.targetPos.x = randomCapsule.x; randomCapsule.alpha = 0; @@ -736,7 +749,8 @@ class FreeplayState extends MusicBeatSubState if (fromCharSelect) randomCapsule.forcePosition(); else { - randomCapsule.initJumpIn(0, force); + // randomCapsule.initJumpIn(0, force); + randomCapsule.forcePosition(); } var hsvShader:HSVShader = new HSVShader(); @@ -750,7 +764,8 @@ class FreeplayState extends MusicBeatSubState var funnyMenu:SongMenuItem = grpCapsules.recycle(SongMenuItem); - funnyMenu.init(FlxG.width, 0, tempSong, styleData); + funnyMenu.initPosition(FlxG.width, 0); + funnyMenu.initData(tempSong, styleData); funnyMenu.onConfirm = function() { capsuleOnOpenDefault(funnyMenu); }; @@ -760,8 +775,12 @@ class FreeplayState extends MusicBeatSubState funnyMenu.capsule.alpha = 0.5; funnyMenu.hsvShader = hsvShader; funnyMenu.newText.animation.curAnim.curFrame = 45 - ((i * 4) % 45); + + // This stops the bounce-in animation. funnyMenu.forcePosition(); + // funnyMenu.initJumpIn(0, force); + grpCapsules.add(funnyMenu); } @@ -770,7 +789,6 @@ class FreeplayState extends MusicBeatSubState rememberSelection(); changeSelection(); refreshCapsuleDisplays(); - } /** @@ -1566,7 +1584,6 @@ class FreeplayState extends MusicBeatSubState if (dj != null) dj.resetAFKTimer(); changeDiff(1); generateSongList(currentFilter, true, false); - } if (controls.BACK && !busy) @@ -1795,7 +1812,7 @@ class FreeplayState extends MusicBeatSubState if (songCapsule.freeplayData != null) { - songCapsule.init(null, null, songCapsule.freeplayData); + songCapsule.initData(songCapsule.freeplayData); songCapsule.checkClip(); } } diff --git a/source/funkin/ui/freeplay/SongMenuItem.hx b/source/funkin/ui/freeplay/SongMenuItem.hx index 665d6fe59..627ca69d8 100644 --- a/source/funkin/ui/freeplay/SongMenuItem.hx +++ b/source/funkin/ui/freeplay/SongMenuItem.hx @@ -494,10 +494,14 @@ class SongMenuItem extends FlxSpriteGroup updateSelected(); } - public function init(?x:Float, ?y:Float, freeplayData:Null, ?styleData:FreeplayStyle = null):Void + public function initPosition(x:Float, y:Float):Void + { + this.x = x; + this.y = y; + } + + public function initData(freeplayData:Null, ?styleData:FreeplayStyle = null):Void { - if (x != null) this.x = x; - if (y != null) this.y = y; this.freeplayData = freeplayData; // im so mad i have to do this but im pretty sure with the capsules recycling i cant call the new function properly :/ @@ -679,6 +683,17 @@ class SongMenuItem extends FlxSpriteGroup if (selected && songText.tooLong) songText.initMove(); } + + public override function kill():Void + { + super.kill(); + + visible = true; + capsule.alpha = 1; + doLerp = false; + doJumpIn = false; + doJumpOut = false; + } } /** From 7eebce432d7627a1cf0bbd43f22a6f9bcb63ff65 Mon Sep 17 00:00:00 2001 From: Kade <26305836+Kade-github@users.noreply.github.com> Date: Thu, 17 Apr 2025 15:50:40 -0700 Subject: [PATCH 37/40] [PUBLIC PR] Prevent softlock caused by removing all keybinds for a UI control (#988) * prevent removing last keybind for UI controls * add popup to alert you cannot remove keybind * ensure controls list is unique * Use public submodule URLs (would be nice if this was automatic but oh well) --------- Co-authored-by: VioletSnowLeopard Co-authored-by: EliteMasterEric --- source/funkin/ui/options/ControlsMenu.hx | 70 +++++++++++++++++++++++- 1 file changed, 67 insertions(+), 3 deletions(-) diff --git a/source/funkin/ui/options/ControlsMenu.hx b/source/funkin/ui/options/ControlsMenu.hx index c9935fbbe..b1cce7665 100644 --- a/source/funkin/ui/options/ControlsMenu.hx +++ b/source/funkin/ui/options/ControlsMenu.hx @@ -44,12 +44,15 @@ class ControlsMenu extends Page var deviceList:TextMenuList; var menuCamera:FlxCamera; var prompt:Prompt; + var popup:Prompt; var camFollow:FlxObject; var labels:FlxTypedGroup; var currentDevice:Device = Keys; var deviceListSelected:Bool = false; + var actionPrevented:Bool = false; + static final CONTROL_BASE_X = 50; static final CONTROL_MARGIN_X = 700; static final CONTROL_SPACING_X = 300; @@ -181,6 +184,13 @@ class ControlsMenu extends Page prompt.back.scrollFactor.set(0, 0); prompt.exists = false; add(prompt); + + popup = new Prompt("\nYou cannot unbind\nthat key!\n\n\nEscape to exit", None); + popup.create(); + popup.createBgFromMargin(100, 0xFFfafd6d); + popup.back.scrollFactor.set(0, 0); + popup.exists = false; + add(popup); } function createItem(x = 0.0, y = 0.0, control:Control, index:Int) @@ -213,6 +223,12 @@ class ControlsMenu extends Page prompt.exists = true; } + function createPopup():Void + { + canExit = false; + popup.exists = true; + } + function goToDeviceList():Void { controlGrid.selectedItem.idle(); @@ -234,9 +250,16 @@ class ControlsMenu extends Page var inputName = device == Keys ? "key" : "button"; var cancel = device == Keys ? "Escape" : "Back"; // todo: alignment - if (device == Keys) prompt.setText('\nPress any key to rebind\n\n\n\n $cancel to cancel'); + if (device == Keys) + { + prompt.setText('\nPress any key to rebind\n\n\n\n $cancel to cancel'); + popup.setText('\nYou cannot unbind\nthat key!\n\n\n$cancel to exit'); + } else + { prompt.setText('\nPress any button\n to rebind\n\n\n $cancel to cancel'); + popup.setText('\nYou cannot unbind\nthat button!\n\n\n$cancel to exit'); + } controlGrid.selectedItem.select(); labels.members[Std.int(controlGrid.selectedIndex / COLUMNS)].alpha = 1.0; @@ -313,6 +336,28 @@ class ControlsMenu extends Page } } + if (actionPrevented && !popup.exists) createPopup(); + + if (popup.exists) + { + switch (currentDevice) + { + case Keys: + { + var key = FlxG.keys.firstJustReleased(); + if (key == ESCAPE) closePopup(); + } + case Gamepad(id): + { + var button = FlxG.gamepads.getByID(id).firstJustReleasedID(); + if (button == BACK) closePopup(); + + var key = FlxG.keys.firstJustReleased(); + if (key == ESCAPE) closePopup(); + } + } + } + switch (currentDevice) { case Keys: @@ -339,6 +384,19 @@ class ControlsMenu extends Page function onInputSelect(input:Int):Void { var item = controlGrid.selectedItem; + var leftItem = controlGrid.members[controlGrid.selectedIndex - 1]; + var rightItem = controlGrid.members[controlGrid.selectedIndex + 1]; + + // check if all keybinds are being removed and this is a UI control, prevent removing last keybind for this + if (input == FlxKey.NONE && controlGrid.selectedIndex != 1 && rightItem.input == FlxKey.NONE) + { + for (group in itemGroups) + { + if (group.toString() == itemGroups[1].toString() && group.contains(item)) actionPrevented = true; + } + } + + if (actionPrevented) return; // check if that key is already set for this if (input != FlxKey.NONE) @@ -379,7 +437,6 @@ class ControlsMenu extends Page if (controlGrid.selectedIndex % 2 == 1) { trace('Modified item on right side of grid'); - var leftItem = controlGrid.members[controlGrid.selectedIndex - 1]; if (leftItem != null && input != FlxKey.NONE && leftItem.input == FlxKey.NONE) { trace('Left item is unbound and right item is not!'); @@ -395,7 +452,6 @@ class ControlsMenu extends Page else { trace('Modified item on left side of grid'); - var rightItem = controlGrid.members[controlGrid.selectedIndex + 1]; if (rightItem != null && input == FlxKey.NONE && rightItem.input != FlxKey.NONE) { trace('Left item is unbound and right item is not!'); @@ -419,6 +475,13 @@ class ControlsMenu extends Page if (deviceList == null) canExit = true; } + function closePopup() + { + popup.exists = false; + actionPrevented = false; + if (deviceList == null) canExit = true; + } + override function destroy() { super.destroy(); @@ -476,6 +539,7 @@ class InputItem extends TextMenuItem function getInput() { var list = PlayerSettings.player1.controls.getInputsFor(control, device); + list = list.distinct(); if (list.length > index) { if (list[index] != FlxKey.ESCAPE || list[index] != FlxGamepadInputID.BACK) return list[index]; From ee906ae8c0086b6ba3e49b94de128308ed63b2d9 Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Thu, 17 Apr 2025 18:55:46 -0400 Subject: [PATCH 38/40] fix: make the sound tray volume be less loud (#990) --- source/funkin/ui/options/FunkinSoundTray.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/funkin/ui/options/FunkinSoundTray.hx b/source/funkin/ui/options/FunkinSoundTray.hx index 0eab4a0c0..047c86c96 100644 --- a/source/funkin/ui/options/FunkinSoundTray.hx +++ b/source/funkin/ui/options/FunkinSoundTray.hx @@ -142,7 +142,7 @@ class FunkinSoundTray extends FlxSoundTray var sound:String = up ? volumeUpSound : volumeDownSound; if (globalVolume == 10) sound = volumeMaxSound; - if (sound != null) FlxG.sound.load(sound).play(); + if (sound != null) FlxG.sound.load(sound).play().volume = 0.3; } return globalVolume; From 6de51848fda9bf4471e138564b952d328c32bca2 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 17 Apr 2025 19:17:06 -0400 Subject: [PATCH 39/40] Update assets submodule --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index 086ae8b58..44011f190 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 086ae8b587d39ba4907646c1776c20b07ed0f0ae +Subproject commit 44011f190e38d00f7d398ac7b3b1a954a2ca1208 From 5bf8154e0b53dd3c5b79a9d0d394b53645aa51d2 Mon Sep 17 00:00:00 2001 From: fabs Date: Thu, 17 Apr 2025 01:57:11 +0100 Subject: [PATCH 40/40] otis muzzle flashing (assets submod) --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index 44011f190..6342a5536 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 44011f190e38d00f7d398ac7b3b1a954a2ca1208 +Subproject commit 6342a55361058e9c7c070eb6437267eac9cd06ce