Compare commits

...
165 Commits
Author SHA1 Message Date
Hundrec 90eabf0b38 Bump docs and templates to 0.8.4
Build and Upload nightly game builds / gather-changes (push) Canceled after 0s
Build and Upload nightly game builds / gather-tags (push) Canceled after 0s
Build and Upload nightly game builds / build-game-on-host (macos, macos) (push) Canceled after 0s
Build and Upload nightly game builds / build-game-on-host (windows, windows) (push) Canceled after 0s
Build and Upload nightly game builds / docker-image (push) Canceled after 0s
Build and Upload nightly game builds / build-game-in-container (html5) (push) Canceled after 0s
Build and Upload nightly game builds / build-game-in-container (linux) (push) Canceled after 0s
2026-03-25 16:19:29 -06:00
Hundrec b3959a5935 Bump version to 0.8.4 2026-03-22 00:02:55 -06:00
HundrecandAbnormal cdca3bf307 Changelog for 0.8.4
Co-Authored-By: Abnormal <86753001+AbnormalPoof@users.noreply.github.com>
2026-03-22 00:01:19 -06:00
Karim AkraandHundrec e491c5e841 [CHORE] rename mobile FNFCUtil classes for a cleaner structure 2026-03-21 16:01:13 -06:00
Karim AkraandHundrec f9b125e44a [FIX] Bump lime to fix FNFC loading on iOS 2026-03-21 16:01:13 -06:00
VirtuGuyandHundrec 1ceb4fad1a Fix ingame cutscenes crashing on mobile 2026-03-21 06:40:41 -06:00
Hyper_andHundrec 21fc59c997 Fix TextureSwap not loading ASTC assets properly 2026-03-21 06:40:33 -06:00
EliteMasterEricandHundrec 0cbde53f86 Update default value just in case resetEvents(events) doesn't get called. 2026-03-21 04:57:45 -06:00
VirtuGuyandHundrec 5119cf1b72 Fix songs crashing 2026-03-21 04:57:45 -06:00
927f3fc300 Use an optional argument for event querying
Co-Authored-By: Eric <4635334+EliteMasterEric@users.noreply.github.com>
2026-03-20 16:54:27 -06:00
EliteMasterEricandHundrec d1e4a17f46 Add null checks to some scripts 2026-03-20 16:52:53 -06:00
ec868f8ef5 Fix HTML5 not compiling due to blacklisted Polymod field
Co-Authored-By: Kolo <67389779+KoloInDaCrib@users.noreply.github.com>
Co-Authored-By: Keoiki <55053690+Keoiki@users.noreply.github.com>
2026-03-20 04:19:23 -06:00
MAJigsaw77andHundrec 1e0a238463 Patch extension-admob setup command so theres no unexpected behaviour 2026-03-20 03:58:18 -06:00
Hundrec e07e59a288 Undo No Animation hold timer change
It was making animations behave differently after a change to hold timers
2026-03-19 23:29:42 -06:00
ADA-FunniandHundrec d6410d5e42 Fallback for old song metadata files causing the song preview to go mute 2026-03-19 23:29:42 -06:00
TrayfellowandHundrec 5974c1a482 Fix freeplay percentage not fully exiting out if it has 3 digits 2026-03-19 21:02:20 -06:00
KoloandHundrec ffdcbdbf21 optimize the script event dispatching 2026-03-19 21:02:20 -06:00
KoloandHundrec 3b27ec5b66 optimization: potentially optimize how events are queried 2026-03-19 21:02:20 -06:00
FuroandHundrec 56a56dbfaa Include lime and openfl for modding 2026-03-19 20:00:18 -06:00
EliteMasterEricandHundrec 537bd5dd83 Add a few more items to the blacklist 2026-03-19 20:00:18 -06:00
charlesisfelineandHundrec b0fd471f8a Blacklist 'readObject` from scripts 2026-03-19 20:00:18 -06:00
ImVeryBadandHundrec 3633df3fdc Add makeVarArgs to ReflectUtil.hx 2026-03-19 20:00:18 -06:00
VirtuGuyandHundrec e6234397f5 Fix camera movement when paused 2026-03-19 19:34:04 -06:00
MAJigsaw77andHundrec 68ce5ce90b Update lime 2026-03-19 18:50:43 -06:00
MAJigsaw77andHundrec a6066f9905 Update flixel-addons 2026-03-19 04:21:37 -06:00
KolzeandHundrec 75506f71a9 Fix spirit trail being fps-dependent 2026-03-19 04:21:25 -06:00
RaltyandHundrec c4b5462a5a Remove duplicated code in project.hxp 2026-03-19 03:15:35 -06:00
MAJigsaw77andHundrec 47d4d77c86 Update hxcpp 2026-03-19 02:50:50 -06:00
MAJigsaw77andHundrec acf17f6165 Properly adjust the debug display math based of the supported mem usages 2026-03-19 02:50:50 -06:00
MAJigsaw77andHundrec 2db26070c6 Properly delete the assets folder on hl across all desktop targets 2026-03-19 02:50:50 -06:00
MAJigsaw77andHundrec e0f9c7336b Update libs refs 2026-03-19 02:50:50 -06:00
AbnormalPoofandHundrec a28a28ae4c Clamp the DPI scale to 1 in the blend shader 2026-03-18 00:26:45 -06:00
Hundrec 7160f748eb Tweak dropdown width 2026-03-17 23:55:20 -06:00
sector-aandHundrec c8e60bed34 Add additional adjustments in the ZoomCamerasong event for widescreen 2026-03-17 23:55:19 -06:00
sector-aandHundrec 11c826daa6 Resize ENUM dropdowns to make them fit in the frames 2026-03-17 23:55:03 -06:00
sector-aandHundrec 6c887b4632 Put some of least used event params under Advanced frame. 2026-03-17 23:55:03 -06:00
MAJigsaw77andHundrec ecfb897dae Remove useless prints 2026-03-17 22:59:20 -06:00
AbnormalPoofandHundrec 56d5767004 Fix HashLink not clearing assets on macOS 2026-03-17 22:59:12 -06:00
MAJigsaw77andHundrec 7d3bfa32ca Run clearAssets only on non display 2026-03-17 22:59:04 -06:00
KolzeandHundrec 348647f486 Fix global offset changing speed 2026-03-17 22:58:00 -06:00
ExcodeandHundrec 48ed659800 Update LevelData.hx 2026-03-17 22:57:43 -06:00
ExcodeandHundrec 8aafe62f9c Update LevelProp.hx 2026-03-17 22:57:43 -06:00
MrMaderaandHundrec 0f7859bfb6 Gamepad binds showed in Freeplay's hint
_lastDeviceUsed -> lastDeviceUsed
2026-03-17 22:56:32 -06:00
Hundrec c3d3d2e547 Move Week 6 Erect results transition further down 2026-03-17 02:52:50 -06:00
37dc3461af Re-implement curNoteKind.suffix
Co-authored-by: TechnikTil <techniktil@tilnotdrip.org>
2026-03-17 02:52:14 -06:00
MightyTheArmiddiloandHundrec c11ac1f658 Fix Compile Errors With FLX_NO_DEBUG 2026-03-17 02:52:10 -06:00
TrayfellowandHundrec 47bbf67ae6 Updated level schema version 2026-03-17 02:51:46 -06:00
TrayfellowandHundrec 528e9e4f03 Data-driven changeable capsule texts 2026-03-17 02:50:30 -06:00
echolotlandHundrec 7626700495 Consistent line width + fix min/max of easing preview graph 2026-03-16 20:34:42 -06:00
AbnormalPoofandHundrec 0d2a663346 Enable null-safety on FunkinFilterRenderer 2026-03-16 13:08:36 -06:00
TechnikTilandHundrec 22736b27da Add filters to FunkinSprite
This commit also removes `FlxFilteredSprite`

Signed-off-by: TechnikTil <techniktil@tilnotdrip.org>
2026-03-16 13:08:36 -06:00
MAJigsaw77andHundrec dcfad7b7e8 Update libs ref 2026-03-16 13:08:35 -06:00
Hundrec ef625b3d9d Bump assets for a few minor fixes 2026-03-16 04:40:11 -06:00
MAJigsaw77andHundrec 6ce3a2e93e Update hxvlc to fix a couple of issues 2026-03-16 03:47:03 -06:00
SuperEricandHundrec b8c2854007 Adjust backingImage size 2026-03-14 23:36:29 -06:00
JVNpixelsandHundrec 04fffe8ade Commas For Story Mode Score 2026-03-14 23:14:15 -06:00
MAJigsaw77andHundrec c37862445f Update lime ref 2026-03-14 23:00:45 -06:00
VirtuGuyandHundrec 0613e9d295 Fix rimlight alt masks not working on web 2026-03-14 22:24:39 -06:00
MAJigsaw77andHundrec bed249dca0 Remove the framerate options on web 2026-03-14 22:13:54 -06:00
MAJigsaw77andHundrec 139940f9ec Update libs refs 2026-03-14 17:30:27 -06:00
Hyper_andHundrec 36e2f47577 Fix Random capsule not updating OST text 2026-03-13 21:48:13 -06:00
VirtuGuyandHundrec ce493a03c9 Fix favoriting songs breaking song text cycling 2026-03-13 21:48:13 -06:00
AnimatingLegendandHundrec aabd1aa9f7 fix: miss-animation timing in no-anim events 2026-03-13 21:25:52 -06:00
KoloandHundrec 89029220e4 optimization: remove a spammy trace 2026-03-13 21:25:52 -06:00
ImVeryBadandHundrec dca54de087 Add isBlank to StringTools for easier blank string checking. 2026-03-13 21:25:52 -06:00
KoloandHundrec 15bef89d71 Script event support for Song Events 2026-03-12 01:43:08 -06:00
6c1f3800db Fix duplicate asset paths in note styles crashing when exiting PlayState in Chart Editor.
Co-authored-by: Kolo <67389779+koloindacrib@users.noreply.github.com>
2026-03-12 01:43:08 -06:00
AbnormalPoofandHundrec 8507f799a1 Update polymod to the latest commit 2026-03-12 01:43:08 -06:00
EliteMasterEricandHundrec dba2794780 Use the font family instead of the file path 2026-03-12 01:42:47 -06:00
KoloandHundrec 48752e444c softcode freeplay capsule level labels 2026-03-12 01:42:24 -06:00
8a1d74b82c Re-enable FlxG.autoPause when doing stickers
Co-Authored-By: Hyper_ <40342021+NotHyper-474@users.noreply.github.com>
2026-03-12 01:40:26 -06:00
AnimatingLegendandHundrec 0d1db6ae9e fix(ui): prevent nametag pixelation on web builds 2026-03-12 01:39:13 -06:00
EliteMasterEricandHundrec 1f1851990f Make sure to update the schema version when adding fields, people! 0.0.x versions for new fields with no breaking changes. 2026-03-12 01:36:31 -06:00
Hyper_andHundrec 67d2a9811b feat: Custom OST names 2026-03-12 01:35:51 -06:00
LasercarandHundrec a782b3f9e8 Improved context menus Directly set the item's position and for hold notes, it's length as well Values can be shown and set in milliseconds or steptime 2026-03-12 01:34:08 -06:00
EricandHundrec 74e28a18ff Add an extra space 2026-03-12 01:31:20 -06:00
MoonDroidandHundrec 220636e01e Style leftover INFO logs with Ansi 2026-03-12 01:31:02 -06:00
MightyTheArmiddiloandHundrec 4f316003af FunkinGroup 2026-03-12 01:28:15 -06:00
AbnormalPoofandHundrec 1a04e35db7 Change instances of FlxG.onMobile to use preprocessor defines 2026-03-11 02:44:30 -06:00
MAJigsaw77andHundrec d3e83dd72d Add Vungle mediation for Admob 2026-03-11 02:34:04 -06:00
c4e2b900c1 Implement SDL3
Co-Authored-By: Abnormal <86753001+AbnormalPoof@users.noreply.github.com>
Co-Authored-By: Karim Akra <144803230+KarimAkra@users.noreply.github.com>
2026-03-10 04:54:26 -05:00
AbnormalPoof ac3178118c Remove FlxColorUtil as Polymod supports FlxColor 2026-03-10 04:37:12 -05:00
AbnormalPoof 681f7a6d07 Remove AbstractUtil since Polymod now supports FlxSignal proper 2026-03-10 04:37:00 -05:00
AbnormalPoof aa24c94782 Remove import aliases for abstracts 2026-03-10 04:36:58 -05:00
ComedyLostandAbnormalPoof 7075c8af4b Fix a crash for inst only songs 2026-03-10 04:33:14 -05:00
AbnormalPoof efc33403b6 fix: Fix REDIRECT_ASSETS_FOLDER not working on HashLink on macOS 2026-03-10 04:33:07 -05:00
Hundrec 25a42b8182 Reword Stage Editor unsaved work prompt 2026-03-09 20:16:07 -06:00
KoloandHundrec 0389af2202 fix moving globally offset characters not working 2026-03-09 20:16:07 -06:00
Hyper_andHundrec b694ec08d6 fix: Improve Preloader time on HashLink 2026-03-09 20:16:07 -06:00
PurSnakeandHundrec 423ccfab98 Implement ScriptedBaseFreeplayDJ usage 2026-03-09 09:38:58 -06:00
AbnormalPoofandHundrec edc1f7113b Update polymod to the latest commits 2026-03-09 09:38:58 -06:00
ImVeryBadandHundrec f0b50de9fa Add support for the new Polymod mod ID system. 2026-03-09 09:38:58 -06:00
LasercarandHundrec 0ad2d507ef Random cap difficulty & BPM value visible fix 2026-03-09 09:16:39 -06:00
LasercarandHundrec 6c13444211 Hide character select keybinds until unlock 2026-03-09 09:06:58 -06:00
MightyTheArmiddiloandHundrec 5a0b4310a2 Add onPause dispatching for cutscenes and conversations 2026-03-09 09:06:58 -06:00
LasercarandHundrec 6b1fc0372b Disable edit buttons if no selection 2026-03-09 08:41:21 -06:00
ImVeryBadandHundrec cf69125d25 Clean up Cursor.hx code 2026-03-09 08:34:15 -06:00
Redar13andHundrec 84b2d5b48d Fix GF bopping when switching levels in the story menu. 2026-03-09 08:28:18 -06:00
RudyrueandHundrec dcb5b3a935 fix timing windows being 1 ms tighter 2026-03-09 08:11:45 -06:00
TrofemandHundrec 011f1541b1 control + character icon combination for character preview 2026-03-09 07:50:49 -06:00
sector-aandHundrec 13c1bf8ac0 Fix null object reference when using collapsible 2026-03-09 07:50:49 -06:00
1893f2b2f6 Implement SetTargetBopSpeed Event
Co-Authored-By: Eric <4635334+EliteMasterEric@users.noreply.github.com>
2026-03-09 07:25:40 -06:00
VirtuGuyandHundrec a0caa41edc Fix phillyStreets rain not fading out 2026-03-09 06:55:23 -06:00
FuroandHundrec 456b2ff6c4 Prevent Importing non osu!mania beatmaps 2026-03-09 03:32:32 -06:00
AbnormalPoofandHundrec a1d1b1b2d1 Fix garbage collection being mostly broken on HashLink 2026-03-09 03:32:32 -06:00
Hyper_andHundrec 0db4ce11b7 fix: Make types explicit to fix crash on HashLink 2026-03-09 02:51:06 -06:00
340fd5dcf1 Add proper HashLink support
Co-authored-by: Sinco <113909588+sphis-Sinco@users.noreply.github.com>
2026-03-09 02:51:06 -06:00
EliteMasterEricandHundrec d569690a51 I think I was tired when I made that last commit lmao... 2026-03-09 02:51:06 -06:00
EliteMasterEricandHundrec 4a5afa2fc7 Oops there was a compile error 2026-03-09 02:51:06 -06:00
EliteMasterEricandHundrec 59c593b812 Adjust scroll speed tween duration based on playback rate 2026-03-09 02:51:06 -06:00
EliteMasterEricandHundrec 6ba9e00a9f Adjust camera zoom tween duration based on playback rate 2026-03-09 02:51:06 -06:00
EliteMasterEricandHundrec ec7bdd32e2 Adjust camera follow tween duration based on playback rate 2026-03-09 02:51:06 -06:00
AnimatingLegendandHundrec 939528a9bd refactor: remove discord_rpc from background process
DiscordRPC is being handled somewhere else. having these managers running twice, both for the same reasons its kind of pointless.
2026-03-09 02:51:06 -06:00
LasercarandHundrec 08b3af959d Chart editor - select above/below playhead 2026-03-06 05:17:14 -07:00
FuroandHundrec cea54e394c Legacy Voices System 2026-03-06 03:45:04 -07:00
Honton129andHundrec e710198afe Edit Note option in Context Menu
Shortcut to the  ``Notes`` toolbox by right-clicking a note and selecting ``Edit Note``.
2026-03-06 03:31:27 -07:00
Hyper_andHundrec bf3601ea8d fix: Properly trace Ansi code for exit trace 2026-03-06 03:31:24 -07:00
Hundrec c5566c8644 Exclude Week 7 bricks from ASTC 2026-03-04 23:21:51 -07:00
MAJigsaw77andHundrec 5944f5b55d Add Pangle mediation for Admob 2026-03-04 23:10:39 -07:00
RaltyandHundrec 077fbf6978 Fix sound tray playing multiple sounds at once, not appearing in lags 2026-03-04 23:10:39 -07:00
Hundrec f02bdc4225 Bump assets 2026-03-04 22:39:17 -07:00
EliteMasterEricandHundrec 8da9ed4ea0 Make sure only one stretchy sound plays at a time. 2026-03-04 22:39:17 -07:00
EliteMasterEricandHundrec 5ed4a2ddd0 Flag instrumental and vocal tracks as important to prevent them getting overriden by other sounds. 2026-03-04 22:39:17 -07:00
EliteMasterEricandHundrec 6293e2808b Reuse the sound objects for the stretchy sounds, to prevent too many of them playing at once. 2026-03-04 22:39:15 -07:00
Hundrec c6dc562769 Adjust some wording in the Backup window 2026-03-04 20:45:54 -07:00
marcoandHundrec a607396d04 Rename button text from 'Take Me There' to 'Open In Folder'
makes it clearer about what it does
2026-03-04 20:45:51 -07:00
ImVeryBadandHundrec 365f8dcfea Filter out unavailable difficulties from Story Mode levels. 2026-03-04 20:45:51 -07:00
FuroandHundrec b3eb709f4e Pixel Icon now changes the character value only if its valid 2026-03-04 15:25:30 -07:00
FuroandHundrec fcbce0a66b Fix Rank Slamming Animation 2026-03-04 15:25:21 -07:00
ComedyLostandHundrec 05032e3126 Play danceLeft anim for alternating characters 2026-03-04 15:25:16 -07:00
Hundrec b8a7189e24 Don't do Sys.exit force-kill on Mobile 2026-03-04 02:43:32 -07:00
AnimatingLegendandHundrec 881aba3b03 fix: prevent background processing upon game exit 2026-03-04 01:38:52 -07:00
Hundrec 5480774afe Fix Polymod for Mobile and HTML5 compiling 2026-03-03 17:21:59 -07:00
VirtuGuyandHundrec d62c8af20c Fix hold note length when hitting too early 2026-03-03 16:40:54 -07:00
MoonDroidandHundrec 7d0644f3c8 Add missing feature flag 2026-03-03 15:03:11 -07:00
Hyper_andHundrec fe21c0addd docs: Debug Hotkeys - Remove duplicate entry, fix error and add Mirrored Notes debug key 2026-03-02 20:16:03 -07:00
Hyper_andHundrec 219143c02d fix non existant paths not being sanitized properly on Linux 2026-03-02 20:16:03 -07:00
MightyTheArmiddiloandHundrec 03930a29c3 Remove duplicate blacklisting of the funkin.api.newgrounds package 2026-03-02 20:16:03 -07:00
ImVeryBadandHundrec 5d378cd9f2 Remove more traces from classes. 2026-03-02 20:16:03 -07:00
HundrecandKade e5db402025 Bump Polymod and adjust outdated mod error message
Co-Authored-By: Kade <26305836+Kade-github@users.noreply.github.com>
2026-03-02 20:16:03 -07:00
ImVeryBadandHundrec d348ef3127 Optional dependencies no longer throw window errors and improve log output messages. 2026-03-02 20:16:03 -07:00
Hundrec 9301c0c7dd Remove Song Preview 0 end times 2026-03-02 18:46:33 -07:00
ADA-FunniandHundrec 02ee8e64c8 Fix an issue where metadata stuff was acting weird. 2026-03-02 18:45:28 -07:00
ADA-FunniandHundrec 40eab2e461 Add customisable Freeplay song previews.
Any preview values over 15,000 get turned into 0 (start) and 0.2 (end).
2026-03-02 18:45:28 -07:00
AbnormalPoofandHundrec d6a65ca681 Tweak some animation lengths for Boyfriend DJ 2026-03-02 18:45:15 -07:00
EliteMasterEricandHundrec f8841bab35 Use a float internally to hold the Score, ensuring hold notes get scored correctly relative to the current framerate. 2026-03-02 01:38:30 -07:00
EliteMasterEricandAbnormalPoof dc23a116dd Update the PolymodErrorHandler to display an error message for API version or dependency issues. 2026-03-02 01:50:09 -06:00
AbnormalPoofandHundrec 024994e85a Re-export Boyfriend DJ using the latest BTA 2026-03-01 22:30:40 -07:00
MAJigsaw77andHundrec 9be1633320 Update extension-admob. 2026-03-01 04:21:54 -07:00
AnimatingLegendandHundrec 3b15873fc7 fix: chart editor playback-speed rounding upon reload 2026-02-28 00:05:34 -07:00
ComedyLostandHundrec 786f19c57d Stop playtest results with botplay 2026-02-28 00:05:34 -07:00
ComedyLostandHundrec ccb874e97a Stop the playbar head dragging when in song 2026-02-28 00:05:34 -07:00
VirtuGuyandHundrec f7fd7a57c3 Play SFX When Undoing Add Event 2026-02-28 00:05:33 -07:00
Hundrec 23f79aef58 Update assets submodule 2026-02-27 21:28:49 -07:00
EliteMasterEricandHundrec 2c394cc636 Fix an issue where displaying Tooltips would mess up HaxeUI field focus. 2026-02-27 21:26:11 -07:00
Hundrec d7a0139e4f Adjust focus lost volume level to 25%
Reducing to 50% wasn't noticeable enough, but 25% should be a clean reduction.
2026-02-27 21:25:57 -07:00
Karim AkraandHundrec 562d23005d Properly center the sound tray when the resolution is changed 2026-02-27 21:25:57 -07:00
EliteMasterEricandHundrec 671c152463 Fix a bug when the HUD camera zoom would be disabled when the MAIN camera zoom (different camera!) was above 1.35x. 2026-02-26 20:21:16 -07:00
EliteMasterEricandHundrec 3ce7c7939f Updated tooltips to use the note kind description 2026-02-25 00:52:47 -07:00
EliteMasterEricandHundrec 28450ccfc6 Build the list of note kinds from the NoteKindManager instead of hardcoding it 2026-02-25 00:52:45 -07:00
Hundrec 31dbd68194 Bump version to 0.8.3
Build and Upload nightly game builds / gather-changes (push) Canceled after 0s
Build and Upload nightly game builds / gather-tags (push) Canceled after 0s
Build and Upload nightly game builds / build-game-on-host (macos, macos) (push) Canceled after 0s
Build and Upload nightly game builds / build-game-on-host (windows, windows) (push) Canceled after 0s
Build and Upload nightly game builds / docker-image (push) Canceled after 0s
Build and Upload nightly game builds / build-game-in-container (html5) (push) Canceled after 0s
Build and Upload nightly game builds / build-game-in-container (linux) (push) Canceled after 0s
2026-02-23 17:11:08 -07:00
Hundrec 29d776061c Changelog for 0.8.3 + adjustments 2026-02-23 17:10:35 -07:00
Hundrec 0805ec914e Bump docs to 0.8.3 2026-02-23 16:47:57 -07:00
Hundrec 98ac6a6ce1 Bump Polymod to fix 2hot script error on Web 2026-02-23 16:04:47 -07:00
ACrazyTownandHundrec 823120cdef Use FlxBitmapText for scoreText 2026-02-23 16:04:29 -07:00
159 changed files with 4424 additions and 2081 deletions
+3 -3
View File
@@ -48,19 +48,19 @@ body:
attributes:
label: Mobile Device Model
description: (Mobile users only) What mobile device are you playing on?
placeholder: ex. iPhone 17, Galaxy S25, iPad 11th Gen
placeholder: ex. iPhone 17, Galaxy S26, iPad 11th Gen
- type: input
attributes:
label: Mobile OS Version
description: (Mobile users only) What version is your Operating System?
placeholder: ex. iOS 26.2, Android 16, iPadOS 26.2
placeholder: ex. iOS 26.4, Android 16, iPadOS 26.4
- type: input
attributes:
label: Version
description: Which version are you playing on? The game version is in the bottom left corner of the main menu.
placeholder: ex. 0.8.1
placeholder: ex. 0.8.4
validations:
required: true
+2 -2
View File
@@ -36,7 +36,7 @@ body:
attributes:
label: Version
description: Which version are you playing on? The game version is in the bottom left corner of the main menu.
placeholder: ex. 0.8.1
placeholder: ex. 0.8.4
validations:
required: true
@@ -52,7 +52,7 @@ body:
attributes:
label: Location
description: Where did you find the issue(s)? Include the name of the song, the variation, the difficulty, the time/section of the song, and any images or videos.
placeholder: ex. Cocoa Erect on Erect/Nightmare difficulties at Section 30
placeholder: ex. Cocoa Erect [Nightmare] at Section 30, step 5
validations:
required: true
+1 -1
View File
@@ -36,7 +36,7 @@ body:
attributes:
label: Version
description: Which version are you compiling? The game version is in the bottom left corner of the main menu or in the project.hxp file.
placeholder: ex. 0.8.1
placeholder: ex. 0.8.4
validations:
required: true
+3 -3
View File
@@ -49,19 +49,19 @@ body:
attributes:
label: Mobile Device Model
description: (Mobile users only) What mobile device are you playing on?
placeholder: ex. iPhone 17, Galaxy S25, iPad 11th Gen
placeholder: ex. iPhone 17, Galaxy S26, iPad 11th Gen
- type: input
attributes:
label: Mobile OS Version
description: (Mobile users only) What version is your Operating System?
placeholder: ex. iOS 26.2, Android 16, iPadOS 26.2
placeholder: ex. iOS 26.4, Android 16, iPadOS 26.4
- type: input
attributes:
label: Version
description: Which version are you playing on? The game version is in the bottom left corner of the main menu.
placeholder: ex. 0.8.1
placeholder: ex. 0.8.4
validations:
required: true
+1 -2
View File
@@ -72,7 +72,6 @@
"haxecheckstyle.externalSourceRoots": [],
"haxecheckstyle.configurationFile": "checkstyle.json",
"haxecheckstyle.codeSimilarityBufferSize": 100,
"lime.projectFile": "project.hxp",
"lime.targets": [
{ "name": "windows", "enabled": true, "label": "Windows" },
{ "name": "mac", "enabled": true, "label": "macOS" },
@@ -80,8 +79,8 @@
{ "name": "html5", "enabled": true, "label": "HTML5" },
{ "name": "android", "enabled": true, "label": "Android" },
{ "name": "ios", "enabled": true, "label": "iOS" },
{ "name": "hl", "enabled": true, "label": "HashLink" },
// Disabled targets
{ "name": "hl", "enabled": false, "label": "HashLink" },
{ "name": "air", "enabled": false },
{ "name": "electron", "enabled": false },
{ "name": "flash", "enabled": false },
+136 -2
View File
@@ -4,6 +4,141 @@ 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.8.4] - 2026-03-26
### Added
- Added several new features to Polymod:
- Added FULL support for abstracts! (Previously, only `FlxColor` was supported)
- Added support for getting the code of a single character string (i.e `’h’.code`) (Thanks NotHyper-474!)
- Added support for import defaults (`import.hx`/`import.hxc`), allowing scripted classes with the same package to share imports. (Thanks KoloInDaCrib!)
- Added `scriptInit` and `listScriptClasses` functions to scripted classes. (Thanks KoloInDaCrib!)
- `.hx` files can now be parsed. (Thanks Starexify!)
- Added support for preprocessing (`#if`/`#else` syntax), allowing for platform-specific code to be implemented in an easier way. (Thanks KoloInDaCrib!)
- Added support for mod IDs. (Thanks Starexify!)
- Implemented `_merge` support for scripts. (Thanks KoloInDaCrib!)
- Please refer to the Cookbook to learn how to use this.
- Added widescreen scale properties to the Zoom Camera event in the Chart Editor to account for widescreen resolutions.
- Added a flipY field for Story Mode level props. ([48ed659](https://github.com/FunkinCrew/Funkin/commit/48ed65980005366371fda7a66c04f73bb5b86200)) - by @ExtraCode75 in [#6340](https://github.com/FunkinCrew/Funkin/pull/6340)
- Implemented working start and end times for Freeplay song previews. ([40eab2e](https://github.com/FunkinCrew/Funkin/commit/40eab2e4618ebb6dc67a06ef67df817920773c56)) - by @ADA-Funni in [#6725](https://github.com/FunkinCrew/Funkin/pull/6725)
- Added a new albumOSTName property to Freeplay album data to make the "OFFICIAL OST" text moddable. ([67d2a98](https://github.com/FunkinCrew/Funkin/commit/67d2a9811b5c92409bdc76e287c984d5777d8621)) - by @NotHyper-474 in [#6578](https://github.com/FunkinCrew/Funkin/pull/6578)
- Added an optional 'capsule' field to level data to display any week label on Freeplay capsules. ([528e9e4](https://github.com/FunkinCrew/Funkin/commit/528e9e4f03c40e59f3932d8b4c522b6eeb66783b)) - by @Trayfellow in [#7149](https://github.com/FunkinCrew/Funkin/pull/7149)
- Implemented ScriptedBaseFreeplayDJ, allowing modders to customize how custom Freeplay DJs are rendered. ([423ccfa](https://github.com/FunkinCrew/Funkin/commit/423ccfab98a29646e4998dacedaf403319d956ff)) - by @PurSnake in [#6806](https://github.com/FunkinCrew/Funkin/pull/6806)
- Added a new FunkinGroup class to make groups of sprites easier to modify. ([4f31600](https://github.com/FunkinCrew/Funkin/commit/4f316003af9a6ca46c77bf4971059e20543cbd7b)) - by @MightyTheArmiddilo in [#6758](https://github.com/FunkinCrew/Funkin/pull/6758)
- Added `Reflect.makeVarArgs` to allow modders to create functions with a variable number of arguments. ([3633df3](https://github.com/FunkinCrew/Funkin/commit/3633df3fdc6dc0af4f9886fcb1e1d4a8d2b8d5aa)) - by @Starexify in [#6738](https://github.com/FunkinCrew/Funkin/pull/6738)
- Added script event support for song events. ([15bef89](https://github.com/FunkinCrew/Funkin/commit/15bef89d71982c5685f4ce6f6d7e6e4d8321508c)) - by @KoloInDaCrib in [#4130](https://github.com/FunkinCrew/Funkin/pull/4130)
- Added a new Chart Editor event type: Set Target Bop Speed. ([1893f2b](https://github.com/FunkinCrew/Funkin/commit/1893f2b2f679d8a4d262052793dde24e94719ee6)) - by @roma-perec-bp in [#6769](https://github.com/FunkinCrew/Funkin/pull/6769)
- Added shortcuts to select all notes above or below the playhead in the Chart Editor. ([08b3af9](https://github.com/FunkinCrew/Funkin/commit/08b3af959d6657511bfe8418da545731050cb24a)) - by @Lasercar in [#4306](https://github.com/FunkinCrew/Funkin/pull/4306)
- Added the ability to view and edit the timing of a note or event through the Chart Editor context menu. ([a782b3f](https://github.com/FunkinCrew/Funkin/commit/a782b3f9e8498634babb864341fd1437bb2c01f6)) - by @Lasercar in [#4200](https://github.com/FunkinCrew/Funkin/pull/4200)
- Added an "Edit Note" option to the Chart Editor context menu that opens the Notes window. ([e710198](https://github.com/FunkinCrew/Funkin/commit/e710198afec9a0fe19effe4cd196ab3a247f4784)) - by @Honton129 in [#7010](https://github.com/FunkinCrew/Funkin/pull/7010)
- Added the ability to Ctrl-click on health icons in the Chart Editor to open character previews. ([011f154](https://github.com/FunkinCrew/Funkin/commit/011f1541b197fd4f962ab8a910cbd417f78c2d09)) - by @Trofem in [#7089](https://github.com/FunkinCrew/Funkin/pull/7089)
### Changed
- Upgraded the input handling system from SDL2 to SDL3!
- Added an “Unlocked Framerate” preference to Desktop platforms.
- The input system is now much more responsive and accurate down to the nanosecond.
- The game now properly supports more controllers and input devices.
- The game now shows popups when installed mods have issues, such as when the API version is incorrect.
- Unfocusing the game now reduces the game’s volume to 25% (down from 50%).
- The notekinds dropdown are no longer hardcoded in the Chart Editor.
- The Character Select key is now hidden in the Controls menu until a new character is unlocked. ([6c13444](https://github.com/FunkinCrew/Funkin/commit/6c134442118d3f5f61b9e3faa631512d210422fb)) - by @Lasercar in [#4882](https://github.com/FunkinCrew/Funkin/pull/4882)
- The Story Mode level score now displays with commas. ([04fffe8](https://github.com/FunkinCrew/Funkin/commit/04fffe8ade4c4b236d59a236f08534e8eede179f)) - by @JVNpixels in [#4965](https://github.com/FunkinCrew/Funkin/pull/4965)
- Freeplay capsule week labels are now softcoded and rendered using a font. ([48752e4](https://github.com/FunkinCrew/Funkin/commit/48752e444cc5f1b8cd0a3b579f843e9ab6580c46)) - by @KoloInDaCrib in [#5249](https://github.com/FunkinCrew/Funkin/pull/5249)
- The Freeplay change character hint now changes when a gamepad is connected. ([0f7859b](https://github.com/FunkinCrew/Funkin/commit/0f7859bfb6b829290804efab9d5c67fbdbec0903)) - by @MrMadera in [#6609](https://github.com/FunkinCrew/Funkin/pull/6609)
- Moved the Winter Horrorland intro cutscene logic into a script. ([9bbe73b](https://github.com/FunkinCrew/funkin.assets/commit/9bbe73b21ccbea29a5d5187709f91b4822bf7e31)) - by @mario64thane in [funkin.assets#63](https://github.com/FunkinCrew/funkin.assets/pull/63)
- Merged FlxFilteredSprite functionality into FunkinSprite. ([22736b2](https://github.com/FunkinCrew/Funkin/commit/22736b27da7d8de8b11ab6cc6c2a622e5579dccd)) - by @TechnikTil in [#7114](https://github.com/FunkinCrew/Funkin/pull/7114)
- Adjusted some wording in the Chart/Stage Editor backup window to be clearer. ([a607396](https://github.com/FunkinCrew/Funkin/commit/a607396d04f24388691ee23bd04022681ca54756)) - by @Marcosayfi in [#7071](https://github.com/FunkinCrew/Funkin/pull/7071)
### Fixed
- Conjoined string interpolation `$a$b` now parses properly in Polymod.
- Video cutscenes are no longer muted at volumes of 3 or lower.
- Video cutscenes no longer occasionally show glitches at the beginning.
- [MOBILE] Loading a cloud save from Newgrounds no longer carries over the volume level from Desktop.
- [HTML5] Scrolling the game out of view now pauses the game instead of allowing gameplay to continue processing.
- The Boyfriend Freeplay DJ new rank animation now transitions smoothly back to its idle animation.
- Hold notes now score much more consistently across different framerate values.
- The bricks in Week 7 stages are now better quality.
- Trimmed out the silent beat at the beginning of Ugh Erect.
- Camera bopping no longer stops at zoom values of 1.35x or higher.
- Charting many hold notes in a row no longer breaks audio handling.
- Adding collapsible frames in event schemas no longer crashes the game.
- Some event properties have been grouped under Advanced frames.
- Chart Editor dropdowns no longer refocus when a tooltip appears.
- Focus and Zoom Camera event durations now account for playback speed.
- [WINDOWS] Exiting the game no longer leaves it running as a background process. ([881aba3](https://github.com/FunkinCrew/Funkin/commit/881aba3b03a26764374441cab244cd2d360a30b9)) - by @AnimatingLegend in [#6885](https://github.com/FunkinCrew/Funkin/pull/6885)
- Story Mode now only shows difficulties that all of a level's songs have. ([365f8dc](https://github.com/FunkinCrew/Funkin/commit/365f8dcfea9569f2edba1ebfceb3d49819ad2b57)) - by @Starexify in [#7046](https://github.com/FunkinCrew/Funkin/pull/7046)
- The Girlfriend Story Mode prop now bops continuously when scrolling through levels. ([84b2d5b](https://github.com/FunkinCrew/Funkin/commit/84b2d5b48d9eb76c918fdba6987ccc1f5da76133)) - by @Redar13 in [#6830](https://github.com/FunkinCrew/Funkin/pull/6830)
- The Freeplay new rank slam animation now properly plays its glow effect. ([fcbce0a](https://github.com/FunkinCrew/Funkin/commit/fcbce0a66b75b0c1224c6a0e2907c170b71998ad)) - by @FuroYT in [#7015](https://github.com/FunkinCrew/Funkin/pull/7015)
- The Freeplay Random capsule no longer displays BPM and difficulty numbers. ([0ad2d50](https://github.com/FunkinCrew/Funkin/commit/0ad2d507efa9dcb577184e28bfd04091b3424bfb)) - by @Lasercar in [#4927](https://github.com/FunkinCrew/Funkin/pull/4927)
- Favoriting a song in Freeplay no longer breaks capsule text cycling. ([ce493a0](https://github.com/FunkinCrew/Funkin/commit/ce493a03c9ffb4c43c2c27885ed0fab0c1c3531d)) - by @VirtuGuy in [#7134](https://github.com/FunkinCrew/Funkin/pull/7134)
- Freeplay songs with invalid pixel icons no longer duplicate the icon from the previous song in the list. ([b3eb709](https://github.com/FunkinCrew/Funkin/commit/b3eb709f4ed535ed2544f3bb530ff3724e07be3f)) - by @FuroYT in [#7006](https://github.com/FunkinCrew/Funkin/pull/7006)
- A Freeplay clear percent of 100% now properly exits to the right when returning to the Main Menu. ([5974c1a](https://github.com/FunkinCrew/Funkin/commit/5974c1a48293f3185c5f249acb9130ebd6b35ae4)) - by @Trayfellow in [#7183](https://github.com/FunkinCrew/Funkin/pull/7183)
- Nudged the Freeplay backing image down by one pixel to align with the bottom of the screen. ([b8c2854](https://github.com/FunkinCrew/Funkin/commit/b8c2854007f36ac7a2b6897eead3f979e11f0ad7)) - by @SuperEric1 in [#6893](https://github.com/FunkinCrew/Funkin/pull/6893)
- Improved performance for songs that use a lot of events. ([3b27ec5](https://github.com/FunkinCrew/Funkin/commit/3b27ec5b66b2fa00d59a77f6ec07aff68e440860)) - by @KoloInDaCrib in [#7171](https://github.com/FunkinCrew/Funkin/pull/7171)
- Adjusted hit window logic to accurately reflect threshold values for all judgments. ([dcb5b3a](https://github.com/FunkinCrew/Funkin/commit/dcb5b3a9358a9bb126153b57aa2d38dc0684c39d)) - by @Rudyrue in [#7104](https://github.com/FunkinCrew/Funkin/pull/7104)
- Adjusted offsets for all health icons to be more consistent. ([83d8134](https://github.com/FunkinCrew/funkin.assets/commit/83d8134f9d0b3cf3b59e9e1d972fa1e200a98ba1)) - by @LeGooeyy in [funkin.assets#278](https://github.com/FunkinCrew/funkin.assets/pull/278)
- Hold notes no longer shift upward when hit early. ([d62c8af](https://github.com/FunkinCrew/Funkin/commit/d62c8af20c90ec3d6e4274aba8ddbfb378ab0256)) - by @VirtuGuy in [#7060](https://github.com/FunkinCrew/Funkin/pull/7060)
- The opponent no longer stops singing upon missing if Boyfriend has no vocals file. ([cea54e3](https://github.com/FunkinCrew/Funkin/commit/cea54e394c78544dd0ec0339721faaac140caa65)) - by @FuroYT in [#7007](https://github.com/FunkinCrew/Funkin/pull/7007)
- onPause script functions now properly trigger when pausing during a cutscene or dialogue. ([5a0b431](https://github.com/FunkinCrew/Funkin/commit/5a0b4310a2c39b033989e0e36b8f456bdac65a5b)) - by @MightyTheArmiddilo in [#6804](https://github.com/FunkinCrew/Funkin/pull/6804)
- Camera events now consistently pause when the Pause Menu is opened. ([e623439](https://github.com/FunkinCrew/Funkin/commit/e6234397f57d2af37035a0ff2504b8241e23949d)) - by @VirtuGuy in [#7145](https://github.com/FunkinCrew/Funkin/pull/7145)
- The Pause Menu global offset value now adjusts at the same rate across all framerates. ([348647f](https://github.com/FunkinCrew/Funkin/commit/348647f48629a24ee3bd2b759bbfc1bff168fb81)) - by @KolzeYT in [#7147](https://github.com/FunkinCrew/Funkin/pull/7147)
- Spirit's trail effect now renders consistently across all framerates. ([75506f7](https://github.com/FunkinCrew/Funkin/commit/75506f71a97d468f516a0232c8be5560afb0682e)) - by @KolzeYT in [#7179](https://github.com/FunkinCrew/Funkin/pull/7179)
- The sound tray now appears consistently and plays sound effects at the right volume. ([2b9906d](https://github.com/FunkinCrew/Funkin/commit/2b9906df29660057410d3054e897012896896d6a)) - by @Raltyro in [#7068](https://github.com/FunkinCrew/Funkin/pull/7068)
- The sticker transition now properly pauses when losing focus with Pause on Unfocus enabled. ([8a1d74b](https://github.com/FunkinCrew/Funkin/commit/8a1d74b82cceab97e777dc49e7a2117b8d27e59b)) - by @ComedyLost in [#7129](https://github.com/FunkinCrew/Funkin/pull/7129)
- [HTML5] Rimlight alt masks now render properly. ([0613e9d](https://github.com/FunkinCrew/Funkin/commit/0613e9d295179e66d30db3d2f6805870eac847d6)) - by @VirtuGuy in [#6900](https://github.com/FunkinCrew/Funkin/pull/6900)
- The Week 2 Erect stage lightning strike fade now pauses and cancels appropriately. ([cd05267](https://github.com/FunkinCrew/funkin.assets/commit/cd05267023859d04cc8920c787b8280682e5f0f5)) - by @ComedyLost in [funkin.assets#390](https://github.com/FunkinCrew/funkin.assets/pull/390)
- A-Bot (Dark)'s frame no longer renders as completely dark in Week 2 Erect. ([a142530](https://github.com/FunkinCrew/funkin.assets/commit/a142530cae6198ca17b560ff73e36d50f1582768)) - by @ComedyLost in [funkin.assets#368](https://github.com/FunkinCrew/funkin.assets/pull/368)
- Dying on the same frame as a lightning strike in Week 2 Erect no longer throws a script error. ([ef89bde](https://github.com/FunkinCrew/funkin.assets/commit/ef89bde19c397f15a53b0fda2f64ba93ff96f56f)) - by @KoloInDaCrib in [funkin.assets#363](https://github.com/FunkinCrew/funkin.assets/pull/363)
- Pico (Dark) and Nene (Dark) no longer throw script errors in debug editors. ([b16ce44](https://github.com/FunkinCrew/funkin.assets/commit/b16ce44beb43d4511c61418c824929fb4dfabb63)) - by @ComedyLost in [funkin.assets#367](https://github.com/FunkinCrew/funkin.assets/pull/367)
- Girlfriend's hair no longer falls when restarting the song in a Week 3 stage. ([ade80ab](https://github.com/FunkinCrew/funkin.assets/commit/ade80ab72915ddd47724c380bb41d6df4c1b79bd)) - by @AnimatingLegend in [funkin.assets#370](https://github.com/FunkinCrew/funkin.assets/pull/370)
- The camera transition to Results no longer reveals the edges of stage art in the Week 6 Erect stage. ([ecf4c54](https://github.com/FunkinCrew/funkin.assets/commit/ecf4c5458a2e1fadba7db31c3272cea485d22b2f)) - by @ComedyLost in [funkin.assets#383](https://github.com/FunkinCrew/funkin.assets/pull/383)
- Fine-tuned offsets for pixel characters. ([8b3c268](https://github.com/FunkinCrew/funkin.assets/commit/8b3c2680a6a966e81b62598e783c60f46355c759)) - by @CEliuxJV in [funkin.assets#376](https://github.com/FunkinCrew/funkin.assets/pull/376)
- Tankman's game over lines now properly play during game overs in Ugh (Pico Mix) and Guns (Pico Mix). ([b900208](https://github.com/FunkinCrew/funkin.assets/commit/b900208143f980adee25d70db247e0b176dd659c)) - by @hucks5 in [funkin.assets#373](https://github.com/FunkinCrew/funkin.assets/pull/373)
- Restarting during the mid-song animation in Stress (Pico Mix) now properly resets the characters' animations. ([2ca92de](https://github.com/FunkinCrew/funkin.assets/commit/2ca92de46eced3f53d305579704fcb7e8c051f3a)) - by @requazar in [funkin.assets#364](https://github.com/FunkinCrew/funkin.assets/pull/364)
- The rain shader now properly fades out with the transition to the Results Screen. ([a0caa41](https://github.com/FunkinCrew/Funkin/commit/a0caa41edcb26298e4f8c243a5eed0b4fe93fcf7)) - by @VirtuGuy in [#6776](https://github.com/FunkinCrew/Funkin/pull/6776)
- Exiting a Chart Editor playtest while using the pixel notestyle no longer crashes the game. ([6c1f380](https://github.com/FunkinCrew/Funkin/commit/6c1f3800dbc9b13fb4b409abce53c1dafc9bbe8f)) - by @Starexify and @KoloInDaCrib in [#7108](https://github.com/FunkinCrew/Funkin/pull/7108)
- Chart Editor Edit buttons are now appropriately disabled when nothing is selected. ([6b1fc03](https://github.com/FunkinCrew/Funkin/commit/6b1fc0372bca202d80f4ccf316580b13a5a80879)) - by @Lasercar in [#4386](https://github.com/FunkinCrew/Funkin/pull/4386)
- Undoing a Chart Editor event now plays the undo sound. ([f7fd7a5](https://github.com/FunkinCrew/Funkin/commit/f7fd7a57c36f0461f56c2ab627a17d752cb82320)) - by @VirtuGuy in [#6963](https://github.com/FunkinCrew/Funkin/pull/6963)
- The playback speed audio setting no longer rounds incorrectly upon reopening the Chart Editor. ([3b15873](https://github.com/FunkinCrew/Funkin/commit/3b15873fc7d0c20840c313f1d2cfed91014c664f)) - by @AnimatingLegend in [#7023](https://github.com/FunkinCrew/Funkin/pull/7023)
- The Chart Editor easing preview graph now properly displays tweens that exceed start/end values. ([7626700](https://github.com/FunkinCrew/Funkin/commit/7626700495743862caddbf50a7dafef51529a2a6)) - by @echolotl in [#7152](https://github.com/FunkinCrew/Funkin/pull/7152)
- The Chart Editor playbar head is no longer draggable during a playtest. ([ccb874e](https://github.com/FunkinCrew/Funkin/commit/ccb874e97a121859c26a8a71b394743d27f6ed37)) - by @ComedyLost in [#6987](https://github.com/FunkinCrew/Funkin/pull/6987)
- The Results Screen no longer shows up after a Chart Editor playtest with Bot Play enabled. ([786f19c](https://github.com/FunkinCrew/Funkin/commit/786f19c57d999dba9309cae28d912b080968670d)) - by @ComedyLost in [#7022](https://github.com/FunkinCrew/Funkin/pull/7022)
- The Osu!Mania Chart Editor importer no longer accepts non-Osu!Mania beatmaps. ([456b2ff](https://github.com/FunkinCrew/Funkin/commit/456b2ff6c4a127733120cef3d03d6102bdd7522f)) - by @FuroYT in [#6624](https://github.com/FunkinCrew/Funkin/pull/6624)
- Characters in the Stage Editor no longer fly away when clicking and dragging them. ([0389af2](https://github.com/FunkinCrew/Funkin/commit/0389af2202aa4dc6a21ce9b7eea95944bfaa6946)) - by @KoloInDaCrib in [#7002](https://github.com/FunkinCrew/Funkin/pull/7002)
- Characters with alternating idle animations now properly play danceLeft when pressing Space in the Animation Editor. ([05032e3](https://github.com/FunkinCrew/Funkin/commit/05032e3126eda78a2bbf70e8c975bcb04eed201b)) - by @ComedyLost in [#7056](https://github.com/FunkinCrew/Funkin/pull/7056)
- [LINUX] Paths are now properly sanitized as they are on other platforms. ([219143c](https://github.com/FunkinCrew/Funkin/commit/219143c02dcf65e68aec80701a82023ca0d70db6)) - by @NotHyper-474 in [#6395](https://github.com/FunkinCrew/Funkin/pull/6395)
- OpenFL and some Lime classes are now accessible to modders. ([56a56db](https://github.com/FunkinCrew/Funkin/commit/56a56dbfaa3f62a3f684df49c682f69a496e45f4)) - by @FuroYT in [#7115](https://github.com/FunkinCrew/Funkin/pull/7115)
- Sandboxed API classes can now be accessed by mods. ([03930a2](https://github.com/FunkinCrew/Funkin/commit/03930a29c3d8aa6a310147a7131c6b7cddefd475)) - by @MightyTheArmiddilo in [#6952](https://github.com/FunkinCrew/Funkin/pull/6952)
- Blacklisted some more classes for security.
- And even more fixes!
## New Contributors for 0.8.4
* @roma-perec-bp made their first contribution in [#6769](https://github.com/FunkinCrew/Funkin/pull/6769)
* @Redar13 made their first contribution in [#6830](https://github.com/FunkinCrew/Funkin/pull/6830)
* @SuperEric1 made their first contribution in [#6893](https://github.com/FunkinCrew/Funkin/pull/6893)
* @AnimatingLegend made their first contribution in [#7023](https://github.com/FunkinCrew/Funkin/pull/7023)
* @Marcosayfi made their first contribution in [#7071](https://github.com/FunkinCrew/Funkin/pull/7071)
* @Rudyrue made their first contribution in [#7104](https://github.com/FunkinCrew/Funkin/pull/7104)
* @KolzeYT made their first contribution in [#7147](https://github.com/FunkinCrew/Funkin/pull/7147)
* @echolotl made their first contribution in [#7152](https://github.com/FunkinCrew/Funkin/pull/7152)
* @mario64thane made their first contribution in [funkin.assets#63](https://github.com/FunkinCrew/funkin.assets/pull/63)
* @LeGooeyy made their first contribution in [funkin.assets#278](https://github.com/FunkinCrew/funkin.assets/pull/278)
* @requazar made their first contribution in [funkin.assets#364](https://github.com/FunkinCrew/funkin.assets/pull/364)
## [0.8.3] - 2026-02-23
### Fixed
- [HTML5] Hitting a can in 2hot no longer creates a script error. (Thanks NotHyper-474!)
- The game no longer consistently lag spikes during gameplay. ([823120c](https://github.com/FunkinCrew/Funkin/commit/823120cdef7bfbb2d3ec37c7085878fcbeebcb30)) - by @ACrazyTown in [#6970](https://github.com/FunkinCrew/Funkin/pull/6970)
## [0.8.2] - 2026-02-23
### Added
@@ -22,7 +157,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Scripted classes can now extend other scripted classes. (Thanks KoloInDaCrib!)
- Tweaked some error messages to be more verbose.
- Added `scriptHas` and `staticScriptHas` functions to check if a script has a specific variable/function.
- The `is` operator is now more accurate to regular Haxe. (Thanks NotHyper-474!)
- The `is` operator is now more accurate to regular Haxe. (Thanks Starexify!)
- Static functions can now use static extensions (`using`s). (Thanks NotHyper-474!)
- `interface`s can now be properly parsed. (Thanks Starexify!)
- Optional fields and extensions can now be used in `typedef`s. (Thanks Starexify!)
@@ -88,7 +223,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [DESKTOP] The game display no longer sometimes becomes offset from the window.
- [MOBILE] Selecting a song using touch controls when a keyboard is connected no longer disables the keyboard strumline layout.
- [ANDROID] Improved Freeplay scrolling behavior on high framerate displays.
- Ctrl-clicking on a hold note no longer crashes the Chart Editor.
- [WINDOWS] Chart Editor save dialog windows no longer freeze the game.
- The Chart Editor Results Screen playtest option now consistently shows the Results Screen after each playtest.
- Scrolling really quickly in Freeplay no longer crashes the game. ([76699b2](https://github.com/FunkinCrew/Funkin/commit/76699b235cd5b359bc34e6950cd05bbbb13918f8)) - by @ComedyLost in [#6962](https://github.com/FunkinCrew/Funkin/pull/6962)
+1 -1
Submodule assets updated: 5be03b9cd8...d1d027d474
+4 -1
View File
@@ -147,6 +147,7 @@
}
],
"excludes": [
"assets/fonts/vcr-bmp.png",
"assets/preload/images/cursor/",
"assets/preload/images/fonts/",
"assets/preload/images/freeplay/",
@@ -197,6 +198,8 @@
"assets/week1/images/erect/orangeLight.png",
"assets/week2/images/erect/stairsDark.png",
"assets/week2/images/erect/stairsLight.png",
"assets/week6/"
"assets/week6/",
"assets/week7/images/bricksGround.png",
"assets/week7/images/erect/bricksGround.png"
]
}
+1 -1
View File
@@ -60,7 +60,7 @@ This section provides guidelines to follow when [opening an issue](https://githu
## Requirements
Make sure you're playing:
- the latest version of the game (currently v0.8.1)
- the latest version of the game (currently v0.8.4)
- without any mods
- on [Newgrounds](https://www.newgrounds.com/portal/view/770371) or downloaded from [itch.io](https://ninja-muffin24.itch.io/funkin)
+2 -3
View File
@@ -8,9 +8,7 @@ Most of this functionality is only available on debug builds of the game!
- `F3`: ***SCREENSHOT***: Takes a screenshot of the game and saves it to the local `screenshots` directory. Works outside of debug builds too!
- `F4`: ***EJECT***: Forcibly switch state to the Main Menu (with no extra transition). Useful if you're stuck in a level and you need to get out!
- `F5`: ***HOT RELOAD***: Forcibly reload the game's scripts and data files, then restart the current state. If any files in the `assets` folder have been modified, the game should process the changes for you! NOTE: Known bug, this does not reset song charts or song scripts, but it should reset everything else (such as stage layout data and character animation data).
- `CTRL-ALT-SHIFT-L`: ***FORCE CRASH***: Immediately crash the game with a detailed crash log and a stack trace. (Only works in the Main Menu on debug builds).
- `CTRL-SHIFT-L`: ***FORCE CRASH***: Immediately crash the game with a detailed crash log and a stack trace.
- `CTRL-ALT-SHIFT-L`: ***FORCE CRASH***: Immediately crash the game with a detailed crash log and a stack trace.
## **Play State**
- `H`: ***HIDE UI***: Makes the user interface invisible. Works in Pause Menu, great for screenshots.
@@ -27,6 +25,7 @@ Most of this functionality is only available on debug builds of the game!
- `P`: ***SWITCH TO PICO/BF***: Switch to Pico or BF (reopens Freeplay as the other character).
- `T`: ***TEST RANK ANIMATION***: Play the Gold Perfect rank animation on the currently selected song.
- `SHIFT-ACCEPT`: ***BOTPLAY MODE***: Start a song with Botplay mode enabled.
- `CTRL-ACCEPT`: ***MIRRORED MODE***: Start a song with mirrored notes enabled.
- `CTRL-Switch Song`: Play the first unlocked alternate instrumental as the song preview.
## **Title State**
+10 -10
View File
@@ -4,7 +4,7 @@
"name": "FlxPartialSound",
"type": "git",
"dir": null,
"ref": "26ed20b9c181a576f4cbe18ccdea38912c01476e",
"ref": "2f984e244f1544ca98c0e03b9b21ae570f07ac55",
"url": "https://github.com/FunkinCrew/FlxPartialSound.git"
},
{
@@ -18,7 +18,7 @@
"name": "extension-admob",
"type": "git",
"dir": null,
"ref": "4c69bb43f1fb89fd6003dc98d2f3a9d75c5364b6",
"ref": "5c908b3f7c74115a8b52eddf1e05525f4873f26e",
"url": "https://github.com/FunkinCrew/extension-admob"
},
{
@@ -52,14 +52,14 @@
"name": "flixel",
"type": "git",
"dir": null,
"ref": "c8e79d2a42f9c11c639519ef2a6fd7ef2379a8aa",
"ref": "f7b94eebf7dbb452a929d0c67ab31a9cbd71d3a0",
"url": "https://github.com/FunkinCrew/flixel"
},
{
"name": "flixel-addons",
"type": "git",
"dir": null,
"ref": "6fa30b3f5209146c852c25f3d1003e08898083e2",
"ref": "187f93b34f93c6a405d634a42913c745e443463a",
"url": "https://github.com/FunkinCrew/flixel-addons"
},
{
@@ -97,7 +97,7 @@
"name": "haxeui-core",
"type": "git",
"dir": null,
"ref": "088b153b35985f8e4419ef710057ebd5b3749513",
"ref": "09f2512564b45e92229e21433e6dd482391f7b7d",
"url": "https://github.com/FunkinCrew/haxeui-core"
},
{
@@ -111,7 +111,7 @@
"name": "hxcpp",
"type": "git",
"dir": null,
"ref": "5932340d095a7eea8635fe4d1355f1c0efd0b3c2",
"ref": "450d112e50acff57b1bc9d584dcf1374c9e33995",
"url": "https://github.com/FunkinCrew/hxcpp"
},
{
@@ -146,7 +146,7 @@
{
"name": "hxvlc",
"type": "haxelib",
"version": "2.2.5"
"version": "2.2.6"
},
{
"name": "json2object",
@@ -169,7 +169,7 @@
"name": "lime",
"type": "git",
"dir": null,
"ref": "cae0c0bc10915f6281625d07e5527f3cecb4347f",
"ref": "58dd49b018729f86600f719a98728ffb024724e3",
"url": "https://github.com/FunkinCrew/lime"
},
{
@@ -183,14 +183,14 @@
"name": "openfl",
"type": "git",
"dir": null,
"ref": "5399946b68fd98f345d6b3e52b95ed6b78bf427c",
"ref": "88534506595a32c3f02b21b3987e789a24074ae7",
"url": "https://github.com/FunkinCrew/openfl"
},
{
"name": "polymod",
"type": "git",
"dir": null,
"ref": "d8f036378c4ff472f1922ad623b8800b6a061c1a",
"ref": "fb123758b90127f5647727727f273d27e93cabdb",
"url": "https://github.com/FunkinCrew/polymod"
},
{
+26 -36
View File
@@ -32,13 +32,13 @@ class Project extends HXProject
* REMEMBER TO CHANGE THIS WHEN THE GAME UPDATES!
* You only have to change it here, the rest of the game will query this value.
*/
static final VERSION:String = "0.8.2";
static final VERSION:String = "0.8.4";
/**
* The build's number and version code.
* Used when publishing the game to mobile app stores. Should increment with each patch.
*/
static final BUILD_NUMBER:Int = 76;
static final BUILD_NUMBER:Int = 85;
/**
* The game's name. Used as the default window title.
@@ -114,11 +114,7 @@ class Project extends HXProject
*/
static final ANDROID_TARGET_SDK_VERSION:Int = 35;
static final ANDROID_EXTENSIONS:Array<String> = [
"funkin.extensions.CallbackUtil",
"funkin.extensions.FNFCExtension",
"funkin.extensions.AudioSession"
];
static final ANDROID_EXTENSIONS:Array<String> = ["funkin.extensions.CallbackUtil", "funkin.extensions.FNFCExtension", "funkin.extensions.AudioSession"];
/**
* The team ID to use for the iOS app. Configured in XCode.
@@ -171,11 +167,7 @@ class Project extends HXProject
/**
* Asset path globs to exclude on platforms with `CENSOR_EXPLETIVES` disabled.
*/
static final EXCLUDE_ASSETS_UNCENSORED:Array<String> = [
"stressCutscene-censored.mp4",
"stressPicoCutscene-censored.mp4",
"darnellCutscene-censored.mp4"
];
static final EXCLUDE_ASSETS_UNCENSORED:Array<String> = ["stressCutscene-censored.mp4", "stressPicoCutscene-censored.mp4", "darnellCutscene-censored.mp4"];
//
// ASSET EMBEDDING
@@ -869,10 +861,6 @@ class Project extends HXProject
// FEATURE FLAGS
//
// Should be true on debug builds or if GITHUB_BUILD is enabled.
FEATURE_DEBUG_FUNCTIONS.apply(this, isDebug() || GITHUB_BUILD.isEnabled(this));
FEATURE_RESULTS_DEBUG.apply(this, isDebug() || GITHUB_BUILD.isEnabled(this));
FEATURE_FUNKVIS.apply(this, true);
FEATURE_LOG_TRACE.apply(this, true);
FEATURE_DEBUG_FILE_LOGGING.apply(this, FEATURE_DEBUG_FUNCTIONS.isEnabled(this));
@@ -894,7 +882,7 @@ class Project extends HXProject
// Should be true on desktop, non-tester builds.
// We don't want testers to accidentally leak songs to their Discord friends!
FEATURE_DISCORD_RPC.apply(this, isDesktop() && !FEATURE_DEBUG_FUNCTIONS.isEnabled(this));
FEATURE_DISCORD_RPC.apply(this, isDesktop() && !FEATURE_DEBUG_FUNCTIONS.isEnabled(this) && !isHashLink());
FEATURE_LOST_FOCUS_VOLUME.apply(this, isDesktop());
@@ -1133,6 +1121,13 @@ class Project extends HXProject
// Ensure all flixel-animate classes are available at runtime.
addHaxeMacro("include('animate')");
// Ensure all openfl classes are available at runtime.
addHaxeMacro("include('openfl')");
// Ensure all lime classes are available at runtime.
// Explicitly ignore tools and internal packages.
addHaxeMacro("include('lime', true, ['lime.tools.*', 'lime._internal.*'])");
// Ensure all Flixel classes are available at runtime.
// Explicitly ignore packages which require additional dependencies.
addHaxeMacro("include('flixel', true, [ 'flixel.addons.editors.spine.*', 'flixel.addons.nape.*', 'flixel.system.macros.*', 'flixel.addons.tile.FlxRayCastTilemap' ])");
@@ -1143,8 +1138,9 @@ class Project extends HXProject
*/
function configureCustomMacros()
{
// This macro allows addition of new functionality to existing Flixel. -->
// These macros allow additions of new functionality to existing Flixel. -->
addHaxeMacro("addMetadata('@:build(funkin.util.macro.FlxMacro.buildFlxBasic())', 'flixel.FlxBasic')");
addHaxeMacro("addMetadata('@:build(funkin.util.macro.FlxMacro.buildFlxSprite())', 'flixel.FlxSprite')");
}
function configureOutputDir()
@@ -1228,21 +1224,12 @@ class Project extends HXProject
intentFilter.addChild(category);
}
final intentFilterDatas:Map<String, Array<String>> = [
"android:mimeType" => ["application/zip"],
// When this is added, it makes the game show up only when it's a FNFC file.
final intentFilterDatas:Map<String,
Array<String>> = ["android:mimeType" => ["application/zip"], // When this is added, it makes the game show up only when it's a FNFC file.
// But it also for some reason make it not show up at all in ZArchiver no matter what the file is...
// "android:host" => ["*"],
"android:scheme" => ["file", "content"],
// I think having all these makes it so the extension check would work even when the files has dots like "epic-song-v2.0.fnfc"
"android:pathPattern" => [
".*\\.fnfc",
".*\\..*\\.fnfc",
".*\\..*\\..*\\.fnfc",
".*\\..*\\..*\\..*\\.fnfc",
".*\\..*\\..*\\..*\\..*\\.fnfc"
]
];
"android:scheme" => ["file", "content"], // I think having all these makes it so the extension check would work even when the files has dots like "epic-song-v2.0.fnfc"
"android:pathPattern" => [".*\\.fnfc", ".*\\..*\\.fnfc", ".*\\..*\\..*\\.fnfc", ".*\\..*\\..*\\..*\\.fnfc", ".*\\..*\\..*\\..*\\..*\\.fnfc"]];
for (key => values in intentFilterDatas)
{
@@ -1351,12 +1338,12 @@ class Project extends HXProject
addHaxelib('thx.core'); // General utility library, "the lodash of Haxe"
addHaxelib('thx.semver'); // Version string handling
if (isDebug())
if (isDebug() && isCPP())
{
addHaxelib('hxcpp-debug-server'); // VSCode debug support
}
if (isDesktop() || isMobile() && !isHashLink() && FEATURE_VIDEO_PLAYBACK.isEnabled(this))
if ((isDesktop() || isMobile()) && FEATURE_VIDEO_PLAYBACK.isEnabled(this))
{
// hxvlc doesn't function on HashLink or non-desktop platforms
// It's also unnecessary if video playback is disabled
@@ -1422,8 +1409,11 @@ class Project extends HXProject
function configureAssets()
{
// Delete the assets folder from the export folder before adding anything.
clearAssets();
if (!isDisplay())
{
// Delete the assets folder from the export folder before adding anything.
clearAssets();
}
var exclude = EXCLUDE_ASSETS.concat(isWeb() ? EXCLUDE_ASSETS_WEB : EXCLUDE_ASSETS_NATIVE);
var shouldPreload = PRELOAD_ALL.isEnabled(this);
@@ -1535,7 +1525,7 @@ class Project extends HXProject
var exportPath:Null<String> = app.path ?? "";
// mac target uses `macos` for the export folder instead of `mac`
var platform:String = (!isMac() ? Std.string(this.target) : 'macos');
var platform:String = isHashLink() ? 'hl' : (!isMac() ? Std.string(this.target) : 'macos');
var basePath:String = Path.join([exportPath, platform, "bin"]);
var assetsPath:String = "";
+23 -18
View File
@@ -87,6 +87,21 @@ class Main extends Sprite
removeEventListener(Event.ADDED_TO_STAGE, init);
}
#if (sys && !mobile)
// Force-kill the game to prevent background processing.
Lib.current.stage.window.onClose.add(function()
{
trace(' EXITING '.bold().bg_red() + ' Game is exiting, cleaning up resources...');
#if hxvlc
// Clean up VLC threads to prevent memory leaks.
hxvlc.util.Handle.dispose();
#end
Sys.exit(0);
});
#end
// Manually crash the game when using a software renderer in order to give a nicer error message.
var context = stage.window.context.type;
if (context != WEBGL && context != OPENGL && context != OPENGLES)
@@ -149,20 +164,15 @@ class Main extends Sprite
});
#end
// Don't call anything from the preferences until the save is loaded!
#if web
// set this variable (which is a function) from the lime version at lime/_internal/backend/html5/HTML5Application.hx
// The framerate cap will more thoroughly initialize via Preferences in InitState.hx
funkin.Preferences.lockedFramerateFunction = untyped js.Syntax.code("window.requestAnimationFrame");
#end
WindowUtil.setVSyncMode(funkin.Preferences.vsyncMode);
// Force a `FunkinCamera` to be the default camera.
// This allows the blend mode shader to work everywhere.
untyped FlxG.cameras = new funkin.graphics.FunkinCameraFrontEnd();
var game:FlxGame = new FlxGame(gameWidth, gameHeight, initialState, Preferences.framerate, Preferences.framerate, skipSplash,
var framerate:Int = Preferences.unlockedFramerate ? 0 : Preferences.framerate;
var game:FlxGame = new FlxGame(gameWidth, gameHeight, initialState, framerate, framerate, skipSplash,
(FlxG.stage.window.fullscreen || Preferences.autoFullscreen));
// FlxG.game._customSoundTray wants just the class, it calls new from
@@ -174,7 +184,7 @@ class Main extends Sprite
addChild(game);
#if FEATURE_DEBUG_FUNCTIONS
game.debugger.interaction.addTool(new funkin.util.TrackerToolButtonUtil());
#if !FLX_NO_DEBUG game.debugger.interaction.addTool(new funkin.util.TrackerToolButtonUtil()); #end
funkin.util.macro.ConsoleMacro.init();
#end
@@ -236,14 +246,7 @@ class Main extends Sprite
function repositionCounters(lerp:Bool):Void
{
// Calling this so it gets scaled based on the resolution of the game and device's resolution.
var scale:Float = Math.min(FlxG.stage.stageWidth / FlxG.width, FlxG.stage.stageHeight / FlxG.height);
#if android
scale = Math.max(scale, 1);
#else
scale = Math.min(scale, 1);
#end
final thypos:Float = Math.max(FullScreenScaleMode.notchSize.x, 10);
var scale:Float = Math.max(Math.min(FlxG.stage.stageWidth / FlxG.width, FlxG.stage.stageHeight / FlxG.height), 1);
if (debugDisplay != null)
{
@@ -251,13 +254,15 @@ class Main extends Sprite
if (FlxG.game != null)
{
final thypos:Float = Math.max(FullScreenScaleMode.notchSize.x, 10);
if (lerp)
{
debugDisplay.x = flixel.math.FlxMath.lerp(debugDisplay.x, FlxG.game.x + thypos, FlxG.elapsed * 3);
}
else
{
debugDisplay.x = FlxG.game.x + FullScreenScaleMode.notchSize.x + 10;
debugDisplay.x = FlxG.game.x + thypos;
}
debugDisplay.y = FlxG.game.y + (3 * scale);
+4 -2
View File
@@ -2,6 +2,8 @@ package source; // Yeah, I know...
import sys.io.File;
using tools.AnsiUtil;
/**
* A script which executes before the game is built.
*/
@@ -12,13 +14,13 @@ class Prebuild
static function main():Void
{
var start:Float = Sys.time();
// Sys.println('[INFO] Performing pre-build tasks...');
// Sys.println(' INFO '.bold().bg_blue() + " Performing pre-build tasks...");
saveBuildTime();
var end:Float = Sys.time();
var duration:Float = end - start;
// Sys.println('[INFO] Finished pre-build tasks in $duration seconds.');
// Sys.println(' INFO '.bold().bg_blue() + ' Finished pre-build tasks in $duration seconds.');
}
static function saveBuildTime():Void
+22 -8
View File
@@ -137,7 +137,7 @@ class InitState extends FlxState
#if mobile
// Setup Mobile FNFC launcher.
funkin.mobile.util.FNFCUtil.init();
funkin.mobile.util.FNFCProvider.init();
#end
SongEventHelper.generateEaseGraphsBitmaps();
@@ -311,19 +311,34 @@ class InitState extends FlxState
#if FEATURE_LOST_FOCUS_VOLUME
@:noCompletion var _lastFocusVolume:Null<Float>;
function onLostFocus()
function onLostFocus():Void
{
if (FlxG.sound.muted || FlxG.sound.volume == 0 || FlxG.autoPause) return;
_lastFocusVolume = FlxG.sound.volume;
FlxG.sound.volume *= Constants.LOST_FOCUS_VOLUME_MULTIPLIER;
}
#end
function onGainFocus()
function onGainFocus():Void
{
#if !mobile
if (Preferences.unlockedFramerate)
{
FlxG.updateFramerate = 0;
FlxG.drawFramerate = 0;
}
else
{
FlxG.updateFramerate = Preferences.framerate;
FlxG.drawFramerate = Preferences.framerate;
}
#end
#if FEATURE_LOST_FOCUS_VOLUME
if (FlxG.sound.muted || FlxG.autoPause) return;
if (_lastFocusVolume != null) FlxG.sound.volume = _lastFocusVolume;
#end
}
#end
/**
* Start the game.
@@ -411,7 +426,6 @@ class InitState extends FlxState
function startGameNormally():Void
{
var params:CLIParams = CLIUtil.processArgs();
trace('Command line args: ${params}');
if (params.chart.shouldLoadChart)
{
@@ -445,7 +459,7 @@ class InitState extends FlxState
{
// FlxG.sound.cache(Paths.music('freakyMenu/freakyMenu'));
#if mobile
funkin.mobile.util.FNFCUtil.onFNFCOpen.add(function(fnfcFile:String)
funkin.mobile.util.FNFCProvider.onFNFCOpen.add(function(fnfcFile:String)
{
flixel.tweens.FlxTween.globalManager.clear();
flixel.util.FlxTimer.globalManager.clear();
@@ -459,7 +473,7 @@ class InitState extends FlxState
FlxG.switchState(() -> new ChartPlaytestMenu(fnfcFile));
});
final fnfcFile = funkin.mobile.util.FNFCUtil.queryFNFC();
final fnfcFile = funkin.mobile.util.FNFCProvider.queryFNFC();
if (fnfcFile != null)
{
trace('launching FNFC from $fnfcFile');
@@ -574,7 +588,7 @@ class InitState extends FlxState
//
// FLIXEL DEBUG SETUP
//
#if FEATURE_DEBUG_FUNCTIONS
#if (FEATURE_DEBUG_FUNCTIONS && !FLX_NO_DEBUG)
trace('Initializing Flixel debugger...');
#if !debug
+1 -1
View File
@@ -98,7 +98,7 @@ class PlayerSettings
*/
function new(id:Int)
{
trace('loading player settings for id: $id');
trace('Loading player settings for id: $id');
this.id = id;
this.controls = new Controls('player$id', None);
+56 -35
View File
@@ -41,12 +41,23 @@ class Preferences
{
#if web
return 60;
#elseif mobile
var refreshRate:Int = FlxG.stage.window.displayMode.refreshRate;
if (refreshRate < 60) refreshRate = 60;
return refreshRate;
#else
var save:Save = Save.instance;
save.options.framerate = value;
Save.system.flush();
FlxG.updateFramerate = value;
FlxG.drawFramerate = value;
if (!unlockedFramerate)
{
FlxG.updateFramerate = value;
FlxG.drawFramerate = value;
}
return value;
#end
}
@@ -69,13 +80,13 @@ class Preferences
static function set_naughtyness(value:Bool):Bool
{
#if NO_FEATURE_NAUGHTYNESS
value = false;
#end
return false;
#else
var save:Save = Save.instance;
save.options.naughtyness = value;
Save.system.flush();
return value;
#end
}
/**
@@ -146,19 +157,33 @@ class Preferences
{
#if NO_FEATURE_DEBUG_DISPLAY
return DebugDisplayMode.Off;
#else
#if hl
// Account for when debugDisplay used to be a boolean
var options:Null<SaveDataOptions> = Save.instance?.options;
if (options != null && Std.isOfType(options.debugDisplay, Bool))
{
var convertedDebugDisplay = cast(options.debugDisplay, Bool) ? DebugDisplayMode.Simple : DebugDisplayMode.Off;
options.debugDisplay = convertedDebugDisplay;
Save.system.flush();
}
#end
return Save?.instance?.options?.debugDisplay ?? 'Off';
#end
}
static function set_debugDisplay(value:DebugDisplayMode):DebugDisplayMode
{
#if NO_FEATURE_DEBUG_DISPLAY
return DebugDisplayMode.Off;
#else
if (value != Save.instance.options.debugDisplay) setDebugDisplayMode(value);
var save = Save.instance;
save.options.debugDisplay = value;
Save.system.flush();
return value;
#end
}
/**
@@ -253,18 +278,23 @@ class Preferences
{
#if mobile
return false;
#end
#else
return Save?.instance?.options?.autoPause ?? true;
#end
}
static function set_autoPause(value:Bool):Bool
{
#if mobile
return false;
#else
if (value != Save.instance.options.autoPause) FlxG.autoPause = value;
var save:Save = Save.instance;
save.options.autoPause = value;
Save.system.flush();
return value;
#end
}
/**
@@ -314,6 +344,9 @@ class Preferences
static function get_vsyncMode():lime.ui.WindowVSyncMode
{
#if (mobile || web)
return lime.ui.WindowVSyncMode.OFF;
#else
var value = Save?.instance?.options?.vsyncMode ?? "Off";
return switch (value)
@@ -327,10 +360,14 @@ class Preferences
default:
lime.ui.WindowVSyncMode.OFF;
};
#end
}
static function set_vsyncMode(value:lime.ui.WindowVSyncMode):lime.ui.WindowVSyncMode
{
#if (mobile || web)
return lime.ui.WindowVSyncMode.OFF;
#else
var string;
switch (value)
@@ -351,51 +388,37 @@ class Preferences
save.options.vsyncMode = string;
Save.system.flush();
return value;
#end
}
public static var unlockedFramerate(get, set):Bool;
static function get_unlockedFramerate():Bool
{
#if (mobile || web)
return false;
#else
return Save?.instance?.options?.unlockedFramerate ?? false;
#end
}
static function set_unlockedFramerate(value:Bool):Bool
{
#if (mobile || web)
return false;
#else
if (value != Save.instance.options.unlockedFramerate)
{
#if web
toggleFramerateCap(value);
#end
}
var save:Save = Save.instance;
save.options.unlockedFramerate = value;
Save.system.flush();
return value;
#end
}
#if web
// We create a haxe version of this just for readability.
// We use these to override `window.requestAnimationFrame` in Javascript to uncap the framerate / "animation" request rate
// Javascript is crazy since u can just do stuff like that lol
public static function unlockedFramerateFunction(callback, element)
{
var currTime = Date.now().getTime();
var timeToCall = 0;
var id = js.Browser.window.setTimeout(function()
{
callback(currTime + timeToCall);
}, timeToCall);
return id;
}
// Lime already implements their own little framerate cap, so we can just use that
// This also gets set in the init function in Main.hx, since we need to definitely override it
public static var lockedFramerateFunction = untyped js.Syntax.code("window.requestAnimationFrame");
#end
/**
* 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
@@ -485,9 +508,7 @@ class Preferences
setDebugDisplayMode(Preferences.debugDisplay);
setDebugDisplayBGOpacity(Preferences.debugDisplayBGOpacity / 100);
#if web
toggleFramerateCap(Preferences.unlockedFramerate);
#end
#if mobile
// Apply the allowScreenTimeout setting.
@@ -497,9 +518,9 @@ class Preferences
static function toggleFramerateCap(unlocked:Bool):Void
{
#if web
var framerateFunction = unlocked ? unlockedFramerateFunction : lockedFramerateFunction;
untyped js.Syntax.code("window.requestAnimationFrame = framerateFunction;");
#if !(mobile || web)
FlxG.drawFramerate = unlocked ? 0 : framerate;
FlxG.updateFramerate = unlocked ? 0 : framerate;
#end
}
+7
View File
@@ -6,6 +6,13 @@ import funkin.audio.waveform.WaveformData;
@:nullSafety
class VoicesGroup extends SoundGroup
{
/**
* Whenever or not the game is using the legacy vocals system (shared Voices.ogg)
*/
public var legacyVoiceSystem:Bool = false;
public var legacyVoiceUsesPlayer:Bool = false;
var playerVoices:Null<FlxTypedGroup<FunkinSound>>;
var opponentVoices:Null<FlxTypedGroup<FunkinSound>>;
@@ -3,10 +3,14 @@ package funkin.audio.waveform;
@:nullSafety
class WaveformDataParser
{
@:native("int16_max")
static final INT16_MAX:Int = 32767;
@:native("int16_min")
static final INT16_MIN:Int = -32768;
@:native("int8_max")
static final INT8_MAX:Int = 127;
@:native("int8_min")
static final INT8_MIN:Int = -128;
public static function interpretFlxSound(sound:Null<flixel.sound.FlxSound>):Null<WaveformData>
+1 -1
View File
@@ -53,7 +53,7 @@ class DataError
case TEnum(c):
trace(' ERROR '.error() + '(${Type.getEnumName(c)}) ${e.toString()}');
default:
trace(' ERROR '.error() + '(${Type.typeof(e)}) ${e.toString()}');
trace(' ERROR '.error() + '(${Type.typeof(e)}) ${Std.string(e)}');
}
}
+44 -12
View File
@@ -2,10 +2,12 @@ package funkin.data.event;
import flixel.util.FlxSort;
import funkin.data.song.SongData.SongEventData;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.play.event.ScriptedSongEvent;
import funkin.play.event.SongEvent;
import funkin.util.macro.ClassMacro;
import funkin.util.SortUtil;
import funkin.util.macro.ClassMacro;
/**
* This class statically handles the parsing of internal and scripted song event handlers.
@@ -133,24 +135,38 @@ class SongEventRegistry
}
/**
* Caching the index for the next event to query greatly reduces lag.
* Kinda nasty that it's tied to a static class though.
*/
static var nextEventIndex:Int = 0;
/**
* Retrieve the list of events to activate this frame.
*
* @param events The list of available song events.
* @param currentTime The current time in milliseconds.
* @param startIndex The index to start querying from.
* Defaults to the index of the last event handled.
* @return The list of events which haven't been handled yet.
*/
public static function queryEvents(events:Array<SongEventData>, currentTime:Float):Array<SongEventData>
public static function queryEvents(events:Array<SongEventData>, currentTime:Float, ?startIndex:Int):Array<SongEventData>
{
var result:Array<SongEventData> = events.filter(function(event:SongEventData):Bool
startIndex ??= nextEventIndex;
var result:Array<SongEventData> = [];
for (i in startIndex...events.length)
{
// If the event is already activated, don't activate it again.
if (event.activated) return false;
if (events[i].activated) continue;
// If the event is in the future, don't activate it.
if (event.time > currentTime) return false;
if (events[i].time > currentTime)
{
nextEventIndex = i;
return result;
}
return true;
});
result.sort(SortUtil.eventDataByTime.bind(FlxSort.ASCENDING));
result.push(events[i]);
}
return result;
}
@@ -183,10 +199,26 @@ class SongEventRegistry
*/
public static function resetEvents(events:Array<SongEventData>):Void
{
events.sort(SortUtil.eventDataByTime.bind(FlxSort.ASCENDING));
nextEventIndex = 0;
allEventHandlers.resize(0);
for (event in events)
{
event.activated = false;
// TODO: Add an onReset() method to SongEvent?
var handler:Null<SongEvent> = getEvent(event.eventKind);
if (handler != null && !allEventHandlers.contains(handler)) allEventHandlers.push(handler);
}
}
static var allEventHandlers:Array<SongEvent> = [];
public static inline function callEvent(scriptEvent:ScriptEvent):Void
{
for (event in allEventHandlers)
{
ScriptEventDispatcher.callEvent(event, scriptEvent);
}
}
}
@@ -49,4 +49,10 @@ typedef AlbumData =
@:optional
@:default([])
public var albumTitleAnimations:Array<AnimationData>;
/**
* An optional override for the Freeplay OST text.
*/
@:optional
public var albumOSTName:String;
}
@@ -14,7 +14,7 @@ class AlbumRegistry extends BaseRegistry<Album, AlbumData, AlbumEntryParams> imp
* Handle breaking changes by incrementing this value
* and adding migration to the `migrateAlbumData()` function.
*/
public static final ALBUM_DATA_VERSION:thx.semver.Version = '1.0.0';
public static final ALBUM_DATA_VERSION:thx.semver.Version = '1.0.3';
public static final ALBUM_DATA_VERSION_RULE:thx.semver.VersionRule = '1.0.x';
@@ -5,5 +5,17 @@ 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.3]
### Added
- Added `albumOSTName` to change the OST at the top right of the Freeplay menu.
## [1.0.2]
### Added
- Added `albumTitleOffsets` to change the horizontal and vertical position of the album title.
## [1.0.1]
### Added
- Added `albumTitleAnimations` to modify animation data for the album art.
## [1.0.0]
Initial release.
+28 -10
View File
@@ -1,11 +1,14 @@
package funkin.data.song;
import funkin.ui.debug.charting.ChartEditorState;
import funkin.data.event.SongEventRegistry;
import funkin.play.event.SongEvent;
import funkin.data.event.SongEventSchema;
import funkin.data.song.SongRegistry;
import thx.semver.Version;
import funkin.util.tools.ICloneable;
import funkin.play.notes.notekind.NoteKind;
import funkin.play.notes.notekind.NoteKindManager;
/**
* Data containing information about a song.
@@ -119,6 +122,18 @@ class SongMetadata implements ICloneable<SongMetadata>
// Update generatedBy and version before writing.
updateVersionToLatest();
// This might not be a very wise hack, but it works.
#if FEATURE_CHART_EDITOR
if (Std.isOfType(FlxG.state, ChartEditorState))
{
var state:ChartEditorState = cast(FlxG.state, ChartEditorState);
final songLength:Float = @:privateAccess state.songLengthInMs;
if (playData.previewStart > 1) playData.previewStart /= songLength;
if (playData.previewEnd > 1) playData.previewEnd /= songLength;
}
#end
var ignoreNullOptionals = true;
var writer = new json2object.JsonWriter<SongMetadata>(ignoreNullOptionals);
// I believe @:jignored should be ignored by the writer?
@@ -483,17 +498,17 @@ class SongPlayData implements ICloneable<SongPlayData>
* @since `2.2.2`
*/
@:optional
@:default(0)
public var previewStart:Int;
@:default(funkin.util.Constants.DEFAULT_PREVIEW_START_TIME)
public var previewStart:Float;
/**
* The end time for the audio preview in Freeplay.
* Defaults to 15 seconds in.
* Defaults to 20% seconds in.
* @since `2.2.2`
*/
@:optional
@:default(15000)
public var previewEnd:Int;
@:default(funkin.util.Constants.DEFAULT_PREVIEW_END_TIME)
public var previewEnd:Float;
public function new()
{
@@ -848,7 +863,7 @@ class SongEventDataRaw implements ICloneable<SongEventDataRaw>
public function getInt(key:String):Null<Int>
{
if (this.value == null) return null;
var result = Reflect.field(this.value, key);
var result:Any = Reflect.field(this.value, key);
if (result == null) return null;
if (Std.isOfType(result, Int)) return result;
if (Std.isOfType(result, String)) return Std.parseInt(cast result);
@@ -863,7 +878,7 @@ class SongEventDataRaw implements ICloneable<SongEventDataRaw>
public function getFloat(key:String):Null<Float>
{
if (this.value == null) return null;
var result = Reflect.field(this.value, key);
var result:Any = Reflect.field(this.value, key);
if (result == null) return null;
if (Std.isOfType(result, Float)) return result;
if (Std.isOfType(result, String)) return Std.parseFloat(cast result);
@@ -1303,12 +1318,15 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
*/
public function buildTooltip():String
{
if ((this.kind?.length ?? 0) == 0) return "";
if ((this.kind?.length ?? 0) == 0) return '';
var result:String = 'Kind: ${this.kind}';
var noteKind:Null<NoteKind> = NoteKindManager.getNoteKind(this.kind);
var noteKindDesc:String = noteKind?.description ?? this.kind;
var result:String = 'Kind: $noteKindDesc';
if (this.params.length == 0) return result;
result += "\nParams:";
result += '\nParams:';
for (param in params)
{
@@ -9,6 +9,7 @@ typedef OsuManiaData =
var General:
{
var PreviewTime:Int;
var Mode:Int; // Should only be 3 if you are sure this is a mania beatmap
};
var Editor:
{
@@ -46,7 +46,7 @@ class OsuManiaImporter
{
var parts:Array<String> = line.split(":");
var key:String = StringTools.trim(parts.shift());
var value:String = StringTools.trim(parts.join(":"));
var value:Any = parseValue(StringTools.trim(parts.join(":")));
if (Reflect.field(result, currentSection) == null) Reflect.setField(result, currentSection, {
});
Reflect.setField(Reflect.field(result, currentSection), key, value);
@@ -200,4 +200,22 @@ class OsuManiaImporter
return new ManiaHitObject(time, noteD, holdDuration);
});
}
static function parseValue(v:String):Any
{
var result:Null<Any>;
if (v.contains('.'))
{
result = Std.parseFloat(v);
if (!Math.isNaN(result)) return result;
}
else
{
result = Std.parseInt(v);
if (result != null) return result;
}
return v;
}
}
@@ -5,7 +5,12 @@ 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.2]
### Added
- Added `capsule` field to change the Level's capsule name on the Freeplay menu.
## [1.0.1]
### Added
- Added `visible` attribute.
## [1.0.0]
@@ -21,6 +21,13 @@ typedef LevelData =
*/
var name:String;
/**
* Optional data for how a level title is labeled on a capsule.
* Used when displaying songs in Freeplay.
*/
@:optional
var capsule:Null<LevelCapsuleData>;
/**
* The graphic for the level, as seen in the scrolling list.
*/
@@ -56,6 +63,24 @@ typedef LevelData =
var background:String;
}
/**
* Data for a level capsule text.
*/
typedef LevelCapsuleData =
{
/**
* The label for the capsule text.
*/
@:optional
var name:String;
/**
* The offset on the position to render the capsule text at.
*/
@:optional
var offsets:Array<Float>;
}
/**
* Data for a single prop for a story mode level.
*/
@@ -129,4 +154,11 @@ typedef LevelPropData =
@:default(false)
@:optional
var flipX:Null<Bool>;
/**
* Flips the sprite on Y axis.
*/
@:default(false)
@:optional
var flipY:Null<Bool>;
}
@@ -14,7 +14,7 @@ class LevelRegistry extends BaseRegistry<Level, LevelData, LevelEntryParams> imp
* Handle breaking changes by incrementing this value
* and adding migration to the `migrateLevelData()` function.
*/
public static final LEVEL_DATA_VERSION:thx.semver.Version = "1.0.1";
public static final LEVEL_DATA_VERSION:thx.semver.Version = "1.0.2";
public static final LEVEL_DATA_VERSION_RULE:thx.semver.VersionRule = ">=1.0.0 <1.1.0";
+1 -1
View File
@@ -23,7 +23,7 @@ class FunkTrail extends FlxTrail
* @param alpha The alpha value for the very first trailsprite.
* @param diff How much lower the alpha of the next trailsprite is.
*/
public function new(target:FlxSprite, ?graphic:FlxGraphicAsset, length:Int = 10, delay:Int = 3, alpha:Float = 0.4, diff:Float = 0.05)
public function new(target:FlxSprite, ?graphic:FlxGraphicAsset, length:Int = 10, delay:Float = 0.1, alpha:Float = 0.4, diff:Float = 0.05)
{
super(target, graphic, length, delay, alpha, diff);
}
@@ -5,9 +5,9 @@ package funkin.external.apple;
* A utility class for FNFC loading on apple devices.
*/
@:build(funkin.util.macro.LinkerMacro.xml('project/Build.xml'))
@:include('FNFCUtil.hpp')
@:include('FNFCExtern.hpp')
@:unreflective
extern class FNFCUtil
extern class FNFCExtern
{
/**
* Copies the FNFC resource from the specified URL into the cache.
@@ -15,7 +15,7 @@ extern class FNFCUtil
* @param url The URL of the FNFC resource to copy into the cache.
* @param callback A function to be called when the copy operation completes.
*/
@:native('Apple_FNFCUtil_CopyFNFCIntoCache')
@:native('Apple_FNFCExtern_CopyFNFCIntoCache')
static function copyFNFCIntoCache(url:cpp.ConstCharStar, callback:cpp.Callable<(event:cpp.ConstCharStar, value:cpp.ConstCharStar) -> Void>):Void;
}
#end
+1 -1
View File
@@ -16,7 +16,7 @@
<file name="${this_dir}/src/AudioSession.mm" />
<file name="${this_dir}/src/MemoryUtil.mm" />
<file name="${this_dir}/src/ScreenUtil.mm" />
<file name="${this_dir}/src/FNFCUtil.mm" />
<file name="${this_dir}/src/FNFCExtern.mm" />
</files>
<target id="haxe">
@@ -14,4 +14,4 @@ typedef void (*FNFCCallback)(const char* event, const char* value);
* @param url The URL of the FNFC resource to copy into the cache.
* @param callback A function to be called when the copy operation completes.
*/
void Apple_FNFCUtil_CopyFNFCIntoCache(const char *url, FNFCCallback callback);
void Apple_FNFCExtern_CopyFNFCIntoCache(const char *url, FNFCCallback callback);
@@ -1,9 +1,9 @@
#include "FNFCUtil.hpp"
#include "FNFCExtern.hpp"
#import <Foundation/Foundation.h>
static FNFCCallback fnfcCallback = nullptr;
void Apple_FNFCUtil_CopyFNFCIntoCache(const char *url, FNFCCallback callback)
void Apple_FNFCExtern_CopyFNFCIntoCache(const char *url, FNFCCallback callback)
{
#if TARGET_OS_IOS
if (!fnfcCallback)
+27 -16
View File
@@ -11,16 +11,18 @@ void Apple_ScreenUtil_GetSafeAreaInsets(double* top, double* bottom, double* lef
#if TARGET_OS_IOS
if (@available(iOS 11, *))
{
UIWindow* window = [UIApplication sharedApplication].windows[0];
UIEdgeInsets safeAreaInsets = window.safeAreaInsets;
float scale = [UIScreen mainScreen].scale;
UIWindow *window = [UIApplication sharedApplication].windows[0];
if (window)
{
UIEdgeInsets insets = window.safeAreaInsets;
CGFloat scale = window.screen.nativeScale;
(*top) = safeAreaInsets.top * scale;
(*bottom) = safeAreaInsets.bottom * scale;
(*left) = safeAreaInsets.left * scale;
(*right) = safeAreaInsets.right * scale;
return;
(*top) = (double)(insets.top * scale);
(*bottom) = (double)(insets.bottom * scale);
(*left) = (double)(insets.left * scale);
(*right) = (double)(insets.right * scale);
return;
}
}
#endif
@@ -33,13 +35,22 @@ void Apple_ScreenUtil_GetSafeAreaInsets(double* top, double* bottom, double* lef
void Apple_ScreenUtil_GetScreenSize(double* width, double* height)
{
#if TARGET_OS_IOS
CGRect screenRect = [[UIScreen mainScreen] bounds];
float scale = [UIScreen mainScreen].scale;
if (@available(iOS 11, *))
{
UIWindow *window = [UIApplication sharedApplication].windows[0];
if (window && window.rootViewController)
{
UIView *view = window.rootViewController.view;
CGSize size = view.bounds.size;
CGFloat scale = window.screen.nativeScale;
(*width) = (double)screenRect.size.width * scale;
(*height) = (double)screenRect.size.height * scale;
#else
(*width) = 0.0;
(*width) = 0.0;
(*width) = (double)((int)(size.width * scale));
(*height) = (double)((int)(size.height * scale));
return;
}
}
#endif
(*width) = 0.0;
(*height) = 0.0;
}
-47
View File
@@ -8,36 +8,6 @@ package funkin.external.windows;
@:include('winapi.hpp')
extern class WinAPI
{
/**
* Shows a message box with an error icon.
*
* @param handle A handle to the parent window.
* @param message The message to display.
* @param title The title of the message box.
*/
@:native('WINAPI_ShowError')
static function showError(handle:cpp.RawPointer<cpp.Void>, message:cpp.ConstCharStar, title:cpp.ConstCharStar):Void;
/**
* Shows a message box with a warning icon.
*
* @param handle A handle to the parent window.
* @param message The message to display.
* @param title The title of the message box.
*/
@:native('WINAPI_ShowWarning')
static function showWarning(handle:cpp.RawPointer<cpp.Void>, message:cpp.ConstCharStar, title:cpp.ConstCharStar):Void;
/**
* Shows a message box with an information icon.
*
* @param handle A handle to the parent window.
* @param message The message to display.
* @param title The title of the message box.
*/
@:native('WINAPI_ShowInformation')
static function showInformation(handle:cpp.RawPointer<cpp.Void>, message:cpp.ConstCharStar, title:cpp.ConstCharStar):Void;
/**
* Disables the "Report to Microsoft" dialog that appears when the application crashes.
*/
@@ -57,22 +27,5 @@ extern class WinAPI
*/
@:native('WINAPI_GetProcessMemoryWorkingSetSize')
static function getProcessMemoryWorkingSetSize():cpp.SizeT;
/**
* Sets the dark mode for the active window.
*
* @param handle A handle to the parent window.
* @param enable Whether to enable or disable dark mode.
*/
@:native('WINAPI_SetDarkMode')
static function setDarkMode(handle:cpp.RawPointer<cpp.Void>, enable:Bool):Void;
/**
* Checks if the system is using dark mode.
*
* @return True if system is in dark mode, false otherwise.
*/
@:native('WINAPI_IsSystemDarkMode')
static function isSystemDarkMode():Bool;
}
#end
@@ -1,32 +1,5 @@
#pragma once
/**
* Shows an error message box.
*
* @param handle A handle to the parent window
* @param message The error message to display
* @param title The title of the message box
*/
void WINAPI_ShowError(void* handle, const char *message, const char *title);
/**
* Shows a warning message box.
*
* @param handle A handle to the parent window
* @param message The warning message to display
* @param title The title of the message box
*/
void WINAPI_ShowWarning(void* handle, const char *message, const char *title);
/**
* Shows an information message box.
*
* @param handle A handle to the parent window
* @param message The information message to display
* @param title The title of the message box
*/
void WINAPI_ShowInformation(void* handle, const char *message, const char *title);
/**
* Disables Windows error reporting dialogs.
*/
@@ -45,18 +18,3 @@ void WINAPI_DisableWindowsGhosting();
* @return The working set size in bytes. Returns 0 if the query fails.
*/
size_t WINAPI_GetProcessMemoryWorkingSetSize();
/**
* Sets dark mode on a window
* @param handle A handle to the parent window.
*
* @param enable True to enable dark mode, false to disable
*/
void WINAPI_SetDarkMode(void* handle, bool enable);
/**
* Checks if the system is using dark mode.
*
* @return True if system is in dark mode, false otherwise
*/
bool WINAPI_IsSystemDarkMode();
-67
View File
@@ -11,37 +11,6 @@
#include <stdint.h>
#include <stdio.h>
void WINAPI_ShowError(void* handle, const char *message, const char *title)
{
HWND hwnd = (HWND)handle;
if (!IsWindow(hwnd))
hwnd = nullptr;
MessageBox(hwnd, message, title, MB_OK | MB_ICONERROR);
}
void WINAPI_ShowWarning(void* handle, const char *message, const char *title)
{
HWND hwnd = (HWND)handle;
if (!IsWindow(hwnd))
hwnd = nullptr;
MessageBox(hwnd, message, title, MB_OK | MB_ICONWARNING);
}
void WINAPI_ShowInformation(void* handle, const char *message, const char *title)
{
HWND hwnd = (HWND)handle;
if (!IsWindow(hwnd))
hwnd = nullptr;
MessageBox(hwnd, message, title, MB_OK | MB_ICONINFORMATION);
}
void WINAPI_DisableErrorReporting()
{
SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX);
@@ -61,39 +30,3 @@ size_t WINAPI_GetProcessMemoryWorkingSetSize()
return 0;
}
void WINAPI_SetDarkMode(void* handle, bool enable)
{
HWND hwnd = (HWND)handle;
if (!IsWindow(hwnd))
return;
int darkMode = enable ? 1 : 0;
if (DwmSetWindowAttribute(hwnd, 20, &darkMode, sizeof(darkMode)) != S_OK)
DwmSetWindowAttribute(hwnd, 19, &darkMode, sizeof(darkMode));
UpdateWindow(hwnd);
}
bool WINAPI_IsSystemDarkMode()
{
HKEY hKey;
DWORD dwValue = 0;
DWORD dwSize = sizeof(DWORD);
DWORD dwType = REG_DWORD;
if (RegOpenKeyEx(HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", 0, KEY_READ, &hKey) == ERROR_SUCCESS)
{
if (RegQueryValueEx(hKey, "AppsUseLightTheme", NULL, &dwType, (LPBYTE)&dwValue, &dwSize) == ERROR_SUCCESS)
{
RegCloseKey(hKey);
return dwValue == 0;
}
RegCloseKey(hKey);
}
return false;
}
-420
View File
@@ -1,420 +0,0 @@
package funkin.graphics;
import flixel.FlxBasic;
import flixel.FlxCamera;
import flixel.FlxG;
import flixel.FlxSprite;
import flixel.graphics.FlxGraphic;
import flixel.graphics.frames.FlxFrame;
import flixel.math.FlxMatrix;
import flixel.math.FlxRect;
import lime.graphics.cairo.Cairo;
import openfl.display.BitmapData;
import openfl.display.BlendMode;
import openfl.display.DisplayObjectRenderer;
import openfl.display.Graphics;
import openfl.display.OpenGLRenderer;
import openfl.display3D.Context3D;
import openfl.filters.BitmapFilter;
import openfl.geom.ColorTransform;
import openfl.geom.Matrix;
import openfl.geom.Rectangle;
#if (js && html5)
import lime._internal.graphics.ImageCanvasUtil;
import openfl.display.CanvasRenderer;
import openfl.display._internal.CanvasGraphics as GfxRenderer;
#else
import openfl.display.CairoRenderer;
import openfl.display._internal.CairoGraphics as GfxRenderer;
#end
/**
* A modified `FlxSprite` that supports filters.
* The name's pretty much self-explanatory.
*/
@:nullSafety
@:access(openfl.geom.Rectangle)
@:access(openfl.filters.BitmapFilter)
@:access(flixel.graphics.frames.FlxFrame)
class FlxFilteredSprite extends FlxSprite
{
@:noCompletion var _renderer:FlxAnimateFilterRenderer = new FlxAnimateFilterRenderer();
@:noCompletion var _filterMatrix:FlxMatrix = new FlxMatrix();
/**
* An `Array` of shader filters (aka `BitmapFilter`).
*/
public var filters(default, set):Null<Array<BitmapFilter>>;
/**
* a flag to update the image with the filters.
* Useful when trying to render a shader at all times.
*/
public var filterDirty:Bool = false;
@:noCompletion var filtered:Bool = false;
// These appear to be a little troublesome to null safe.
@:nullSafety(Off)
@:noCompletion var _blankFrame:FlxFrame;
@:nullSafety(Off)
var _filterBmp1:BitmapData;
@:nullSafety(Off)
var _filterBmp2:BitmapData;
override public function update(elapsed:Float)
{
super.update(elapsed);
if (!filterDirty && filters != null)
{
for (filter in filters)
{
if (filter.__renderDirty)
{
filterDirty = true;
break;
}
}
}
}
@:noCompletion
override function initVars():Void
{
super.initVars();
_filterMatrix = new FlxMatrix();
filters = null;
filtered = false;
}
override public function draw():Void
{
checkEmptyFrame();
if (alpha == 0 || _frame.type == FlxFrameType.EMPTY) return;
if (dirty) // rarely
calcFrame(useFramePixels);
if (filterDirty) filterFrame();
for (camera in cameras)
{
if (!camera.visible || !camera.exists || !isOnScreen(camera)) continue;
getScreenPosition(_point, camera).subtractPoint(offset);
if (isSimpleRender(camera)) drawSimple(camera);
else
drawComplex(camera);
#if FLX_DEBUG
FlxBasic.visibleCount++;
#end
}
#if FLX_DEBUG
if (FlxG.debugger.drawDebug) drawDebug();
#end
}
@:noCompletion
override function drawComplex(camera:FlxCamera):Void
{
_frame.prepareMatrix(_matrix, FlxFrameAngle.ANGLE_0, checkFlipX(), checkFlipY());
_matrix.concat(_filterMatrix);
_matrix.translate(-origin.x, -origin.y);
_matrix.scale(scale.x, scale.y);
if (bakedRotationAngle <= 0)
{
updateTrig();
if (angle != 0) _matrix.rotateWithTrig(_cosAngle, _sinAngle);
}
_point.add(origin.x, origin.y);
_matrix.translate(_point.x, _point.y);
if (isPixelPerfectRender(camera))
{
_matrix.tx = Math.floor(_matrix.tx);
_matrix.ty = Math.floor(_matrix.ty);
}
camera.drawPixels((filtered) ? _blankFrame : _frame, framePixels, _matrix, colorTransform, blend, antialiasing, shader);
}
@:noCompletion
function filterFrame()
{
filterDirty = false;
_filterMatrix.identity();
if (filters != null && filters.length > 0)
{
_flashRect.setEmpty();
for (filter in filters)
{
_flashRect.__expand(-filter.__leftExtension,
-filter.__topExtension, filter.__leftExtension
+ filter.__rightExtension,
filter.__topExtension
+ filter.__bottomExtension);
}
_flashRect.width += frameWidth;
_flashRect.height += frameHeight;
@:nullSafety(Off)
if (_blankFrame == null) _blankFrame = new FlxFrame(null);
if (_blankFrame.parent == null || _flashRect.width > _blankFrame.parent.width || _flashRect.height > _blankFrame.parent.height)
{
if (_blankFrame.parent != null)
{
_blankFrame.parent.destroy();
_filterBmp1.dispose();
_filterBmp2.dispose();
}
_blankFrame.parent = FlxGraphic.fromRectangle(Math.ceil(_flashRect.width * 1.25), Math.ceil(_flashRect.height * 1.25), 0, true);
_filterBmp1 = new BitmapData(_blankFrame.parent.width, _blankFrame.parent.height, 0);
_filterBmp2 = new BitmapData(_blankFrame.parent.width, _blankFrame.parent.height, 0);
}
_blankFrame.offset.copyFrom(_frame.offset);
@:nullSafety(Off)
_blankFrame.parent.bitmap = _renderer.applyFilter(_blankFrame.parent.bitmap, _filterBmp1, _filterBmp2, frame.parent.bitmap, filters, _flashRect,
frame.frame.copyToFlash());
_blankFrame.frame = FlxRect.get(0, 0, _blankFrame.parent.bitmap.width, _blankFrame.parent.bitmap.height);
_filterMatrix.translate(_flashRect.x, _flashRect.y);
_frame = _blankFrame.copyTo();
filtered = true;
}
else
{
resetFrame();
filtered = false;
}
}
@:noCompletion
function set_filters(value:Null<Array<BitmapFilter>>)
{
if (filters != value) filterDirty = true;
return filters = value;
}
@:noCompletion
override function set_frame(value:FlxFrame)
{
if (value != frame) filterDirty = true;
return super.set_frame(value);
}
override public function destroy()
{
super.destroy();
}
}
@:noCompletion
@:access(openfl.display.OpenGLRenderer)
@:access(openfl.filters.BitmapFilter)
@:access(openfl.geom.Rectangle)
@:access(openfl.display.Stage)
@:access(openfl.display.Graphics)
@:access(openfl.display.Shader)
@:access(openfl.display.BitmapData)
@:access(openfl.geom.ColorTransform)
@:access(openfl.display.DisplayObject)
@:access(openfl.display3D.Context3D)
@:access(openfl.display.CanvasRenderer)
@:access(openfl.display.CairoRenderer)
@:access(openfl.display3D.Context3D)
class FlxAnimateFilterRenderer
{
var renderer:OpenGLRenderer;
var context:Context3D;
public function new()
{
// context = new openfl.display3D.Context3D(null);
renderer = new OpenGLRenderer(FlxG.game.stage.context3D);
renderer.__worldTransform = new Matrix();
renderer.__worldColorTransform = new ColorTransform();
}
@:noCompletion function setRenderer(renderer:DisplayObjectRenderer, rect:Rectangle)
{
@:privateAccess
if (true)
{
var displayObject = FlxG.game;
var pixelRatio = FlxG.game.stage.__renderer.__pixelRatio;
var offsetX = rect.x > 0 ? Math.ceil(rect.x) : Math.floor(rect.x);
var offsetY = rect.y > 0 ? Math.ceil(rect.y) : Math.floor(rect.y);
if (renderer.__worldTransform == null)
{
renderer.__worldTransform = new Matrix();
renderer.__worldColorTransform = new ColorTransform();
}
if (displayObject.__cacheBitmapColorTransform == null) displayObject.__cacheBitmapColorTransform = new ColorTransform();
renderer.__stage = displayObject.stage;
renderer.__allowSmoothing = true;
renderer.__setBlendMode(NORMAL);
renderer.__worldAlpha = 1 / displayObject.__worldAlpha;
renderer.__worldTransform.identity();
renderer.__worldTransform.invert();
renderer.__worldTransform.concat(new Matrix());
renderer.__worldTransform.tx -= offsetX;
renderer.__worldTransform.ty -= offsetY;
renderer.__worldTransform.scale(pixelRatio, pixelRatio);
renderer.__pixelRatio = pixelRatio;
}
}
public function applyFilter(target:BitmapData = null, target1:BitmapData = null, target2:BitmapData = null, bmp:BitmapData, filters:Array<BitmapFilter>,
rect:Rectangle, bmpRect:Rectangle)
{
if (filters == null || filters.length == 0) return bmp;
renderer.__setBlendMode(NORMAL);
renderer.__worldAlpha = 1;
if (renderer.__worldTransform == null)
{
renderer.__worldTransform = new Matrix();
renderer.__worldColorTransform = new ColorTransform();
}
renderer.__worldTransform.identity();
renderer.__worldColorTransform.__identity();
var bitmap:BitmapData = (target == null) ? new BitmapData(Math.ceil(rect.width * 1.25), Math.ceil(rect.height * 1.25), true, 0) : target;
var bitmap2 = (target1 == null) ? new BitmapData(Math.ceil(rect.width * 1.25), Math.ceil(rect.height * 1.25), true, 0) : target1,
bitmap3 = (target2 == null) ? bitmap2.clone() : target2;
renderer.__setRenderTarget(bitmap);
bmp.__renderTransform.translate(Math.abs(rect.x) - bmpRect.x, Math.abs(rect.y) - bmpRect.y);
bmpRect.x = Math.abs(rect.x);
bmpRect.y = Math.abs(rect.y);
var bestResolution = renderer.__context3D.__backBufferWantsBestResolution;
renderer.__context3D.__backBufferWantsBestResolution = false;
renderer.__scissorRect(bmpRect);
renderer.__renderFilterPass(bmp, renderer.__defaultDisplayShader, true);
renderer.__scissorRect();
renderer.__context3D.__backBufferWantsBestResolution = bestResolution;
bmp.__renderTransform.identity();
var shader, cacheBitmap = null;
for (filter in filters)
{
if (filter.__preserveObject)
{
renderer.__setRenderTarget(bitmap3);
renderer.__renderFilterPass(bitmap, renderer.__defaultDisplayShader, filter.__smooth);
}
for (i in 0...filter.__numShaderPasses)
{
shader = filter.__initShader(renderer, i, (filter.__preserveObject) ? bitmap3 : null);
renderer.__setBlendMode(filter.__shaderBlendMode);
renderer.__setRenderTarget(bitmap2);
renderer.__renderFilterPass(bitmap, shader, filter.__smooth);
cacheBitmap = bitmap;
bitmap = bitmap2;
bitmap2 = cacheBitmap;
}
filter.__renderDirty = false;
}
if (target1 == null) bitmap2.dispose();
if (target2 == null) bitmap3.dispose();
// var gl = renderer.__gl;
// var renderBuffer = bitmap.getTexture(renderer.__context3D);
// @:privateAccess
// gl.readPixels(0, 0, bitmap.width, bitmap.height, renderBuffer.__format, gl.UNSIGNED_BYTE, bitmap.image.data);
// bitmap.image.version = 0;
// @:privateAccess
// bitmap.__textureVersion = -1;
return bitmap;
}
public function applyBlend(blend:BlendMode, bitmap:BitmapData)
{
bitmap.__update(false, true);
var bmp = new BitmapData(bitmap.width, bitmap.height, 0);
#if (js && html5)
ImageCanvasUtil.convertToCanvas(bmp.image);
@:privateAccess
var renderer = new CanvasRenderer(bmp.image.buffer.__srcContext);
#else
var renderer = new CairoRenderer(new Cairo(bmp.getSurface()));
#end
// setRenderer(renderer, bmp.rect);
var m = new Matrix();
var c = new ColorTransform();
renderer.__allowSmoothing = true;
renderer.__overrideBlendMode = blend;
renderer.__worldTransform = m;
renderer.__worldAlpha = 1;
renderer.__worldColorTransform = c;
renderer.__setBlendMode(blend);
#if (js && html5)
bmp.__drawCanvas(bitmap, renderer);
#else
bmp.__drawCairo(bitmap, renderer);
#end
return bitmap;
}
public function graphicstoBitmapData(gfx:Graphics)
{
if (gfx.__bounds == null) return null;
// var cacheRTT = renderer.__context3D.__state.renderToTexture;
// var cacheRTTDepthStencil = renderer.__context3D.__state.renderToTextureDepthStencil;
// var cacheRTTAntiAlias = renderer.__context3D.__state.renderToTextureAntiAlias;
// var cacheRTTSurfaceSelector = renderer.__context3D.__state.renderToTextureSurfaceSelector;
// var bmp = new BitmapData(Math.ceil(gfx.__width), Math.ceil(gfx.__height), 0);
// renderer.__context3D.setRenderToTexture(bmp.getTexture(renderer.__context3D));
// gfx.__owner.__renderTransform.identity();
// gfx.__renderTransform.identity();
// Context3DGraphics.render(gfx, renderer);
GfxRenderer.render(gfx, cast renderer.__softwareRenderer);
var bmp = gfx.__bitmap;
gfx.__bitmap = null;
// if (cacheRTT != null)
// {
// renderer.__context3D.setRenderToTexture(cacheRTT, cacheRTTDepthStencil, cacheRTTAntiAlias, cacheRTTSurfaceSelector);
// }
// else
// {
// renderer.__context3D.setRenderToBackBuffer();
// }
return bmp;
}
}
@@ -1,8 +1,8 @@
package funkin.graphics;
import funkin.graphics.FunkinSprite;
import animate.FlxAnimateController;
@:access(funkin.graphics.FunkinSprite)
class FunkinAnimationController extends FlxAnimateController
{
/**
@@ -16,6 +16,12 @@ class FunkinAnimationController extends FlxAnimateController
_parentSprite = sprite;
}
override function set_frameIndex(frame:Int):Int
{
_parentSprite._renderTextureDirty = true;
return super.set_frameIndex(frame);
}
/**
* We override `FlxAnimationController`'s `play` method to account for texture atlases.
*/
+7 -3
View File
@@ -186,11 +186,15 @@ class FunkinCamera extends FlxCamera
_backgroundFrame.parent.bitmap = _blendRenderTexture.graphic.bitmap;
_backgroundRenderTexture.init(Std.int(this.width * Lib.current.stage.window.scale), Std.int(this.height * Lib.current.stage.window.scale));
// On some displays, the DPI can be less than 1, which causes the blend shader to look bad
// We just clamp the scale to 1 to avoid this!
var clampedScale:Float = Math.max(1, Lib.current.stage.window.scale);
_backgroundRenderTexture.init(Std.int(this.width * clampedScale), Std.int(this.height * clampedScale));
_backgroundRenderTexture.drawToCamera((camera, matrix) ->
{
camera.zoom = this.zoom;
matrix.scale(Lib.current.stage.window.scale, Lib.current.stage.window.scale);
matrix.scale(clampedScale, clampedScale);
camera.drawPixels(_backgroundFrame, null, matrix, canvas.transform.colorTransform, null, false, _blendShader);
});
@@ -198,7 +202,7 @@ class FunkinCamera extends FlxCamera
// Resize the frame so it always fills the screen
_cameraMatrix.identity();
_cameraMatrix.scale(1 / (this.scaleX * Lib.current.stage.window.scale), 1 / (this.scaleY * Lib.current.stage.window.scale));
_cameraMatrix.scale(1 / (this.scaleX * clampedScale), 1 / (this.scaleY * clampedScale));
_cameraMatrix.translate(((width - width / this.scaleX) * 0.5), ((height - height / this.scaleY) * 0.5));
super.drawPixels(_backgroundRenderTexture.graphic.imageFrame.frame, null, _cameraMatrix, null, null, smoothing, null);
+136
View File
@@ -6,6 +6,7 @@ import flixel.graphics.FlxGraphic;
import flixel.tweens.FlxTween;
import openfl.display3D.textures.TextureBase;
import funkin.graphics.framebuffer.FixedBitmapData;
import funkin.graphics.framebuffer.FunkinFilterRenderer;
import openfl.display.BitmapData;
import flixel.math.FlxRect;
import flixel.math.FlxPoint;
@@ -21,6 +22,8 @@ import animate.internal.elements.AtlasInstance;
import animate.internal.elements.SymbolInstance;
import animate.FlxAnimate;
import animate.FlxAnimateFrames;
import animate.internal.RenderTexture;
import openfl.filters.BitmapFilter;
import haxe.io.Path;
using StringTools;
@@ -114,6 +117,11 @@ typedef AtlasSpriteSettings =
@:access(animate.FlxAnimateController)
class FunkinSprite extends FlxAnimate
{
/**
* The filters array to be applied to the sprite.
*/
public var filters(default, set):Null<Array<BitmapFilter>> = null;
/**
* @param x Starting X position
* @param y Starting Y position
@@ -124,6 +132,8 @@ class FunkinSprite extends FlxAnimate
{
super(x, y);
filterRenderer = new FunkinFilterRenderer(this);
if (path != null)
{
var ext:String = Path.extension(path);
@@ -774,10 +784,136 @@ class FunkinSprite extends FlxAnimate
matrix.ty = Math.round(matrix.ty / this.scale.y) * this.scale.y;
}
var filterRenderer:FunkinFilterRenderer;
var filtered:Bool = false;
var filterOffsets:Array<Float> = [0, 0];
override function checkRenderTexture():Bool
{
// Forcefully enable render texture when we have filters.
if (filters != null && filters.length > 0) return true;
return super.checkRenderTexture();
}
function set_filters(value:Null<Array<BitmapFilter>>):Null<Array<BitmapFilter>>
{
if (filters != value) _renderTextureDirty = true;
filters = value;
return value;
}
override public function draw():Void
{
for (filter in filters ?? [])
{
@:privateAccess
if (filter.__renderDirty) _renderTextureDirty = true;
}
super.draw();
}
override function drawFrameComplex(frame:FlxFrame, camera:FlxCamera):Void
{
final willUseRenderTexture = checkRenderTexture();
final matrix = this._matrix;
frame.prepareMatrix(matrix, FlxFrameAngle.ANGLE_0, checkFlipX(), checkFlipY());
prepareDrawMatrix(matrix, camera);
if (willUseRenderTexture)
{
var bounds:Array<Int> = [Math.ceil(frame.frame.width), Math.ceil(frame.frame.height)];
if (_renderTexture == null) _renderTexture = new RenderTexture(bounds[0], bounds[1]);
if (_renderTextureDirty)
{
_renderTexture.init(bounds[0], bounds[1]);
_renderTexture.drawToCamera((camera, mat) ->
{
camera.drawPixels(frame, framePixels, mat, null, null, antialiasing, null);
});
_renderTexture.render();
filterRenderer.applyFilters();
_renderTextureDirty = false;
}
if (filtered)
{
matrix.translate(filterOffsets[0], filterOffsets[1]);
camera.drawPixels(filterRenderer.graphic?.imageFrame.frame, null, matrix, colorTransform, blend, antialiasing, shader);
}
else
{
camera.drawPixels(_renderTexture.graphic.imageFrame.frame, framePixels, matrix, colorTransform, blend, antialiasing, shader);
}
}
else
{
camera.drawPixels(frame, framePixels, matrix, colorTransform, blend, antialiasing, shader);
}
}
override function drawAnimate(camera:FlxCamera):Void
{
final willUseRenderTexture = checkRenderTexture();
final matrix = _matrix;
matrix.identity();
@:privateAccess
var bounds = timeline._bounds;
if (!willUseRenderTexture) matrix.translate(-bounds.x, -bounds.y);
prepareAnimateMatrix(matrix, camera, bounds);
if (renderStage) drawStage(camera);
timeline.currentFrame = animation.frameIndex;
#if !flash
if (willUseRenderTexture)
{
if (_renderTexture == null) _renderTexture = new RenderTexture(Math.ceil(bounds.width), Math.ceil(bounds.height));
if (_renderTextureDirty)
{
_renderTexture.init(Math.ceil(bounds.width), Math.ceil(bounds.height));
_renderTexture.drawToCamera((camera, matrix) ->
{
matrix.translate(-bounds.x, -bounds.y);
timeline.draw(camera, matrix, null, null, antialiasing, null);
});
_renderTexture.render();
filterRenderer.applyFilters();
_renderTextureDirty = false;
}
if (filtered)
{
matrix.translate(filterOffsets[0], filterOffsets[1]);
camera.drawPixels(filterRenderer.graphic?.imageFrame.frame, null, matrix, colorTransform, blend, antialiasing, shader);
}
else
{
camera.drawPixels(_renderTexture.graphic.imageFrame.frame, framePixels, matrix, colorTransform, blend, antialiasing, shader);
}
}
else
#end
{
timeline.draw(camera, matrix, colorTransform, blend, antialiasing, shader);
}
}
public override function destroy():Void
{
@:nullSafety(Off) // TODO: Remove when flixel.FlxSprite is null safed.
frames = null;
filterRenderer.destroy();
// Cancel all tweens so they don't continue to run on a destroyed sprite.
// This prevents crashes.
FlxTween.cancelTweensOf(this);
@@ -0,0 +1,163 @@
package funkin.graphics.framebuffer;
import funkin.graphics.framebuffer.FixedBitmapData;
import openfl.display.BitmapData;
import flixel.math.FlxRect;
import flixel.graphics.FlxGraphic;
import flixel.util.FlxDestroyUtil;
import funkin.graphics.FunkinSprite;
import openfl.filters.BitmapFilter;
import animate.internal.FilterRenderer;
/**
* A helper for rendering filters on `FunkinSprite` instances.
*/
@:access(animate.FlxAnimate)
@:access(openfl.filters.BitmapFilter)
@:access(animate.internal.FilterRenderer)
@:access(openfl.display.OpenGLRenderer)
@:access(openfl.geom.ColorTransform)
@:access(openfl.display.BitmapData)
@:nullSafety
class FunkinFilterRenderer implements IFlxDestroyable
{
/**
* Graphic containing the current frame with filters.
*/
public var graphic(default, null):Null<FlxGraphic>;
var bitmapPool:Map<String, Array<BitmapData>> = [];
var parent:FunkinSprite;
public function new(parent:FunkinSprite)
{
this.parent = parent;
}
/**
* Apply filters to the current frame.
* The result will be contained in the `graphic` variable.
*/
public function applyFilters():Void
{
parent.filtered = false;
if (parent.filters == null || parent.filters.length < 1) return;
var textureBitmap:BitmapData = parent._renderTexture.graphic.bitmap;
var bounds:FlxRect = FlxRect.get().copyFromFlash(textureBitmap.rect);
FilterRenderer.expandFilterBounds(bounds, parent.filters);
parent.filterOffsets = [bounds.x * parent.scale.x, bounds.y * parent.scale.x];
var ceilWidth:Int = Math.ceil(bounds.width);
var ceilHeight:Int = Math.ceil(bounds.height);
if (graphic != null) putBitmap(graphic.bitmap);
var bitmap:BitmapData = getBitmap(ceilWidth, ceilHeight);
if (graphic == null)
{
graphic = FlxGraphic.fromBitmapData(bitmap, false, null, false);
}
else
{
graphic.bitmap = bitmap;
graphic.imageFrame.frame.frame.set(0, 0, bitmap.width, bitmap.height);
}
var filterBmp1:Null<BitmapData> = null;
var filterBmp2:Null<BitmapData> = null;
var needsSecondBitmap:Bool = false;
var needsPreserveObject:Bool = false;
for (filter in parent.filters)
{
if (filter != null)
{
if (filter.__needSecondBitmapData) needsSecondBitmap = true;
if (filter.__preserveObject) needsPreserveObject = true;
}
}
if (needsSecondBitmap) filterBmp1 = getBitmap(graphic.width, graphic.height);
if (needsPreserveObject) filterBmp2 = getBitmap(filterBmp1?.width ?? 1, filterBmp1?.height ?? 1);
_applyFilters(graphic.bitmap, textureBitmap, parent.filters, filterBmp1, filterBmp2, bounds);
if (filterBmp1 != null) putBitmap(filterBmp1);
if (filterBmp2 != null) putBitmap(filterBmp2);
bounds.put();
parent.filtered = true;
}
function _applyFilters(target:BitmapData, bmp:BitmapData, filters:Array<BitmapFilter>, target1:Null<BitmapData>, target2:Null<BitmapData>,
bounds:FlxRect):Void
{
var renderer = FilterRenderer.renderer;
var bitmap:BitmapData = target;
var bitmap2:BitmapData = target1 ?? bmp;
var bitmap3:BitmapData = target2 ?? bmp;
renderer.__setBlendMode(NORMAL);
renderer.__worldAlpha = 1;
if (renderer.__worldTransform == null)
{
renderer.__worldTransform = new openfl.geom.Matrix();
renderer.__worldColorTransform = new openfl.geom.ColorTransform();
}
renderer.__worldTransform.identity();
renderer.__worldColorTransform.__identity();
bmp.__renderTransform.identity();
bmp.__renderTransform.translate(-bounds.x, -bounds.y);
renderer.setShader(renderer.__defaultShader);
renderer.__setRenderTarget(bitmap);
renderer.__scissorRect(null);
renderer.__renderFilterPass(bmp, renderer.__defaultDisplayShader, true);
for (filter in filters)
{
if (filter == null) continue;
bitmap = FilterRenderer.__renderGpuFilter(filter, bitmap, bitmap2, bitmap3);
}
}
function getBitmap(width:Int, height:Int):BitmapData
{
final id:String = Std.string(width) + 'x' + Std.string(height);
var bitmaps:Array<BitmapData> = bitmapPool.get(id) ?? [];
if (bitmaps.length < 1)
{
var bitmap:BitmapData = FixedBitmapData.create(width, height);
bitmaps.push(bitmap);
}
var bitmap:Null<BitmapData> = bitmaps.shift();
if (bitmap == null) throw 'The bitmap is null???? :whattheshit:';
bitmap.__fillRect(bitmap.rect, 0, true);
bitmapPool.set(id, bitmaps);
return bitmap;
}
function putBitmap(bitmap:BitmapData):Void
{
final id:String = Std.string(bitmap.width) + 'x' + Std.string(bitmap.height);
var bitmaps:Array<BitmapData> = bitmapPool.get(id) ?? [];
if (!bitmaps.contains(bitmap)) bitmaps.push(bitmap);
bitmapPool.set(id, bitmaps);
}
/**
* Clean up memory.
*/
public function destroy():Void
{
for (bitmaps in bitmapPool.iterator())
{
for (bitmap in bitmaps)
{
if (bitmap.__texture != null) bitmap.__texture.dispose();
bitmap.dispose();
}
}
}
}
@@ -216,14 +216,7 @@ class DropShadowShader extends FlxShader
*/
public function loadAltMask(path:String):Void
{
#if html5
BitmapData.loadFromFile(path).onComplete(function(bmp:BitmapData)
{
altMaskImage = bmp;
});
#else
altMaskImage = Assets.getBitmapData(path, false);
#end
}
/**
@@ -8,16 +8,9 @@ class TextureSwap extends FlxShader
public var swappedImage(default, set):BitmapData;
public var amount(default, set):Float;
public function loadSwapImage(path:String)
public function loadSwapImage(path:String):Void
{
#if html5
BitmapData.loadFromFile(path).onComplete(function(bmp:BitmapData)
{
swappedImage = bmp;
});
#else
swappedImage = BitmapData.fromFile(path);
#end
swappedImage = Assets.getBitmapData(path, false);
}
function set_swappedImage(_bitmapData:BitmapData):BitmapData
+770
View File
@@ -0,0 +1,770 @@
package funkin.group;
import flixel.util.FlxColor;
import flixel.FlxG;
import flixel.FlxSprite;
import flixel.util.FlxSort;
import funkin.util.SortUtil;
import flixel.math.FlxPoint;
/**
* A FunkinGroup of FlxSprites.
*/
typedef FunkinSpriteGroup = FunkinGroup<FlxSprite>;
/**
* FlxSpriteGroup but better. Kinda like if `FlxNestedSprite` and `FlxSpriteGroup` were merged.
*/
class FunkinGroup<T:FlxSprite> extends FlxSprite
{
/**
* The children of this FunkinGroup.
*/
public var children:Null<Array<T>>;
/**
* The size of this FunkinGroup. Read only.
*/
public var size(get, never):Int;
function get_size():Int
{
return children.length;
}
/**
* The length of this FunkinGroup. Read only.
*
* Alias to `size`.
*/
public var length(get, never):Int;
function get_length():Int
{
return size;
}
/**
* The max size of this FunkinGroup. 0 and below is infinite.
*/
public var maxSize(default, set):Int = 0;
function set_maxSize(value:Int):Int
{
if (value < 0) value = 0;
maxSize = value;
if (size > maxSize && maxSize > 0)
{
for (child in 0...size)
{
if (child > maxSize) remove(children[child]);
}
}
return maxSize;
}
/**
* The width of all the FunkinGroup's children's displays put together.
*
* Meant as a replacement of frameWidth.
*/
public var accurateWidth(get, null):Float = 0;
/**
* The height of all the FunkinGroup's children's displays put together.
*
* Meant as a replacement of frameHeight.
*/
public var accurateHeight(get, null):Float = 0;
/**
* If this is false, the FunkinGroup will update children normally. Otherwise,
* it will not (obviously).
*
* Useful for outside objects to modify this group's children. (Extending
* classes can just override updateChildren)
*
* `false` by default.
*/
public var customChildUpdate:Bool = false;
/**
* Should this FunkinGroup treat itself more like one image (in scale terms).
*
* `true` by default.
*/
public var preciseScale:Bool = true;
/**
* Should this FunkinGroup treat itself more like one image (in angle terms).
*
* `true` by default.
*/
public var preciseAngle:Bool = true;
override function get_width():Float
{
if (size < 1) return 0;
var leftMostSpr:T = sort(function(order:Int, a:T, b:T):Int
{
if (a == null || b == null) return 0;
return FlxSort.byValues(order, x + a.localX, x + b.localX);
}, false)[0];
var rightMostSpr:T = sort(function(order:Int, a:T, b:T):Int
{
if (a == null || b == null) return 0;
return FlxSort.byValues(order, (x + a.localX) + a.width, (x + b.localX) + b.width);
}, false, FlxSort.DESCENDING)[0];
return Math.abs((x + rightMostSpr.localX) + rightMostSpr.width) - (x + leftMostSpr.localX);
}
override function get_height():Float
{
if (size < 1) return 0;
var downwardsMostSpr:T = sort(function(order:Int, a:T, b:T):Int
{
if (a == null || b == null) return 0;
return FlxSort.byValues(order, (y + a.localY) + a.height, (y + b.localY) + b.height);
}, false, FlxSort.DESCENDING)[0];
var upwardsMostSpr:T = sort(function(order:Int, a:T, b:T):Int
{
if (a == null || b == null) return 0;
return FlxSort.byValues(order, y + a.localY, y + b.localY);
}, false)[0];
return Math.abs(((y + downwardsMostSpr.localY) + downwardsMostSpr.height) - (y + upwardsMostSpr.localY));
}
// include scale with frame sizes for better accuracy
function get_accurateWidth():Float
{
if (size < 1) return 0;
var leftMostSpr:T = sort(function(order:Int, a:T, b:T):Int
{
if (a == null || b == null) return 0;
return FlxSort.byValues(order, x + a.localX, x + b.localX);
}, false)[0];
var rightMostSpr:T = sort(function(order:Int, a:T, b:T):Int
{
if (a == null || b == null) return 0;
return FlxSort.byValues(order, (x + a.localX) + (a.frameWidth * a.scale.x), (x + b.localX) + (b.frameWidth * a.scale.x));
}, false, FlxSort.DESCENDING)[0];
return Math.abs(((x + rightMostSpr.localX) + (rightMostSpr.frameWidth * rightMostSpr.scale.x)) - (x + leftMostSpr.localX));
}
function get_accurateHeight():Float
{
if (size < 1) return 0;
var downwardsMostSpr:T = sort(function(order:Int, a:T, b:T):Int
{
if (a == null || b == null) return 0;
return FlxSort.byValues(order, (y + a.localY) + (a.frameHeight * a.scale.y), (y + b.localY) + (b.frameHeight * a.scale.y));
}, false, FlxSort.DESCENDING)[0];
var upwardsMostSpr:T = sort(function(order:Int, a:T, b:T):Int
{
if (a == null || b == null) return 0;
return FlxSort.byValues(order, y + a.localY, y + b.localY);
}, false)[0];
return Math.abs(((y + downwardsMostSpr.localY) + (downwardsMostSpr.frameHeight * downwardsMostSpr.scale.y)) - (y + upwardsMostSpr.localY));
}
/**
* Sets this FunkinGroup's `origin` to the center of its complete graphic.
*
* Replacement for `centerOrigin`.
*/
public function resetOrigin():Void
{
if (size < 1) return;
var upwardsMostSpr:T = sort(function(order:Int, a:T, b:T):Int
{
if (a == null || b == null) return 0;
return FlxSort.byValues(order, y + a.localY, y + b.localY);
}, false)[0];
var leftMostSpr:T = sort(function(order:Int, a:T, b:T):Int
{
if (a == null || b == null) return 0;
return FlxSort.byValues(order, x + a.localX, x + b.localX);
}, false)[0];
origin.set(leftMostSpr.localX + width / 2, upwardsMostSpr.localY + height / 2);
}
/**
* Constructor for FunkinGroup.
*
* @param x Starting X.
* @param y Starting Y.
* @param maxSize Starting max size.
* @param preciseScale Whether to treat the FunkinGroup like one image (with scale).
* @param preciseAngle Whether to treat the FunkinGroup like one image (with angle).
*/
public function new(?x:Float, ?y:Float, ?maxSize:Int, ?preciseScale:Bool, ?preciseAngle:Bool)
{
super(x, y);
children = [];
this.maxSize = maxSize ?? 0;
if (preciseScale != null) this.preciseScale = preciseScale;
if (preciseAngle != null) this.preciseAngle = preciseAngle;
}
/**
* Gets the child at an index.
*
* @param index The position.
* @return The child or null.
*/
public inline function getChildAt(index:Int):Null<T>
{
if (index < 0 || index >= size) return null;
return children[index];
}
/**
* Sets the child at an index.
*
* @param index The position.
* @param replacement A new child to replace the old one.
*/
public inline function setChildAt(index:Int, replacement:T):Void
{
if (index < 0 || index >= size) return;
children[index] = replacement;
}
override public function update(elapsed:Float):Void
{
super.update(elapsed);
updateChildren();
for (child in children)
{
if (child != null && child.exists && child.active) child.update(elapsed);
}
}
override public function draw():Void
{
for (child in children)
{
if (child != null && child.exists && child.visible) child.draw();
}
}
/**
* Updates the children here. Uses the child's local variables like `localX` and `localY` to update the child's position. Like `FlxNestedSprite`!
* Can be overriden by outside classes with `customChildUpdate`.
*/
public function updateChildren():Void
{
if (customChildUpdate) return;
for (child in children)
{
if (child != null && child.exists && child.active)
{
child.angle = angle + child.localAngle;
child.scale.x = scale.x * child.localScale.x;
child.scale.y = scale.y * child.localScale.y;
var displace = new FlxPoint(child.localX, child.localY);
var dx:Float = 0;
var dy:Float = 0;
dx = origin.x - child.width / 2;
dy = origin.y - child.height / 2;
if (preciseScale && !preciseAngle)
{
dx += scale.x * (child.localX - origin.x + child.width / 2);
dy += scale.y * (child.localY - origin.y + child.height / 2);
}
else if (preciseAngle && !preciseScale)
{
var radians:Float = angle * (Math.PI / 180);
var cos:Float = Math.cos(radians);
var sin:Float = Math.sin(radians);
dx += cos * (child.localX - origin.x + child.width / 2);
dx -= sin * (child.localY - origin.y + child.height / 2);
dy += cos * (child.localY - origin.y + child.height / 2);
dy += sin * (child.localX - origin.x + child.width / 2);
}
else if (preciseAngle && preciseScale)
{
var radians:Float = angle * (Math.PI / 180);
var cos:Float = Math.cos(radians);
var sin:Float = Math.sin(radians);
dx += scale.x * cos * (child.localX - origin.x + child.width / 2);
dx -= scale.y * sin * (child.localY - origin.y + child.height / 2);
dy += scale.y * cos * (child.localY - origin.y + child.height / 2);
dy += scale.x * sin * (child.localX - origin.x + child.width / 2);
}
if (preciseScale || preciseAngle) displace.set(dx, dy);
child.x = x + displace.x;
child.y = y + displace.y;
child.alpha = alpha * child.localAlpha;
child.visible = visible && child.localVisible;
// force child cameras to the group's cameras.
if (child.cameras != cameras) child.cameras = cameras;
}
}
}
/**
* Adds a child to this FunkinGroup. Will also return said child for convenience.
* Can't add if `size` is at `maxSize`, instead returning null.
*
* @param child The child that the caller wants this FunkinGroup to add.
* @return The same child or null.
*/
public function add(child:T):Null<T>
{
if (maxSize > 0 && size >= maxSize) return null;
children.push(child);
return child;
}
/**
* Makes a child right in this FunkinGroup. Will also return said child for convenience.
* Can't create the child if `size` is at `maxSize`, instead returning null.
*
* @return The created child or null.
*/
public function make():Null<T>
{
if (maxSize > 0 && size >= maxSize) return null;
var newChild:T = cast new FlxSprite();
children.push(newChild);
return newChild;
}
/**
* Adds a child to this FunkinGroup at a given index. Will also return said child for convenience.
*
* @param child The child that the caller wants this FunkinGroup to add.
* @param index The position the caller wants the child to go in.
* @return The same child or null.
*/
public function insert(child:T, index:Int):Null<T>
{
if (size < index) return null;
children.insert(index, child);
return child;
}
/**
* Moves select children from another FunkinGroup into this one. Only works if both FunkinGroups contain the same type.
*
* @param grp The other group to take from.
* @param children The children to move.
*/
public function move(grp:FunkinGroup<T>, children:Array<T>):Void
{
for (child in children)
{
if (grp.children.contains(child))
{
grp.remove(child);
add(child);
// update child's local position so the child stays where it was
child.localX = x - child.x;
child.localY = y - child.y;
}
}
}
override public function destroy():Void
{
for (child in children)
{
child.destroy();
}
children = null;
super.destroy();
}
/**
* Removes a child from the group, also returns it for convenience.
*
* @param child The child to remove.
* @return The removed child.
*/
public function remove(child:T):Null<T>
{
var index = children.indexOf(child);
if (index != -1) children.splice(index, 1);
return child;
}
/**
* Applies a function to all children.
*
* @param func A function that modifies one child at a time.
*/
public function forEach(func:T->Void):Void
{
for (child in children)
{
if (child != null)
{
func(child);
}
}
}
/**
* Sorts the children of this FunkinGroup. Returns the sorted children
*
* @param func The sorting function to use - you can use one of the premade ones in
* `FlxSort` or write your own using `FlxSort.byValues()` as a "backend".
* @param setGroup Whether to actually sort the children of this group,
* so the caller can grab a sorted list without this group
* actually sorting the children.
* @param order A constant that defines the sort order.
* Possible values are `FlxSort.ASCENDING` (default) and `FlxSort.DESCENDING`.
* @return The sorted children list.
*/
public inline function sort(func:(Int, T, T) -> Int, setGroup:Bool = true, order = FlxSort.ASCENDING):Null<Array<T>>
{
if (setGroup)
{
children.sort(func.bind(order));
return children;
}
else
{
var fakeKids = children.copy();
fakeKids.sort(func.bind(order));
return fakeKids;
}
}
/**
* Refreshes the group, by redoing the render order of all children.
* It does this based on the `zIndex` of each child.
*/
public function refresh():Void
{
sort(SortUtil.byZIndex);
}
/**
* Get's the first alive child under this FunkinGroup. Returns null if it can't
* find squat.
*
* @return The alive child or null.
*/
public inline function getFirstAlive():Null<T>
{
for (child in children)
{
if (child.exists && child.alive) return child;
}
return null;
}
/**
* Get's the first dead child under this FunkinGroup. Returns null if it can't
* find squat.
* getFirstAlive's evil twin.
*
* @return The dead child or null.
*/
public inline function getFirstDead():Null<T>
{
for (child in children)
{
if (!child.alive) return child;
}
return null;
}
/**
* Counts the amount of alive children in this FunkinGroup.
*
* @return The alive child number or null.
*/
public inline function countLiving():Int
{
var i = 0;
for (child in children)
{
if (child.alive) i++;
}
return i;
}
/**
* Counts the amount of dead children in this FunkinGroup.
*
* @return The dead child number or null.
*/
public inline function countDead():Int
{
var i = 0;
for (child in children)
{
if (!child.alive) i++;
}
return i;
}
/**
* Gets the first nonexistent child in the family and returns it.
* Good for recycling.
*
* @return The child or null.
*/
public inline function getFirstAvailable():Null<T>
{
for (child in children)
{
if (!child.exists) return child;
}
return null;
}
/**
* Gets the index of the first null child under this FunkinGroup.
* -1 means it failed
*
* @return The index.
*/
public inline function getFirstNull():Int
{
for (child in 0...size)
{
if (children[child] == null) return child;
}
return -1;
}
/**
* Gets the first existing child in the family and returns it.
* Good for recycling.
*
* @return The child or null.
*/
public inline function getFirstExisting():Null<T>
{
for (child in children)
{
if (child.exists) return child;
}
return null;
}
/**
* Gets a random child from this FunkinGroup.
* @param startIndex Optional offset off the front of the array.
* Default value is `0`, or the beginning of the array.
* @param length Optional restriction on the number of values you want to randomly select from.
* @return A child or null.
*/
public inline function getRandom(startIndex:Int = 0, length:Int = 0):Null<T>
{
if (size <= 0) return null;
if (startIndex < 0) startIndex = 0;
if (length <= 0) length = size;
return FlxG.random.getObject(children, startIndex, length);
}
/**
* Brings a child back from the graveyard.
*
* @param child The child to revive.
*/
public function reviveChild(child:T):Void
{
if (child != null) child.revive();
}
/**
* Kills all the children and then itself.
* Revive this group via `revive()`.
*/
override public function kill():Void
{
for (child in children)
{
if (child != null) child.kill();
}
super.kill();
}
/**
* Revives all the children and then itself.
*/
override public function revive():Void
{
for (child in children)
{
if (child != null) child.revive();
}
super.revive();
}
override public function clone():FunkinGroup<T>
{
var group = new FunkinGroup<T>(x, y, maxSize);
for (child in children)
{
group.add(cast child.clone());
}
return group;
}
// =============================================================
// Unavailable functions that won't work with `FunkinGroup`.
// =============================================================
/**
* This functionality isn't supported in `FunkinGroup`.
* @return this group
*/
override public function makeGraphic(Width:Int, Height:Int, Color:Int = FlxColor.WHITE, Unique:Bool = false, ?Key:String):FlxSprite
{
#if FLX_DEBUG
throw "This function is not supported in FunkinGroup";
#end
return this;
}
/**
* This functionality isn't supported in `FunkinGroup`.
* @return this group
*/
override public function loadGraphicFromSprite(Sprite:FlxSprite):FlxSprite
{
#if FLX_DEBUG
throw "This function is not supported in FunkinGroup";
#end
return this;
}
/**
* This functionality isn't supported in `FunkinGroup`.
* @return this group
*/
override public function loadGraphic(Graphic:flixel.system.FlxAssets.FlxGraphicAsset, Animated:Bool = false, Width:Int = 0, Height:Int = 0,
Unique:Bool = false, ?Key:String):FlxSprite
{
return this;
}
/**
* This functionality isn't supported in `FunkinGroup`.
* @return this group
*/
override public function loadRotatedGraphic(Graphic:flixel.system.FlxAssets.FlxGraphicAsset, Rotations:Int = 16, Frame:Int = -1, AntiAliasing:Bool = false,
AutoBuffer:Bool = false, ?Key:String):FlxSprite
{
#if FLX_DEBUG
throw "This function is not supported in FunkinGroup";
#end
return this;
}
override function set_pixels(Value:openfl.display.BitmapData):openfl.display.BitmapData
{
return Value;
}
override function set_frame(Value:flixel.graphics.frames.FlxFrame):flixel.graphics.frames.FlxFrame
{
return Value;
}
override function get_pixels():openfl.display.BitmapData
{
return null;
}
/**
* Internal function to update the current animation frame.
*
* @param RunOnCpp Whether the frame should also be recalculated if we're on a non-flash target
*/
override inline function calcFrame(RunOnCpp:Bool = false):Void
{
// Nothing to do here
}
/**
* This functionality isn't supported in `FunkinGroup`.
*/
override inline function resetHelpers():Void
{
}
/**
* This functionality isn't supported in `FunkinGroup`.
*/
override public inline function stamp(Brush:FlxSprite, X:Int = 0, Y:Int = 0):Void
{
}
override function set_frames(Frames:flixel.graphics.frames.FlxFramesCollection):flixel.graphics.frames.FlxFramesCollection
{
return Frames;
}
/**
* This functionality isn't supported in `FunkinGroup`.
*/
override inline function updateColorTransform():Void
{
}
}
@@ -0,0 +1,15 @@
package funkin.group;
/**
* A script that can be tied to a FunkinGroup.
* Create a scripted class that extends FunkinGroup to use this.
*/
@:hscriptClass
class ScriptedFunkinGroup extends funkin.group.FunkinGroup<Dynamic> implements polymod.hscript.HScriptedClass {}
/**
* A script that can be tied to a FunkinSpriteGroup.
* Create a scripted class that extends FunkinSpriteGroup to use this.
*/
@:hscriptClass
class ScriptedFunkinSpriteGroup extends funkin.group.FunkinGroup.FunkinSpriteGroup implements polymod.hscript.HScriptedClass {}
+37 -8
View File
@@ -1,5 +1,6 @@
package funkin.input;
import funkin.util.InputUtil;
import flixel.input.gamepad.FlxGamepad;
import flixel.util.FlxDirectionFlags;
import flixel.input.FlxInput.FlxInputState;
@@ -381,22 +382,22 @@ class Controls extends FlxActionSet
public function getDialogueName(action:FlxActionDigital, ?ignoreSurrounding:Bool = false):String
{
if (action.inputs.length == 0) return 'N/A';
var input = action.inputs[0];
var input = FunkinAction.lastDeviceUsed == KEYBOARD ? action.inputs[0] : action.inputs[1];
if (ignoreSurrounding == false)
{
return switch (input.device)
return switch (FunkinAction.lastDeviceUsed)
{
case KEYBOARD: return '[${(input.inputID : FlxKey)}]';
case GAMEPAD: return '(${(input.inputID : FlxGamepadInputID)})';
case KEYBOARD: return '[${InputUtil.format(input.inputID, Keys).toUpperCase()}]';
case GAMEPAD: return '(${InputUtil.format(input.inputID, Gamepad(input.deviceID)).toUpperCase()})';
case device: throw 'unhandled device: $device';
}
}
else
{
return switch (input.device)
return switch (FunkinAction.lastDeviceUsed)
{
case KEYBOARD: return '${(input.inputID : FlxKey)}';
case GAMEPAD: return '${(input.inputID : FlxGamepadInputID)}';
case KEYBOARD: return InputUtil.format(input.inputID, Keys).toUpperCase();
case GAMEPAD: return InputUtil.format(input.inputID, Gamepad(input.deviceID)).toUpperCase();
case device: throw 'unhandled device: $device';
}
}
@@ -1180,7 +1181,7 @@ class FunkinAction extends FlxActionDigital
public var nameReleased(default, null):Null<String>;
var cache:Map<String,
{timestamp:Int, value:Bool}> = [];
{timestamp:Float, value:Bool}> = [];
public function new(?name:String = "", ?namePressed:String, ?nameReleased:String)
{
@@ -1188,6 +1189,8 @@ class FunkinAction extends FlxActionDigital
this.namePressed = namePressed;
this.nameReleased = nameReleased;
updateLastDeviceUsed();
}
/**
@@ -1203,6 +1206,7 @@ class FunkinAction extends FlxActionDigital
*/
public function checkPressed():Bool
{
if (checkFiltered(PRESSED)) updateLastDeviceUsed();
return checkFiltered(PRESSED);
}
@@ -1211,6 +1215,7 @@ class FunkinAction extends FlxActionDigital
*/
public function checkJustPressed():Bool
{
if (checkFiltered(JUST_PRESSED)) updateLastDeviceUsed();
return checkFiltered(JUST_PRESSED);
}
@@ -1227,6 +1232,7 @@ class FunkinAction extends FlxActionDigital
*/
public function checkJustReleased():Bool
{
if (checkFiltered(JUST_RELEASED)) updateLastDeviceUsed();
return checkFiltered(JUST_RELEASED);
}
@@ -1353,6 +1359,29 @@ class FunkinAction extends FlxActionDigital
return triggered;
}
public static var lastDeviceUsed:FlxInputDevice;
/**
* Checks which is the last device you have used and stores the value in `lastDeviceUsed`.
*/
public function updateLastDeviceUsed()
{
if (FlxG.keys.pressed.ANY)
{
lastDeviceUsed = FlxInputDevice.KEYBOARD;
return;
}
if (FlxG.gamepads.lastActive != null)
{
lastDeviceUsed = FlxInputDevice.GAMEPAD;
return;
}
// Default value (just in case everything's null somehow)
lastDeviceUsed = FlxInputDevice.KEYBOARD;
}
}
/**
+58 -354
View File
@@ -156,384 +156,87 @@ class Cursor
if (value != null && cursorMode != value)
{
cursorMode = value;
setCursorGraphic(cursorMode);
loadCursorGraphicSync(cursorMode);
}
return cursorMode;
}
/**
* Synchronous.
* Loads the cursor graphic synchronously.
*
* @param value The cursor mode to load. If null, the cursor is unloaded.
*/
static function setCursorGraphic(?value:CursorMode = null):Void
static function loadCursorGraphicSync(?value:CursorMode = null):Void
{
if (value == null)
{
FlxG.mouse.unload();
return;
}
switch (value)
{
case Default:
if (assetCursorDefault == null)
{
var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_DEFAULT_PARAMS.graphic);
assetCursorDefault = bitmapData;
applyCursorParams(assetCursorDefault, CURSOR_DEFAULT_PARAMS);
}
else
{
applyCursorParams(assetCursorDefault, CURSOR_DEFAULT_PARAMS);
}
case Cross:
if (assetCursorCross == null)
{
var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_CROSS_PARAMS.graphic);
assetCursorCross = bitmapData;
applyCursorParams(assetCursorCross, CURSOR_CROSS_PARAMS);
}
else
{
applyCursorParams(assetCursorCross, CURSOR_CROSS_PARAMS);
}
case Eraser:
if (assetCursorEraser == null)
{
var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_ERASER_PARAMS.graphic);
assetCursorEraser = bitmapData;
applyCursorParams(assetCursorEraser, CURSOR_ERASER_PARAMS);
}
else
{
applyCursorParams(assetCursorEraser, CURSOR_ERASER_PARAMS);
}
case Grabbing:
if (assetCursorGrabbing == null)
{
var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_GRABBING_PARAMS.graphic);
assetCursorGrabbing = bitmapData;
applyCursorParams(assetCursorGrabbing, CURSOR_GRABBING_PARAMS);
}
else
{
applyCursorParams(assetCursorGrabbing, CURSOR_GRABBING_PARAMS);
}
case Hourglass:
if (assetCursorHourglass == null)
{
var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_HOURGLASS_PARAMS.graphic);
assetCursorHourglass = bitmapData;
applyCursorParams(assetCursorHourglass, CURSOR_HOURGLASS_PARAMS);
}
else
{
applyCursorParams(assetCursorHourglass, CURSOR_HOURGLASS_PARAMS);
}
case Pointer:
if (assetCursorPointer == null)
{
var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_POINTER_PARAMS.graphic);
assetCursorPointer = bitmapData;
applyCursorParams(assetCursorPointer, CURSOR_POINTER_PARAMS);
}
else
{
applyCursorParams(assetCursorPointer, CURSOR_POINTER_PARAMS);
}
case Text:
if (assetCursorText == null)
{
var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_TEXT_PARAMS.graphic);
assetCursorText = bitmapData;
applyCursorParams(assetCursorText, CURSOR_TEXT_PARAMS);
}
else
{
applyCursorParams(assetCursorText, CURSOR_TEXT_PARAMS);
}
case ZoomIn:
if (assetCursorZoomIn == null)
{
var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_ZOOM_IN_PARAMS.graphic);
assetCursorZoomIn = bitmapData;
applyCursorParams(assetCursorZoomIn, CURSOR_ZOOM_IN_PARAMS);
}
else
{
applyCursorParams(assetCursorZoomIn, CURSOR_ZOOM_IN_PARAMS);
}
case ZoomOut:
if (assetCursorZoomOut == null)
{
var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_ZOOM_OUT_PARAMS.graphic);
assetCursorZoomOut = bitmapData;
applyCursorParams(assetCursorZoomOut, CURSOR_ZOOM_OUT_PARAMS);
}
else
{
applyCursorParams(assetCursorZoomOut, CURSOR_ZOOM_OUT_PARAMS);
}
case Crosshair:
if (assetCursorCrosshair == null)
{
var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_CROSSHAIR_PARAMS.graphic);
assetCursorCrosshair = bitmapData;
applyCursorParams(assetCursorCrosshair, CURSOR_CROSSHAIR_PARAMS);
}
else
{
applyCursorParams(assetCursorCrosshair, CURSOR_CROSSHAIR_PARAMS);
}
case Cell:
if (assetCursorCell == null)
{
var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_CELL_PARAMS.graphic);
assetCursorCell = bitmapData;
applyCursorParams(assetCursorCell, CURSOR_CELL_PARAMS);
}
else
{
applyCursorParams(assetCursorCell, CURSOR_CELL_PARAMS);
}
case Scroll:
if (assetCursorScroll == null)
{
var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_SCROLL_PARAMS.graphic);
assetCursorScroll = bitmapData;
applyCursorParams(assetCursorScroll, CURSOR_SCROLL_PARAMS);
}
else
{
applyCursorParams(assetCursorScroll, CURSOR_SCROLL_PARAMS);
}
default:
setCursorGraphic(null);
}
applyCursorParams(value);
}
/**
* Asynchronous.
* Loads the cursor graphic asynchronously.
*
* @param value The cursor mode to load. If null, the cursor is unloaded.
*/
static function loadCursorGraphic(?value:CursorMode = null):Void
static function loadCursorGraphicAsync(?value:CursorMode = null):Void
{
if (value == null)
applyCursorParams(value, true);
}
static function applyCursorParams(mode:Null<CursorMode>, async:Bool = false)
{
if (mode == null)
{
FlxG.mouse.unload();
return;
}
switch (value)
var data = switch (mode)
{
case Default:
if (assetCursorDefault == null)
{
var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_DEFAULT_PARAMS.graphic);
future.onComplete(function(bitmapData:BitmapData)
{
assetCursorDefault = bitmapData;
applyCursorParams(assetCursorDefault, CURSOR_DEFAULT_PARAMS);
});
future.onError(onCursorError.bind(Default));
}
else
{
applyCursorParams(assetCursorDefault, CURSOR_DEFAULT_PARAMS);
}
case Default: { cache: assetCursorDefault, params: CURSOR_DEFAULT_PARAMS, set: (bmp) -> assetCursorDefault = bmp };
case Cross: { cache: assetCursorCross, params: CURSOR_CROSS_PARAMS, set: (bmp) -> assetCursorCross = bmp };
case Eraser: { cache: assetCursorEraser, params: CURSOR_ERASER_PARAMS, set: (bmp) -> assetCursorEraser = bmp };
case Grabbing: { cache: assetCursorGrabbing, params: CURSOR_GRABBING_PARAMS, set: (bmp) -> assetCursorGrabbing = bmp };
case Hourglass: { cache: assetCursorHourglass, params: CURSOR_HOURGLASS_PARAMS, set: (bmp) -> assetCursorHourglass = bmp };
case Pointer: { cache: assetCursorPointer, params: CURSOR_POINTER_PARAMS, set: (bmp) -> assetCursorPointer = bmp };
case Text: { cache: assetCursorText, params: CURSOR_TEXT_PARAMS, set: (bmp) -> assetCursorText = bmp };
case TextVertical: { cache: assetCursorTextVertical, params: CURSOR_TEXT_VERTICAL_PARAMS, set: (bmp) -> assetCursorTextVertical = bmp };
case ZoomIn: { cache: assetCursorZoomIn, params: CURSOR_ZOOM_IN_PARAMS, set: (bmp) -> assetCursorZoomIn = bmp };
case ZoomOut: { cache: assetCursorZoomOut, params: CURSOR_ZOOM_OUT_PARAMS, set: (bmp) -> assetCursorZoomOut = bmp };
case Crosshair: { cache: assetCursorCrosshair, params: CURSOR_CROSSHAIR_PARAMS, set: (bmp) -> assetCursorCrosshair = bmp };
case Cell: { cache: assetCursorCell, params: CURSOR_CELL_PARAMS, set: (bmp) -> assetCursorCell = bmp };
case Scroll: { cache: assetCursorScroll, params: CURSOR_SCROLL_PARAMS, set: (bmp) -> assetCursorScroll = bmp };
default: null;
}
case Cross:
if (assetCursorCross == null)
{
var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_CROSS_PARAMS.graphic);
future.onComplete(function(bitmapData:BitmapData)
{
assetCursorCross = bitmapData;
applyCursorParams(assetCursorCross, CURSOR_CROSS_PARAMS);
});
future.onError(onCursorError.bind(Cross));
}
else
{
applyCursorParams(assetCursorCross, CURSOR_CROSS_PARAMS);
}
if (data == null)
{
FlxG.mouse.unload();
return;
}
case Eraser:
if (assetCursorEraser == null)
{
var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_ERASER_PARAMS.graphic);
future.onComplete(function(bitmapData:BitmapData)
{
assetCursorEraser = bitmapData;
applyCursorParams(assetCursorEraser, CURSOR_ERASER_PARAMS);
});
future.onError(onCursorError.bind(Eraser));
}
else
{
applyCursorParams(assetCursorEraser, CURSOR_ERASER_PARAMS);
}
if (data.cache != null)
{
applyGraphic(data.cache, data.params);
return;
}
case Grabbing:
if (assetCursorGrabbing == null)
{
var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_GRABBING_PARAMS.graphic);
future.onComplete(function(bitmapData:BitmapData)
{
assetCursorGrabbing = bitmapData;
applyCursorParams(assetCursorGrabbing, CURSOR_GRABBING_PARAMS);
});
future.onError(onCursorError.bind(Grabbing));
}
else
{
applyCursorParams(assetCursorGrabbing, CURSOR_GRABBING_PARAMS);
}
case Hourglass:
if (assetCursorHourglass == null)
{
var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_HOURGLASS_PARAMS.graphic);
future.onComplete(function(bitmapData:BitmapData)
{
assetCursorHourglass = bitmapData;
applyCursorParams(assetCursorHourglass, CURSOR_HOURGLASS_PARAMS);
});
future.onError(onCursorError.bind(Hourglass));
}
else
{
applyCursorParams(assetCursorHourglass, CURSOR_HOURGLASS_PARAMS);
}
case Pointer:
if (assetCursorPointer == null)
{
var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_POINTER_PARAMS.graphic);
future.onComplete(function(bitmapData:BitmapData)
{
assetCursorPointer = bitmapData;
applyCursorParams(assetCursorPointer, CURSOR_POINTER_PARAMS);
});
future.onError(onCursorError.bind(Pointer));
}
else
{
applyCursorParams(assetCursorPointer, CURSOR_POINTER_PARAMS);
}
case Text:
if (assetCursorText == null)
{
var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_TEXT_PARAMS.graphic);
future.onComplete(function(bitmapData:BitmapData)
{
assetCursorText = bitmapData;
applyCursorParams(assetCursorText, CURSOR_TEXT_PARAMS);
});
future.onError(onCursorError.bind(Text));
}
else
{
applyCursorParams(assetCursorText, CURSOR_TEXT_PARAMS);
}
case ZoomIn:
if (assetCursorZoomIn == null)
{
var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_ZOOM_IN_PARAMS.graphic);
future.onComplete(function(bitmapData:BitmapData)
{
assetCursorZoomIn = bitmapData;
applyCursorParams(assetCursorZoomIn, CURSOR_ZOOM_IN_PARAMS);
});
future.onError(onCursorError.bind(ZoomIn));
}
else
{
applyCursorParams(assetCursorZoomIn, CURSOR_ZOOM_IN_PARAMS);
}
case ZoomOut:
if (assetCursorZoomOut == null)
{
var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_ZOOM_OUT_PARAMS.graphic);
future.onComplete(function(bitmapData:BitmapData)
{
assetCursorZoomOut = bitmapData;
applyCursorParams(assetCursorZoomOut, CURSOR_ZOOM_OUT_PARAMS);
});
future.onError(onCursorError.bind(ZoomOut));
}
else
{
applyCursorParams(assetCursorZoomOut, CURSOR_ZOOM_OUT_PARAMS);
}
case Crosshair:
if (assetCursorCrosshair == null)
{
var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_CROSSHAIR_PARAMS.graphic);
future.onComplete(function(bitmapData:BitmapData)
{
assetCursorCrosshair = bitmapData;
applyCursorParams(assetCursorCrosshair, CURSOR_CROSSHAIR_PARAMS);
});
future.onError(onCursorError.bind(Crosshair));
}
else
{
applyCursorParams(assetCursorCrosshair, CURSOR_CROSSHAIR_PARAMS);
}
case Cell:
if (assetCursorCell == null)
{
var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_CELL_PARAMS.graphic);
future.onComplete(function(bitmapData:BitmapData)
{
assetCursorCell = bitmapData;
applyCursorParams(assetCursorCell, CURSOR_CELL_PARAMS);
});
future.onError(onCursorError.bind(Cell));
}
else
{
applyCursorParams(assetCursorCell, CURSOR_CELL_PARAMS);
}
case Scroll:
if (assetCursorScroll == null)
{
var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_SCROLL_PARAMS.graphic);
future.onComplete(function(bitmapData:BitmapData)
{
assetCursorScroll = bitmapData;
applyCursorParams(assetCursorScroll, CURSOR_SCROLL_PARAMS);
});
future.onError(onCursorError.bind(Scroll));
}
else
{
applyCursorParams(assetCursorScroll, CURSOR_SCROLL_PARAMS);
}
default:
loadCursorGraphic(null);
if (async)
{
var future:Future<BitmapData> = Assets.loadBitmapData(data.params.graphic);
future.onComplete((bmp:BitmapData) -> {
data.set(bmp);
applyGraphic(bmp, data.params);
});
future.onError(onCursorError.bind(mode));
}
else
{
var bitmapData:BitmapData = Assets.getBitmapData(data.params.graphic);
data.set(bitmapData);
applyGraphic(bitmapData, data.params);
}
}
static inline function applyCursorParams(graphic:BitmapData, params:CursorParams):Void
private static inline function applyGraphic(graphic:BitmapData, params:CursorParams):Void
{
FlxG.mouse.load(graphic, params.scale, params.offsetX, params.offsetY);
}
@@ -580,6 +283,7 @@ enum CursorMode
Hourglass;
Pointer;
Text;
TextVertical;
ZoomIn;
ZoomOut;
Crosshair;
+3 -25
View File
@@ -133,11 +133,6 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
};
}
/**
* Convert from int to Int64.
*/
static final NS_PER_MS:Int64 = Constants.NS_PER_MS;
/**
* Returns a precise timestamp, measured in nanoseconds.
* Timestamp is only useful for comparing against other timestamps.
@@ -151,11 +146,11 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
// NOTE: This timestamp isn't that precise on standard HTML5 builds.
// This is because of browser safeguards against timing attacks.
// See https://web.dev/coop-coep to enable headers which allow for more precise timestamps.
return haxe.Int64.fromFloat(js.Browser.window.performance.now()) * NS_PER_MS;
#elseif cpp
return haxe.Int64.fromFloat(js.Browser.window.performance.now()) * Constants.NS_PER_MS;
#elseif lime_cffi
// NOTE: If the game hard crashes on this line, rebuild Lime!
// `lime rebuild windows -clean`
return lime._internal.backend.native.NativeCFFI.lime_sdl_get_ticks() * NS_PER_MS;
return Int64.fromFloat(lime._internal.backend.native.NativeCFFI.lime_system_get_timer());
#else
throw "Eric didn't implement precise timestamps on this platform!";
#end
@@ -290,11 +285,6 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
var key:FlxKey = convertKeyCode(keyCode);
if (_keyList.indexOf(key) == -1) return;
// TODO: Remove this line with SDL3 when timestamps change meaning.
// This is because SDL3's timestamps are measured in nanoseconds, not milliseconds.
timestamp *= Constants.NS_PER_MS; // 18126000000 38367000000
// timestamp -= globalOffset * Constants.NS_PER_MS;
// trace(timestamp);
updateKeyStates(key, true);
if (getInputByKey(key)?.justPressed ?? false)
@@ -313,10 +303,6 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
var key:FlxKey = convertKeyCode(keyCode);
if (_keyList.indexOf(key) == -1) return;
// TODO: Remove this line with SDL3 when timestamps change meaning.
// This is because SDL3's timestamps ar e measured in nanoseconds, not milliseconds.
timestamp *= Constants.NS_PER_MS;
updateKeyStates(key, false);
if (getInputByKey(key)?.justReleased ?? false)
@@ -337,10 +323,6 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
var buttonListEntry = _buttonList.get(gamepad.id);
if (buttonListEntry == null || buttonListEntry.indexOf(buttonId) == -1) return;
// TODO: Remove this line with SDL3 when timestamps change meaning.
// This is because SDL3's timestamps ar e measured in nanoseconds, not milliseconds.
timestamp *= Constants.NS_PER_MS;
updateButtonStates(gamepad, buttonId, true);
if (getInputByButton(gamepad, buttonId)?.justPressed ?? false)
@@ -361,10 +343,6 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
var buttonListEntry = _buttonList.get(gamepad.id);
if (buttonListEntry == null || buttonListEntry.indexOf(buttonId) == -1) return;
// TODO: Remove this line with SDL3 when timestamps change meaning.
// This is because SDL3's timestamps ar e measured in nanoseconds, not milliseconds.
timestamp *= Constants.NS_PER_MS;
updateButtonStates(gamepad, buttonId, false);
if (getInputByButton(gamepad, buttonId)?.justReleased ?? false)
+4
View File
@@ -97,6 +97,10 @@ class AdMobUtil
Admob.configureUnity(Admob.getTCFConsentForPurpose(0) == 1, StringTools.startsWith(Admob.getUSPrivacy(), '1Y'));
Admob.configurePangle(Admob.getTCFConsentForPurpose(0) == 1, StringTools.startsWith(Admob.getUSPrivacy(), '1Y'));
Admob.configureVungle(StringTools.startsWith(Admob.getUSPrivacy(), '1Y'));
Admob.init(#if TESTING_ADS true #else false #end);
}
@@ -1,7 +1,7 @@
package funkin.mobile.util;
#if ios
import funkin.external.apple.FNFCUtil as NativeFNFCUtil;
import funkin.external.apple.FNFCExtern;
#end
#if android
import funkin.external.android.JNIUtil;
@@ -13,7 +13,7 @@ import flixel.util.FlxSignal;
/**
* A class for handling the flow of loading FNFC song packs on mobile.
*/
class FNFCUtil
class FNFCProvider
{
public static var onFNFCOpen:FlxTypedSignal<String->Void>;
@@ -22,10 +22,9 @@ class FNFCUtil
onFNFCOpen = new FlxTypedSignal<String->Void>();
#if ios
FlxG.stage.window.onDropFile.add(function(_)
FlxG.stage.window.onDropFile.add(function(path:String, state:String, x:Float, y:Float):Void
{
final url = queryFNFC();
if (url != null) getFNFCFromURL(url);
queryFNFC();
});
#elseif android
CallbackUtil.onFNFCOpen.add(onFNFCOpen.dispatch);
@@ -52,7 +51,7 @@ class FNFCUtil
private static function getFNFCFromURL(url:String):Void
{
var cURL:cpp.ConstCharStar = cast url;
NativeFNFCUtil.copyFNFCIntoCache(cURL, cpp.Callable.fromStaticFunction(fnfcCallback));
FNFCExtern.copyFNFCIntoCache(cURL, cpp.Callable.fromStaticFunction(fnfcCallback));
}
@:noCompletion
+11 -15
View File
@@ -5,8 +5,9 @@ import funkin.external.apple.ScreenUtil as NativeScreenUtil;
#elseif android
import funkin.external.android.ScreenUtil as NativeScreenUtil;
#end
import lime.math.Rectangle;
import lime.system.System;
import lime.app.Application;
import openfl.geom.Rectangle;
/**
* A Utility class to get mobile screen related informations.
@@ -22,6 +23,9 @@ class ScreenUtil
{
final notchRect:Rectangle = new Rectangle();
notchRect.x = 0.0;
notchRect.y = 0.0;
#if android
final rectDimensions:Array<Array<Float>> = [[], [], [], []];
@@ -59,32 +63,24 @@ class ScreenUtil
var bottomInset:Float = -1;
var deviceWidth:Float = -1;
var deviceHeight:Float = -1;
var displayOrientation:DisplayOrientation = System.getDisplayOrientation(0);
NativeScreenUtil.getSafeAreaInsets(cpp.RawPointer.addressOf(topInset), cpp.RawPointer.addressOf(bottomInset), cpp.RawPointer.addressOf(leftInset),
cpp.RawPointer.addressOf(rightInset));
NativeScreenUtil.getScreenSize(cpp.RawPointer.addressOf(deviceWidth), cpp.RawPointer.addressOf(deviceHeight));
notchRect.x = 0;
notchRect.y = 0.0;
// Calculate the rectangle dimensions for the notch
// Note: iOS only spits out *insets* for "safe areas", so we can only get a broad position for the notch
// left + right insets are the same, so we can use either
// Note: *inset* is the distance from the edge of the screen where a safe area gets defined
// see: https://developer.apple.com/documentation/uikit/uiview/safeareainsets
switch (displayOrientation)
switch (System.getDisplayOrientation(Application.current.window.display))
{
case DISPLAY_ORIENTATION_LANDSCAPE: // landscape
notchRect.width = leftInset + rightInset;
notchRect.height = bottomInset - topInset;
notchRect.y = topInset;
notchRect.width = leftInset;
notchRect.height = deviceHeight;
case DISPLAY_ORIENTATION_LANDSCAPE_FLIPPED: // landscape
notchRect.width = leftInset + rightInset;
notchRect.height = bottomInset - topInset;
notchRect.y = topInset;
notchRect.x = deviceWidth - notchRect.width; // move notchRect if we are flipped, notch is at the right of screen
notchRect.width = leftInset;
notchRect.height = deviceHeight;
notchRect.x = deviceWidth - rightInset;
case DISPLAY_ORIENTATION_PORTRAIT: // portrait
notchRect.width = deviceWidth;
notchRect.height = topInset;
+186 -47
View File
@@ -1,6 +1,6 @@
package funkin.modding;
import polymod.Polymod;
import polymod.Polymod.PolymodError;
@:nullSafety
class PolymodErrorHandler
@@ -10,67 +10,206 @@ class PolymodErrorHandler
// Perform an action based on the error code.
switch (error.code)
{
case FRAMEWORK_INIT, FRAMEWORK_AUTODETECT, SCRIPT_PARSING:
// Unimportant.
return;
//
// Mod Metadata Parsing Errors
//
case MOD_LOAD_PREPARE, MOD_LOAD_DONE:
logInfo('LOADING MOD - ${error.message}');
case MISSING_ICON:
logWarn('A mod is missing an icon. Please add one.');
case SCRIPT_PARSE_ERROR:
// A syntax error when parsing a script.
logError(error.message);
case MOD_MISSING_DIRECTORY:
// A mod directory was included in the list of mods to load, but it isn't installed.
trace(' WARNING '.warning() + 'Tried to load a mod that was not installed: ${error.message}');
// Notify the user via popup.
funkin.util.WindowUtil.showError('Polymod Script Parsing Error', error.message);
funkin.util.WindowUtil.showError('Mod Load Error', error.message);
case MOD_MISSING_ID:
// A mod ID was included in the list of mods to load, but it isn't installed.
trace(' WARNING '.warning() + ' Tried to load a mod that was not installed: ${error.message}');
// Notify the user via popup.
funkin.util.WindowUtil.showError('Mod Load Error', error.message);
case MOD_MISSING_METADATA:
// A mod ID was included in the list of mods to load, but the mod folder doesn't have metadata.
trace(' ERROR '.error() + ' Tried to load a mod with no metadata: ${error.message}');
// Notify the user via popup.
funkin.util.WindowUtil.showError('Mod Load Error', error.message);
case MOD_METADATA_PARSE_FAILED:
// Polymod tries to load the mod's metadata, but it could not be parsed.
trace(' ERROR '.error() + ' Failed to parse mod metadata: ${error.message}');
// Notify the user via popup.
funkin.util.WindowUtil.showError('Mod Metadata Parse Error', error.message);
case MOD_VERSION_PARSE_FAILED:
// Polymod tries to load the mod's version, but it could not be parsed.
trace(' ERROR '.error() + ' Failed to parse mod version: ${error.message}');
// Notify the user via popup.
funkin.util.WindowUtil.showError('Mod Version Parse Error', error.message);
case MOD_API_VERSION_PARSE_FAILED:
// Polymod tries to load the mod's API version, but it could not be parsed.
trace(' ERROR '.error() + ' Failed to parse mod API version: ${error.message}');
// Notify the user via popup.
funkin.util.WindowUtil.showError('Mod API Version Parse Error', error.message);
case MOD_MISSING_ICON:
// A mod is missing an icon.
trace(' WARNING '.warning() + ' A mod is missing an icon: ${error.message}');
//
// Mod Loading Errors
//
case MOD_API_VERSION_MISMATCH:
// Polymod tried to load a mod but failed because it has the wrong API version.
// This is an important issue that requires user attention so it can be resolved by the mod developer.
trace(' WARNING '.warning() + ' Failed to load mod - ${error.message}');
var regex:EReg = ~/Mod "([-_a-zA-Z0-9]+)" is not compatible with API version "(.*?)", got "(.*?)"/;
if (regex.match(error.message))
{
// Notify the user via formatted popup.
var modId:String = regex.matched(1);
// var apiVersion:String = regex.matched(2);
var modVersion:String = regex.matched(3);
var message:String = 'Installed mod "$modId" was built for modding version "v$modVersion". It is not compatible with game version ${Constants.GENERATED_BY}, and must be skipped.'
+ '\n\nPlease inform the mod developer that "$modId" must be updated for compatibility.';
funkin.util.WindowUtil.showError('Mod Outdated', message);
}
else
{
// Notify the user via standard popup.
funkin.util.WindowUtil.showError('Mod Outdated', error.message);
}
case MOD_LOAD_FAILED:
// Polymod failed to load a mod. A different error would have already been logged.
trace(' WARNING '.warning() + ' Failed to load mod - ${error.message}');
case MOD_LOAD_DONE:
trace(' INFO '.info() + ' Loaded mod - ${error.message}');
//
// Mod Dependency Errors
//
case MOD_OPTIONAL_DEPENDENCY_UNMET:
// Polymod looked at the list of available mods, and one of them was missing an optional dependency.
trace(' INFO '.info() + ' Installed mod is missing an optional dependency: ${error.message}');
case MOD_DEPENDENCY_UNMET:
switch (error.origin)
{
case SCAN:
// Polymod looked at the list of available mods, and one of them was missing a dependency.
// This is an important warning that should be shown to the user.
trace(' WARNING '.warning() + ' Installed mod is missing a dependency: ${error.message}');
// Notify the user via popup.
funkin.util.WindowUtil.showError('Mod Dependency Error', error.message);
default:
// Polymod tried to load a mod, but failed because it has a dependency that wasn't met.
// This is a major error that should be shown to the user.
trace(' ERROR '.error() + ' Failed to load mod due to missing dependency: ${error.message}');
// Notify the user via popup.
funkin.util.WindowUtil.showError('Mod Dependency Error', error.message);
}
case MOD_DEPENDENCY_VERSION_MISMATCH:
switch (error.origin)
{
case SCAN:
// Polymod looked at the list of available mods, and one of them has a dependency on a different mod,
// but the mod has the wrong version.
// This is an important warning that should be shown to the user.
trace(' WARNING '.warning() + ' Installed mod has a mismatched dependency: ${error.message}');
// Notify the user via popup.
funkin.util.WindowUtil.showError('Mod Dependency Error', error.message);
default:
// Polymod tried to load a mod, but failed because one of its dependencies was the wrong version.
// This is a major error that should be shown to the user.
trace(' ERROR '.error() + ' Failed to load mod due to mismatched dependency: ${error.message}');
// Notify the user via popup.
funkin.util.WindowUtil.showError('Mod Dependency Error', error.message);
}
case MOD_DEPENDENCY_CYCLICAL:
// Polymod tried to load a mod, but failed because one of its dependencies depends on that mod.
// This is a major error that should be shown to the user.
trace(' ERROR '.error() + ' Failed to load mod due to cyclical dependency: ${error.message}');
// Notify the user via popup.
funkin.util.WindowUtil.showError('Mod Dependency Error', error.message);
//
// Script Parsing Errors
//
case SCRIPT_PARSE_FAILED:
// A syntax error when parsing a script.
trace(' ERROR '.error() + ' ' + error.message);
// Notify the user via popup.
funkin.util.WindowUtil.showError('Script Parsing Error', error.message);
case SCRIPT_RUNTIME_EXCEPTION:
// A runtime error when running a script.
logError(error.message);
trace(' ERROR '.error() + ' ' + error.message);
// Notify the user via popup.
funkin.util.WindowUtil.showError('Polymod Script Exception', error.message);
case SCRIPT_CLASS_MODULE_NOT_FOUND:
// A scripted class tried to reference an unknown class or module.
logError(error.message);
funkin.util.WindowUtil.showError('Script Exception', error.message);
// Last word is the class name.
var msg:String = 'Import error in ${error.origin}';
msg += '\n${error.message}';
case SCRIPTED_CLASS_NOT_REGISTERED:
// Polymod attempted to initialize a scripted class, but it wasn't registered.
trace(' ERROR '.error() + ' ' + error.message);
// Notify the user via popup.
funkin.util.WindowUtil.showError('Script Parsing Error', error.message);
case SCRIPTED_CLASS_ALREADY_REGISTERED:
// Polymod attempted to register a scripted class, but one with the same name and package already exists.
trace(' ERROR '.error() + ' ' + error.message);
// Notify the user via popup.
funkin.util.WindowUtil.showError('Polymod Script Import Error', msg);
case SCRIPT_CLASS_MODULE_BLACKLISTED:
// A scripted class tried to reference a blacklisted class or module.
logError(error.message);
funkin.util.WindowUtil.showError('Script Parsing Error', error.message);
case SCRIPTED_CLASS_REDUNDANT_IMPORT:
// A scripted class imported a module that's already imported.
trace(' WARNING '.warning() + ' ' + error.message);
case SCRIPTED_CLASS_UNRESOLVED_IMPORT:
// A scripted class tried to import a module that doesn't exist.
trace(' ERROR '.error() + ' ' + error.message);
// Notify the user via popup.
funkin.util.WindowUtil.showError('Polymod Script Blacklist Violation', error.message);
funkin.util.WindowUtil.showError('Script Import Error', error.message);
case SCRIPTED_CLASS_BLACKLISTED_MODULE:
// A scripted class tried to import a module that's blacklisted.
trace(' ERROR '.error() + ' ' + error.message);
// Notify the user via popup.
funkin.util.WindowUtil.showError('Script Blacklist Violation', error.message);
case SCRIPTED_CLASS_BLACKLISTED_FIELD:
// A scripted class tried to access a field that's blacklisted.
trace(' ERROR '.error() + ' ' + error.message);
// Notify the user via popup.
funkin.util.WindowUtil.showError('Script Blacklist Violation', error.message);
//
// Other Errors
//
case FRAMEWORK_INIT, MOD_DEPENDENCY_CHECK_SKIPPED, SCRIPT_PARSE_START, SCRIPT_PARSE_DONE:
// Unimportant messages that we don't need to log.
return;
default:
// Log the message based on its severity.
switch (error.severity)
{
case NOTICE:
logInfo(error.message);
case WARNING:
logWarn(error.message);
case ERROR:
logError(error.message);
trace(' ERROR '.error() + ' ' + error.message);
case WARNING:
trace(' WARNING '.warning() + ' ' + error.message);
case INFO:
trace(' INFO '.info() + ' ' + error.message);
case DEBUG:
// trace(' DEBUG '.debug() + error.message);
}
}
}
static function logInfo(message:String):Void
{
trace(' INFO '.bg_blue().bold() + ' ${message}');
}
static function logError(message:String):Void
{
trace(' ERROR '.error() + ' ${message}');
}
static function logWarn(message:String):Void
{
trace(' WARNING '.bg_yellow().bold() + ' ${message}');
}
}
+48 -43
View File
@@ -46,13 +46,13 @@ class PolymodHandler
* Using more complex rules allows mods from older compatible versions to stay functioning,
* while preventing mods made for future versions from being installed.
*/
public static final API_VERSION_RULE:String = ">=0.8.0 <0.9.0";
public static final API_VERSION_RULE:String = '>=0.8.0 <0.9.0';
/**
* Where relative to the executable that mods are located.
*/
static final MOD_FOLDER:String =
#if (REDIRECT_ASSETS_FOLDER && macos)
#if (REDIRECT_ASSETS_FOLDER && mac)
'../../../../../../../example_mods'
#elseif REDIRECT_ASSETS_FOLDER
'../../../../example_mods'
@@ -61,7 +61,7 @@ class PolymodHandler
#end;
static final CORE_FOLDER:Null<String> =
#if (REDIRECT_ASSETS_FOLDER && macos)
#if (REDIRECT_ASSETS_FOLDER && mac)
'../../../../../../../assets'
#elseif REDIRECT_ASSETS_FOLDER
'../../../../assets'
@@ -69,6 +69,14 @@ class PolymodHandler
null
#end;
/**
* Populated with the directories of mods once they're successfully loaded.
*/
public static var loadedModDirs:Array<String> = [];
/**
* Populated with the IDs of mods once they're successfully loaded.
*/
public static var loadedModIds:Array<String> = [];
// Use SysZipFileSystem on native and MemoryZipFilesystem on web.
@@ -92,7 +100,7 @@ class PolymodHandler
createModRoot();
#end
trace('Initializing Polymod (using all mods)...');
loadModsById(getAllModIds());
loadModsByDir(getAllModDirs());
}
/**
@@ -105,7 +113,7 @@ class PolymodHandler
createModRoot();
#end
trace('Initializing Polymod (using configured mods)...');
loadModsById(Save.instance.enabledModIds.value);
loadModsByDir(Save.instance.enabledModDirs.value);
}
/**
@@ -119,22 +127,22 @@ class PolymodHandler
#end
// We still need to configure the debug print calls etc.
trace('Initializing Polymod (using no mods)...');
loadModsById([]);
loadModsByDir([]);
}
/**
* Load all the mods with the given ids.
* @param ids The ORDERED list of mod ids to load.
* Load all the mods with the directories they're in.
* @param dirs The ORDERED list of mod ids to load.
*/
public static function loadModsById(ids:Array<String>):Void
public static function loadModsByDir(dirs:Array<String>):Void
{
if (ids.length == 0)
if (dirs.length == 0)
{
trace('You attempted to load zero mods.');
}
else
{
trace('Attempting to load ${ids.length} mods...');
trace('Attempting to load ${dirs.length} mods...');
}
buildImports();
@@ -145,7 +153,7 @@ class PolymodHandler
// Root directory for all mods.
modRoot: MOD_FOLDER,
// The directories for one or more mods to load.
dirs: ids,
dirs: dirs,
// Framework being used to load assets.
framework: OPENFL,
// The current version of our API.
@@ -182,18 +190,20 @@ class PolymodHandler
{
if (loadedModList.length == 0)
{
trace('Mod loading complete. We loaded no mods / ${ids.length} mods.');
trace('Mod loading complete. We loaded no mods / ${dirs.length} mods.');
}
else
{
trace('Mod loading complete. We loaded ${loadedModList.length} / ${ids.length} mods.');
trace('Mod loading complete. We loaded ${loadedModList.length} / ${dirs.length} mods.');
}
}
loadedModIds = [];
loadedModDirs = [];
for (mod in loadedModList)
{
trace(' * ${mod.title} v${mod.modVersion} [${mod.id}]');
loadedModDirs.push(mod.dirName);
loadedModIds.push(mod.id);
}
@@ -247,25 +257,13 @@ class PolymodHandler
static function buildImports():Void
{
// Add default imports for common classes.
static final DEFAULT_IMPORTS:Array<Class<Dynamic>> = [
funkin.Assets,
funkin.Paths,
funkin.Preferences,
funkin.util.Constants,
flixel.FlxG
];
static final DEFAULT_IMPORTS:Array<Class<Dynamic>> = [funkin.Assets, funkin.Paths, funkin.Preferences, funkin.util.Constants, flixel.FlxG];
for (cls in DEFAULT_IMPORTS)
{
Polymod.addDefaultImport(cls);
}
// Add import aliases for certain classes.
// NOTE: Scripted classes are automatically aliased to their parent class.
Polymod.addImportAlias('flixel.math.FlxPoint', flixel.math.FlxPoint.FlxBasePoint);
Polymod.addImportAlias('funkin.data.event.SongEventSchema', funkin.data.event.SongEventSchema.SongEventSchemaRaw);
// `lime.utils.Assets` literally just has a private `resolveClass` function for some reason? so we replace it with our own.
Polymod.addImportAlias('lime.utils.Assets', funkin.Assets);
Polymod.addImportAlias('openfl.utils.Assets', funkin.Assets);
@@ -419,6 +417,11 @@ class PolymodHandler
// Disallow direct manipulation of save data.
Polymod.blacklistStaticFields(flixel.FlxG, ['save']);
// `haxe.Unserializer`
// Just to be double-sure, lets blacklist some fields of the Unserializer to make it harder to use if you DO get one.
Polymod.blacklistStaticFields(haxe.Unserializer, ['run']);
Polymod.blacklistInstanceFields(haxe.Unserializer, ['unserialize']);
// `funkin.save.Save`
// Direct access to save data is important for scripts (like checking unlocks),
// but we don't want scripts to be able to perform operations like writing scores.
@@ -427,6 +430,12 @@ class PolymodHandler
'clearData', // No score manipulation please
'setLevelScore', 'setSongScore', 'applySongRank']);
// `openfl.filesystem.FileStream`, `openfl.net.Socket`, `openfl.utils.ByteArray.ByteArrayData`
// Returns `Unseralizer.run` if encoded in HXSF format, though it does have to be seralized correctly for the exploit to work.
#if !html5 Polymod.blacklistInstanceFields(openfl.filesystem.FileStream, ['readObject']); #end
Polymod.blacklistInstanceFields(openfl.net.Socket, ['readObject']);
Polymod.blacklistInstanceFields(openfl.utils.ByteArray.ByteArrayData, ['readObject']);
// `funkin.api.*`
// Contains functions which may allow for cheating and such.
for (cls in ClassMacro.listClassesInPackage('funkin.api'))
@@ -455,15 +464,6 @@ class PolymodHandler
Polymod.blacklistImport(className);
}
// `funkin.api.newgrounds.*`
// Contains functions which allow for cheating medals and leaderboards.
for (cls in ClassMacro.listClassesInPackage('funkin.api.newgrounds'))
{
if (cls == null) continue;
var className:String = Type.getClassName(cls);
Polymod.blacklistImport(className);
}
// `io.newgrounds.*`
// Contains functions which allow for cheating medals and leaderboards.
for (cls in ClassMacro.listClassesInPackage('io.newgrounds'))
@@ -520,11 +520,6 @@ class PolymodHandler
var output:polymod.format.ParseRules = polymod.format.ParseRules.getDefault();
// Ensure TXT files have merge support.
output.addType('txt', TextFileFormat.LINES);
// Ensure script files have merge support.
output.addType('hscript', TextFileFormat.PLAINTEXT);
output.addType('hxs', TextFileFormat.PLAINTEXT);
output.addType('hxc', TextFileFormat.PLAINTEXT);
output.addType('hx', TextFileFormat.PLAINTEXT);
// You can specify the format of a specific file, with file extension.
// output.addFile("data/introText.txt", TextFileFormat.LINES)
@@ -569,18 +564,28 @@ class PolymodHandler
return modIds;
}
/**
* Retrieve a list of ALL mod directory names, including disabled mods.
* @return An array of mod direcotry names
*/
public static function getAllModDirs():Array<String>
{
var modDirs:Array<String> = [for (i in getAllMods()) i.dirName];
return modDirs;
}
/**
* Retrieve a list of metadata for all enabled mods.
* @return An array of mod metadata
*/
public static function getEnabledMods():Array<ModMetadata>
{
var modIds:Array<String> = Save.instance.enabledModIds.value;
var modDirs:Array<String> = Save.instance.enabledModDirs.value;
var modMetadata:Array<ModMetadata> = getAllMods();
var enabledMods:Array<ModMetadata> = [];
for (item in modMetadata)
{
if (modIds.indexOf(item.id) != -1)
if (modDirs.indexOf(item.dirName) != -1)
{
enabledMods.push(item);
}
+6 -6
View File
@@ -140,7 +140,7 @@ class HitNoteScriptEvent extends NoteScriptEvent
/**
* The score the player received for hitting the note.
*/
public var score:Int;
public var score:Float;
/**
* If the hit causes a combo break.
@@ -158,7 +158,7 @@ class HitNoteScriptEvent extends NoteScriptEvent
*/
public var doesNotesplash:Bool = false;
public function new(note:NoteSprite, healthChange:Float, score:Int, judgement:String, isComboBreak:Bool, comboCount:Int = 0, hitDiff:Float = 0,
public function new(note:NoteSprite, healthChange:Float, score:Float, judgement:String, isComboBreak:Bool, comboCount:Int = 0, hitDiff:Float = 0,
doesNotesplash:Bool = false):Void
{
super(NOTE_HIT, note, healthChange, comboCount, true);
@@ -200,7 +200,7 @@ class GhostMissNoteScriptEvent extends ScriptEvent
/**
* How much score should be lost when this ghost note is pressed.
*/
public var scoreChange(default, default):Int;
public var scoreChange(default, default):Float;
/**
* Whether to play the record scratch sound.
@@ -212,7 +212,7 @@ class GhostMissNoteScriptEvent extends ScriptEvent
*/
public var playAnim(default, default):Bool;
public function new(dir:NoteDirection, hasPossibleNotes:Bool, healthChange:Float, scoreChange:Int):Void
public function new(dir:NoteDirection, hasPossibleNotes:Bool, healthChange:Float, scoreChange:Float):Void
{
super(NOTE_GHOST_MISS, true);
this.dir = dir;
@@ -239,7 +239,7 @@ class HoldNoteScriptEvent extends NoteScriptEvent
/**
* The score the player received for hitting the note.
*/
public var score:Int;
public var score:Float;
/**
* If the hit causes a combo break.
@@ -257,7 +257,7 @@ class HoldNoteScriptEvent extends NoteScriptEvent
*/
public var doesNotesplash:Bool = false;
public function new(type:ScriptEventType, holdNote:SustainTrail, healthChange:Float, score:Int, isComboBreak:Bool, comboCount:Int = 0,
public function new(type:ScriptEventType, holdNote:SustainTrail, healthChange:Float, score:Float, isComboBreak:Bool, comboCount:Int = 0,
cancelable:Bool = false):Void
{
super(type, null, healthChange, comboCount, true);
+7 -5
View File
@@ -605,6 +605,9 @@ class PauseSubState extends MusicBeatSubState
// ===============
var fastOffset:Bool = false;
var lastOffsetPress:Float = 0;
#if !mobile
var offset:Float = Preferences.globalOffset ?? 0;
#end
/**
* Process user inputs every frame.
@@ -680,7 +683,6 @@ class PauseSubState extends MusicBeatSubState
function handleModifyingOffsets():Bool
{
#if !mobile
var offset:Int = Preferences.globalOffset ?? 0;
if (FlxG.keys.pressed.SHIFT && (controls.UI_UP || controls.UI_DOWN))
{
lastOffsetPress += FlxG.elapsed;
@@ -697,20 +699,20 @@ class PauseSubState extends MusicBeatSubState
{
offset += (controls.UI_UP_P || controls.UI_UP) ? 1 : -1;
offsetText.text = 'Global Offset: ${offset}ms';
offsetText.text = 'Global Offset: ${Std.int(offset)}ms';
}
}
else
{
offset += (controls.UI_UP_P || controls.UI_UP) ? 1 : -1;
offset += ((controls.UI_UP_P || controls.UI_UP) ? 1 : -1) * (FlxG.elapsed * 30);
offsetText.text = 'Global Offset: ${offset}ms';
offsetText.text = 'Global Offset: ${Std.int(offset)}ms';
}
if (offset > 1500) offset = 1500;
if (offset < -1500) offset = -1500;
Preferences.globalOffset = offset;
Preferences.globalOffset = Std.int(offset);
return true;
}
+92 -62
View File
@@ -11,7 +11,8 @@ import flixel.FlxSubState;
import flixel.math.FlxMath;
import flixel.math.FlxPoint;
import flixel.sound.FlxSound;
import flixel.text.FlxText;
import flixel.text.FlxBitmapFont;
import flixel.text.FlxBitmapText;
import flixel.tweens.FlxTween;
import flixel.tweens.FlxEase;
import flixel.ui.FlxBar;
@@ -41,7 +42,6 @@ import funkin.play.components.HealthIcon;
import funkin.play.components.PopUpStuff;
import funkin.play.components.Subtitles;
import funkin.play.cutscene.dialogue.Conversation;
import funkin.play.cutscene.VanillaCutscenes;
import funkin.play.cutscene.VideoCutscene;
import funkin.play.notes.NoteDirection;
import funkin.play.notes.notekind.NoteKindManager;
@@ -238,9 +238,10 @@ class PlayState extends MusicBeatSubState
/**
* The player's current score.
* TODO: Move this to its own class.
* This needs to be a float because you gain partial points as you hold a hold note,
* possibly less than one point each update depending on your framerate.
*/
public var songScore:Int = 0;
public var songScore:Float = 0;
/**
* Start at this point in the song once the countdown is done.
@@ -548,9 +549,9 @@ class PlayState extends MusicBeatSubState
* RENDER OBJECTS
*/
/**
* The FlxText which displays the current score.
* The FlxBitmapText which displays the current score.
*/
var scoreText:FlxText;
var scoreText:FlxBitmapText;
/**
* The bar which displays the player's health.
@@ -625,6 +626,11 @@ class PlayState extends MusicBeatSubState
*/
public var camPause:FunkinCamera;
/**
* The camera which contains, and controls visibility of, the fade out to the results screen.
*/
public var camTransition:FunkinCamera;
/**
* The combo popups. Includes the real-time combo counter and the rating.
*/
@@ -766,8 +772,6 @@ class PlayState extends MusicBeatSubState
previousCameraFollowPoint = params.cameraFollowPoint;
mirrorSongData = params.mirrored ?? false;
trace("Params: " + mirrorSongData + ", " + isBotPlayMode);
// Basic object initialization
// TODO: Add something to toggle this on!
@@ -793,6 +797,7 @@ class PlayState extends MusicBeatSubState
camCutouts = new FunkinCamera('playStateCamCutouts');
camSubtitles = new FunkinCamera('playStateCamSubtitles');
camPause = new FunkinCamera('playStateCamPause');
camTransition = new FunkinCamera('playStateCamTransition');
var currentChart = currentSong.getDifficulty(currentDifficulty, currentVariation);
var noteStyleId:Null<String> = currentChart?.noteStyle;
@@ -807,7 +812,7 @@ class PlayState extends MusicBeatSubState
// Healthbar
healthBarBG = FunkinSprite.create(0, 0, 'healthBar');
healthBar = new FlxBar(0, 0, RIGHT_TO_LEFT, Std.int(healthBarBG.width - 8), Std.int(healthBarBG.height - 8), null, 0, 2);
scoreText = new FlxText(0, 0, 0, '', 20);
scoreText = new FlxBitmapText(0, 0, '', FlxBitmapFont.fromAngelCode(Paths.font("vcr-bmp.png"), Paths.font("vcr-bmp.fnt")));
// Combo & Pop Up
comboPopUps = new PopUpStuff(noteStyle);
@@ -941,19 +946,10 @@ class PlayState extends MusicBeatSubState
// This gets set back to false when the chart actually starts.
startingSong = true;
// TODO: We hardcoded the transition into Winter Horrorland. Do this with a ScriptedSong instead.
if ((currentSong.id ?? '').toLowerCase() == 'winter-horrorland')
{
// VanillaCutscenes will call startCountdown later.
VanillaCutscenes.playHorrorStartCutscene();
}
else
{
// Call a script event to start the countdown.
// Songs with cutscenes should call event.cancel().
// As long as they call `PlayState.instance.startCountdown()` later, the countdown will start.
startCountdown();
}
// Call a script event to start the countdown.
// Songs with cutscenes should call event.cancel().
// As long as they call `PlayState.instance.startCountdown()` later, the countdown will start.
startCountdown();
// Create the pause button.
#if mobile
@@ -1118,7 +1114,7 @@ class PlayState extends MusicBeatSubState
cameraZoomRate = Constants.DEFAULT_ZOOM_RATE;
health = Constants.HEALTH_STARTING;
songScore = 0;
songScore = 0.0;
Highscore.tallies.combo = 0;
// so the song doesn't start too early :D
@@ -1241,8 +1237,6 @@ class PlayState extends MusicBeatSubState
FlxG.watch.addQuick('health', health);
FlxG.watch.addQuick('cameraBopIntensity', cameraBopIntensity);
// TODO: Add a song event for Handle GF dance speed.
// Handle player death.
if (!isInCutscene && !disableKeys)
{
@@ -1358,17 +1352,25 @@ class PlayState extends MusicBeatSubState
*/
function pause(mode:PauseMode = Standard, lostFocus:Bool = false):Void
{
if (!mayPauseGame || justUnpaused || isGamePaused || isPlayerDying) return;
if (!mayPauseGame || justUnpaused || isGamePaused || isPlayerDying || isSongEnd) return;
switch (mode)
{
case Conversation:
preparePauseUI();
openPauseSubState(Conversation, camPause, lostFocus, () -> currentConversation?.pauseMusic());
final event = new PauseScriptEvent(false);
dispatchEvent(event);
if (!event.eventCanceled) openPauseSubState(Conversation, camPause, lostFocus, () -> currentConversation?.pauseMusic());
case Cutscene:
preparePauseUI();
openPauseSubState(Cutscene, camPause, lostFocus, () -> VideoCutscene.pauseVideo());
final event = new PauseScriptEvent(false);
dispatchEvent(event);
if (!event.eventCanceled) openPauseSubState(Cutscene, camPause, lostFocus, () -> VideoCutscene.pauseVideo());
default: // also known as standard
if (!isInCountdown || isInCutscene) return;
@@ -1444,7 +1446,7 @@ class PlayState extends MusicBeatSubState
vwooshTimer.cancel();
songScore = 0;
songScore = 0.0;
updateScoreText();
health = Constants.HEALTH_STARTING;
@@ -1500,7 +1502,7 @@ class PlayState extends MusicBeatSubState
dispatchEvent(eventEvent);
// Calling event.cancelEvent() skips the event. Neat!
if (!eventEvent.eventCanceled)
if (!eventEvent.eventCanceled && !shouldSubstatePause)
{
SongEventRegistry.handleEvent(event);
}
@@ -1511,15 +1513,17 @@ class PlayState extends MusicBeatSubState
public override function dispatchEvent(event:ScriptEvent):Void
{
// ORDER: Module, Song, Note, Stage, Conversation, Character
// ORDER: Module, Song, Events, Notes, Stage, Conversation, Characters
// Modules should get the first chance to cancel the event.
// super.dispatchEvent(event) dispatches event to module scripts.
super.dispatchEvent(event);
// Dispatch event to song script.
ScriptEventDispatcher.callEvent(currentSong, event);
// Dispatch event to event notes
if (songEvents != null && songEvents.length > 0) SongEventRegistry.callEvent(event);
// Dispatch event to note kind scripts
NoteKindManager.callEvent(event);
@@ -1843,8 +1847,11 @@ class PlayState extends MusicBeatSubState
iconP2?.onStepHit(Std.int(Conductor.instance.currentStep));
// Only bop camera if zoom level is below 135%
// This is an arbitrary number chosen so that the camera doesn't move insanely far in when the bop speed is fast.
final MAX_RELATIVE_CAM_ZOOM:Float = 1.35;
if (Preferences.zoomCamera
&& FlxG.camera.zoom < (1.35 * FlxCamera.defaultZoom)
&& camHUD.zoom < (MAX_RELATIVE_CAM_ZOOM * defaultHUDCameraZoom)
&& cameraZoomRate > 0
&& (Conductor.instance.currentStep + cameraZoomRateOffset * Constants.STEPS_PER_BEAT) % (cameraZoomRate * Constants.STEPS_PER_BEAT) == 0)
{
@@ -1960,12 +1967,14 @@ class PlayState extends MusicBeatSubState
camCutouts.bgColor.alpha = 0; // Show the game scene behind the camera.
if (Preferences.subtitles) camSubtitles.bgColor.alpha = 0; // Show the game scene behind the camera.
camPause.bgColor.alpha = 0; // Show the game scene behind the camera.
camTransition.bgColor.alpha = 0;
FlxG.cameras.reset(camGame);
FlxG.cameras.add(camHUD, false);
FlxG.cameras.add(camCutscene, false);
FlxG.cameras.add(camCutouts, false);
if (Preferences.subtitles) FlxG.cameras.add(camSubtitles, false);
FlxG.cameras.add(camTransition, false);
FlxG.cameras.add(camPause, false);
// Configure camera follow point.
@@ -2006,7 +2015,10 @@ class PlayState extends MusicBeatSubState
// The score text below the health bar.
scoreText.x = healthBarBG.x + healthBarBG.width - 190;
scoreText.y = healthBarBG.y + 30;
scoreText.setFormat(Paths.font('vcr.ttf'), 16, FlxColor.WHITE, RIGHT, FlxTextBorderStyle.OUTLINE, FlxColor.BLACK);
scoreText.alignment = RIGHT;
scoreText.borderStyle = OUTLINE;
scoreText.borderColor = FlxColor.BLACK;
scoreText.letterSpacing = -1;
scoreText.scrollFactor.set();
scoreText.zIndex = 802;
add(scoreText);
@@ -2617,7 +2629,6 @@ class PlayState extends MusicBeatSubState
if (vocals != null)
{
trace('Playing vocals...');
add(vocals);
vocals.time = startTimestamp - Conductor.instance.instrumentalOffset;
@@ -2697,9 +2708,9 @@ class PlayState extends MusicBeatSubState
}
else
{
// TODO: Add an option for this maybe?
var commaSeparated:Bool = true;
scoreText.text = 'Score: ${FlxStringUtil.formatMoney(songScore, false, commaSeparated)}';
final SHOW_DECIMALS:Bool = false;
final COMMA_SEPARATED:Bool = true;
scoreText.text = 'Score: ${FlxStringUtil.formatMoney(songScore, SHOW_DECIMALS, COMMA_SEPARATED)}';
}
}
@@ -2758,6 +2769,16 @@ class PlayState extends MusicBeatSubState
// Calling event.cancelEvent() skips all the other logic! Neat!
if (event.eventCanceled) continue;
if (vocals != null)
{
if (vocals.legacyVoiceSystem)
{
if (vocals.legacyVoiceUsesPlayer) vocals.playerVolume = playerVocalsVolume;
else
vocals.opponentVolume = opponentVocalsVolume;
}
}
// Command the opponent to hit the note on time.
// NOTE: This is what handles the strumline and cleaning up the note itself!
opponentStrumline.hitNote(note);
@@ -2865,7 +2886,7 @@ class PlayState extends MusicBeatSubState
if (!isBotPlayMode && holdNote.scoreable)
{
health += Constants.HEALTH_HOLD_BONUS_PER_SECOND * elapsed;
songScore += Std.int(Constants.SCORE_HOLD_BONUS_PER_SECOND * elapsed);
songScore += Constants.SCORE_HOLD_BONUS_PER_SECOND * elapsed;
}
// Make sure the player keeps singing while the note is held by the bot.
@@ -2898,7 +2919,7 @@ class PlayState extends MusicBeatSubState
// 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 = healthChangeUncapped.clamp(healthChangeMax, 0);
var scoreChange = Std.int(Constants.SCORE_HOLD_DROP_PENALTY_PER_SECOND * remainingLengthSec);
var scoreChange:Float = Constants.SCORE_HOLD_DROP_PENALTY_PER_SECOND * remainingLengthSec;
var event:HoldNoteScriptEvent = new HoldNoteScriptEvent(NOTE_HOLD_DROP, holdNote, healthChange, scoreChange, true, Highscore.tallies.combo);
dispatchEvent(event);
@@ -2912,7 +2933,11 @@ class PlayState extends MusicBeatSubState
// Play the miss sound.
if (event.playSound)
{
if (vocals != null) vocals.playerVolume = 0;
if (vocals != null)
{
if (vocals.legacyVoiceSystem && !vocals.legacyVoiceUsesPlayer) vocals.opponentVolume = 0;
vocals.playerVolume = 0;
}
FunkinSound.playOnce(Paths.soundRandom('missnote', 1, 3), FlxG.random.float(0.5, 0.6));
}
}
@@ -3014,9 +3039,7 @@ class PlayState extends MusicBeatSubState
if (targetNote == null) continue;
// Judge and hit the note.
// trace('Hit note! ${targetNote.noteData}');
goodNoteHit(targetNote, input);
// trace('Score: ${songScore}');
notesInDirection.remove(targetNote);
@@ -3090,7 +3113,11 @@ class PlayState extends MusicBeatSubState
playerStrumline.hitNote(note, !event.isComboBreak);
if (event.doesNotesplash) playerStrumline.playNoteSplash(note.noteData.getDirection());
if (note.isHoldNote && note.holdNoteSprite != null) playerStrumline.playNoteHoldCover(note.holdNoteSprite);
if (vocals != null) vocals.playerVolume = playerVocalsVolume;
if (vocals != null)
{
if (vocals.legacyVoiceSystem && !vocals.legacyVoiceUsesPlayer) vocals.opponentVolume = opponentVocalsVolume;
vocals.playerVolume = playerVocalsVolume;
}
// Display the combo meter and add the calculation to the score.
if (note.scoreable)
@@ -3143,7 +3170,7 @@ class PlayState extends MusicBeatSubState
var event:GhostMissNoteScriptEvent = new GhostMissNoteScriptEvent(direction, // Direction missed in.
hasPossibleNotes, // Whether there was a note you could have hit.
Constants.HEALTH_GHOST_MISS_PENALTY, // How much health to add (negative).
- 10 // Amount of score to add (negative).
Constants.SCORE_GHOST_MISS_PENALTY // Amount of score to add (negative).
);
dispatchEvent(event);
@@ -3262,7 +3289,7 @@ class PlayState extends MusicBeatSubState
/**
* Handles applying health, score, and ratings.
*/
function applyScore(score:Int, daRating:String, healthChange:Float, isComboBreak:Bool)
function applyScore(score:Float, daRating:String, healthChange:Float, isComboBreak:Bool)
{
switch (daRating)
{
@@ -3419,7 +3446,7 @@ class PlayState extends MusicBeatSubState
{
// crackhead double thingie, sets whether was new highscore, AND saves the song!
var data = {
score: songScore,
score: Std.int(songScore),
tallies: {
sick: Highscore.tallies.sick,
good: Highscore.tallies.good,
@@ -3437,7 +3464,7 @@ class PlayState extends MusicBeatSubState
Highscore.talliesLevel = Highscore.combineTallies(Highscore.tallies, Highscore.talliesLevel);
#if FEATURE_NEWGROUNDS
Leaderboards.submitSongScore(currentSong.id, suffixedDifficulty, songScore);
Leaderboards.submitSongScore(currentSong.id, suffixedDifficulty, Std.int(songScore));
#end
if (!isPracticeMode && !isBotPlayMode)
@@ -3468,7 +3495,7 @@ class PlayState extends MusicBeatSubState
// Determine the score rank for this song we just finished.
var scoreRank:Null<ScoringRank> = Scoring.calculateRank({
score: songScore,
score: Std.int(songScore),
tallies: {
sick: Highscore.tallies.sick,
good: Highscore.tallies.good,
@@ -3503,7 +3530,7 @@ class PlayState extends MusicBeatSubState
{
isNewHighscore = false;
PlayStatePlaylist.campaignScore += songScore;
PlayStatePlaylist.campaignScore += Std.int(songScore);
// Pop the next song ID from the list.
// Returns null if the list is empty.
@@ -3627,7 +3654,7 @@ class PlayState extends MusicBeatSubState
{
if (isSubState)
{
if (isPlaytestResults)
if (isPlaytestResults && !isBotPlayMode)
{
moveToResultsScreen(false, prevScoreData);
}
@@ -3769,13 +3796,11 @@ class PlayState extends MusicBeatSubState
FlxG.camera.targetOffset.x += 20;
// Replace zoom animation with a fade out for now.
FlxG.camera.fade(FlxColor.BLACK, 0.6);
FlxTween.tween(camHUD, {alpha: 0}, 0.6);
FlxTween.tween(camHUD, {alpha: 0}, 0.6, {
onComplete: function(_)
{
moveToResultsScreen(isNewHighscore, prevScoreData);
}
camTransition.fade(FlxColor.BLACK, 0.6, false, function()
{
moveToResultsScreen(isNewHighscore, prevScoreData);
});
// Zoom in on Girlfriend (or BF if no GF)
@@ -3829,7 +3854,7 @@ class PlayState extends MusicBeatSubState
title: PlayStatePlaylist.isStoryMode ? ('${PlayStatePlaylist.campaignTitle}') : ('${currentChart.songName} by ${currentChart.songArtist}'),
prevScoreData: prevScoreData,
scoreData: {
score: PlayStatePlaylist.isStoryMode ? PlayStatePlaylist.campaignScore : songScore,
score: PlayStatePlaylist.isStoryMode ? PlayStatePlaylist.campaignScore : Std.int(songScore),
tallies: {
sick: talliesToUse.sick,
good: talliesToUse.good,
@@ -3910,9 +3935,11 @@ class PlayState extends MusicBeatSubState
@:nullSafety(Off)
FlxG.camera.target = null;
var adjustedDuration:Float = duration / playbackRate;
// Follow tween! Caching it so we can cancel/pause it later if needed.
var followPos:FlxPoint = cameraFollowPoint.getPosition() - FlxPoint.weak(FlxG.camera.width * 0.5, FlxG.camera.height * 0.5);
cameraFollowTween = FlxTween.tween(FlxG.camera.scroll, {x: followPos.x, y: followPos.y}, duration, {
cameraFollowTween = FlxTween.tween(FlxG.camera.scroll, {x: followPos.x, y: followPos.y}, adjustedDuration, {
ease: ease,
onComplete: function(_)
{
@@ -3950,7 +3977,8 @@ class PlayState extends MusicBeatSubState
else
{
// Zoom tween! Caching it so we can cancel/pause it later if needed.
cameraZoomTween = FlxTween.tween(this, {currentCameraZoom: targetZoom}, duration, {ease: ease});
var adjustedDuration:Float = duration / playbackRate;
cameraZoomTween = FlxTween.tween(this, {currentCameraZoom: targetZoom}, adjustedDuration, {ease: ease});
}
}
@@ -3976,7 +4004,7 @@ class PlayState extends MusicBeatSubState
/**
* The magical function that shall tween the scroll speed.
*/
public function tweenScrollSpeed(?speed:Float, ?duration:Float, ?ease:Null<Float->Float>, strumlines:Array<String>):Void
public function tweenScrollSpeed(?speed:Float, duration:Float = 0, ?ease:Null<Float->Float>, strumlines:Array<String>):Void
{
// Cancel the current tween if it's active.
cancelScrollSpeedTweens();
@@ -4003,9 +4031,11 @@ class PlayState extends MusicBeatSubState
}
else
{
var adjustedDuration:Float = duration / playbackRate;
scrollSpeedTweens.push(FlxTween.tween(strum, {
'scrollSpeed': value
}, duration, {ease: ease}));
}, adjustedDuration, {ease: ease}));
}
// make sure charts dont break if the charter is dumb and stupid
prevScrollTargets.push([value, i]);
+6 -1
View File
@@ -941,7 +941,7 @@ class ResultState extends MusicBeatSubState
var isScoreValid = !(params?.isPracticeMode ?? false) && !(params?.isBotPlayMode ?? false);
var isPersonalBest = rank > Scoring.calculateRank(params?.prevScoreData);
if (isScoreValid && isPersonalBest)
if ((isScoreValid && isPersonalBest) || params.forceRankSlam)
{
trace('THE RANK IS Higher.....');
@@ -1158,4 +1158,9 @@ typedef ResultsStateParams =
* The previous score data, used for rank comparision.
*/
var ?prevScoreData:SaveScoreData;
/**
* Forces to do the rank slamming animation in freeplay for debug purposes
*/
var ?forceRankSlam:Bool;
};
@@ -253,6 +253,9 @@ class BaseCharacter extends Bopper
// Set the x and y to be their original values.
this.resetPosition();
// Resets danceEvery value to the original one.
this.danceEvery = _data.danceEvery;
this.dance(true); // Force to avoid the old animation playing with the wrong offset at the start of the song.
// Make sure we are playing the idle animation
// ...then update the hitbox so that this.width and this.height are correct.
@@ -52,6 +52,8 @@ class PopUpStuff extends FlxTypedGroup<FunkinSprite>
rating.velocity.y -= FlxG.random.int(140, 175);
rating.velocity.x -= FlxG.random.int(0, 10);
rating.graphic.destroyOnNoUse = false;
add(rating);
var fadeEase = noteStyle.isJudgementSpritePixel(daRating) ? EaseUtil.stepped(2) : null;
@@ -101,6 +103,8 @@ class PopUpStuff extends FlxTypedGroup<FunkinSprite>
numScore.velocity.y -= FlxG.random.int(130, 150);
numScore.velocity.x = FlxG.random.float(-5, 5);
numScore.graphic.destroyOnNoUse = false;
add(numScore);
var fadeEase = noteStyle.isComboNumSpritePixel(digit) ? EaseUtil.stepped(2) : null;
@@ -1,62 +0,0 @@
package funkin.play.cutscene;
import flixel.FlxSprite;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
import funkin.audio.FunkinSound;
import flixel.util.FlxTimer;
import funkin.util.HapticUtil;
/**
* Static methods for playing cutscenes in the PlayState.
* TODO: Un-hardcode this shit!!!!!1!
*/
class VanillaCutscenes
{
static var blackScreen:FlxSprite;
static final TWEEN_DURATION:Float = 2.0;
/**
* Plays the cutscene that appears at the start of Winter Horrorland.
* TODO: Move this to `winter-horrorland.hxc`
*/
public static function playHorrorStartCutscene():Void
{
PlayState.instance.isInCutscene = true;
PlayState.instance.camHUD.visible = false;
blackScreen = new FlxSprite(-200, -200).makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK);
blackScreen.scrollFactor.set(0, 0);
blackScreen.zIndex = 1000000;
PlayState.instance.add(blackScreen);
new FlxTimer().start(0.1, function(_)
{
trace('Playing horrorland cutscene...');
PlayState.instance.remove(blackScreen);
// Force set the camera position and zoom.
PlayState.instance.cameraFollowPoint.setPosition(400, -2050);
PlayState.instance.resetCamera();
FlxG.camera.zoom = 2.5;
// Play the Sound effect.
HapticUtil.vibrate(0.1, 0.5, 1, 1);
FunkinSound.playOnce(Paths.sound('Lights_Turn_On'), function()
{
// Fade in the HUD.
trace('SFX done...');
PlayState.instance.camHUD.visible = true;
PlayState.instance.camHUD.alpha = 0.0; // Use alpha rather than visible to let us fade it in.
FlxTween.tween(PlayState.instance.camHUD, {alpha: 1.0}, TWEEN_DURATION, {ease: FlxEase.quadInOut});
// Start the countdown.
trace('Zoom out done...');
trace('Begin countdown (ends cutscene)');
PlayState.instance.startCountdown();
});
});
}
}
+8 -3
View File
@@ -70,6 +70,7 @@ class VideoCutscene
{
if (PlayState.instance == null) return;
#if FEATURE_VIDEO_PLAYBACK
if (!openfl.Assets.exists(filePath))
{
// Display a popup.
@@ -78,8 +79,7 @@ class VideoCutscene
return;
}
var rawFilePath = Paths.stripLibrary(filePath);
#end
// Trigger the cutscene. Don't play the song in the background.
PlayState.instance.isInCutscene = true;
@@ -100,13 +100,18 @@ class VideoCutscene
}
#end
#if NO_FEATURE_VIDEO_PLAYBACK
trace(" WARNING ".bold().bg_yellow() + " Video playback is not enabled. Calling video end callback instead.");
finishVideo();
#else
#if html5
playVideoHTML5(rawFilePath);
playVideoHTML5(Paths.stripLibrary(filePath));
#elseif hxvlc
playVideoNative(filePath);
#else
throw "No video support for this platform!";
#end
#end
}
public static function isPlaying():Bool
@@ -6,6 +6,7 @@ import funkin.data.song.SongData.SongEventData;
// Data from the event schema
import funkin.data.event.SongEventSchema;
import funkin.data.event.SongEventSchema.SongEventFieldType;
import funkin.modding.events.ScriptEvent.SongTimeScriptEvent;
/**
* This class handles song events which change the camera focus.
+16 -10
View File
@@ -139,16 +139,22 @@ class ScrollSpeedEvent extends SongEvent
type: SongEventFieldType.ENUM,
keys: ['In' => 'In', 'Out' => 'Out', 'In/Out' => 'InOut']
}, {
name: 'strumline',
title: 'Target Strumline',
defaultValue: DEFAULT_STRUMLINE,
type: SongEventFieldType.ENUM,
keys: ['Both' => 'both', 'Player' => 'player', 'Opponent' => 'opponent']
}, {
name: 'absolute',
title: 'Absolute',
defaultValue: DEFAULT_ABSOLUTE,
type: SongEventFieldType.BOOL,
name: 'advanced',
title: 'Advanced',
type: SongEventFieldType.FRAME,
collapsible: true,
children: [{
name: 'strumline',
title: 'Target Strumline',
defaultValue: DEFAULT_STRUMLINE,
type: SongEventFieldType.ENUM,
keys: ['Both' => 'both', 'Player' => 'player', 'Opponent' => 'opponent']
}, {
name: 'absolute',
title: 'Absolute',
defaultValue: DEFAULT_ABSOLUTE,
type: SongEventFieldType.BOOL,
}]
}]);
}
}
@@ -109,20 +109,26 @@ class SetHealthIconSongEvent extends SongEvent
defaultValue: DEFAULT_FLIPX,
type: SongEventFieldType.BOOL,
}, {
name: 'isPixel',
title: 'Is Pixel?',
defaultValue: DEFAULT_ISPIXEL,
type: SongEventFieldType.BOOL,
}, {
name: 'offsetX',
title: 'X Offset',
defaultValue: DEFAULT_X_OFFSET,
type: SongEventFieldType.FLOAT,
}, {
name: 'offsetY',
title: 'Y Offset',
defaultValue: DEFAULT_Y_OFFSET,
type: SongEventFieldType.FLOAT,
name: 'advanced',
title: 'Advanced',
type: SongEventFieldType.FRAME,
collapsible: true,
children: [{
name: 'isPixel',
title: 'Is Pixel?',
defaultValue: DEFAULT_ISPIXEL,
type: SongEventFieldType.BOOL,
}, {
name: 'offsetX',
title: 'X Offset',
defaultValue: DEFAULT_X_OFFSET,
type: SongEventFieldType.FLOAT,
}, {
name: 'offsetY',
title: 'Y Offset',
defaultValue: DEFAULT_Y_OFFSET,
type: SongEventFieldType.FLOAT,
}]
}]);
}
}
@@ -0,0 +1,105 @@
package funkin.play.event;
import flixel.FlxSprite;
import funkin.play.character.BaseCharacter;
import funkin.play.stage.Bopper;
// Data from the chart
import funkin.data.song.SongData.SongEventData;
// Data from the event schema
import funkin.data.event.SongEventSchema;
import funkin.data.event.SongEventSchema.SongEventFieldType;
/**
* This class handles song events which changes dance speed of specific character or stage prop.
*/
class SetTargetBopSpeedSongEvent extends SongEvent
{
public function new()
{
super('SetTargetBopSpeed');
}
static final DEFAULT_TARGET:String = 'boyfriend';
public override function handleEvent(data:SongEventData):Void
{
// Does nothing if there is no PlayState camera or stage.
if (PlayState.instance == null || PlayState.instance.currentStage == null) return;
var targetName:Null<String> = data.getString('target');
if (targetName == null) targetName = DEFAULT_TARGET;
var rate = data.getFloat('rate');
if (rate == null) rate = Constants.DEFAULT_PROP_RATE;
var target:FlxSprite = null;
switch (targetName)
{
case 'boyfriend' | 'bf' | 'player':
trace('Set dance rate to $rate on boyfriend.');
target = PlayState.instance.currentStage.getBoyfriend();
case 'dad' | 'opponent':
trace('Set dance rate to $rate on dad.');
target = PlayState.instance.currentStage.getDad();
case 'girlfriend' | 'gf':
trace('Set dance rate to $rate on girlfriend.');
target = PlayState.instance.currentStage.getGirlfriend();
default:
target = PlayState.instance.currentStage.getNamedProp(targetName);
if (target == null) trace('Unknown target to set dance rate: $targetName');
else
trace('Set dance rate to $targetName from stage.');
}
if (target != null)
{
if (Std.isOfType(target, BaseCharacter))
{
var targetChar:BaseCharacter = cast target;
targetChar.danceEvery = rate;
}
else if (Std.isOfType(target, Bopper))
{
var targetProp:Bopper = cast target;
targetProp.danceEvery = rate;
}
}
else
{
trace('Unknown SetTargetBopSpeed target: $targetName');
}
}
public override function getTitle():String
{
return "Set Target Bop Speed";
}
/**
* ```
* {
* "target": STRING, // Name of character or prop to point to.
* "anim": STRING, // Name of animation to play.
* }
* ```
* @return SongEventSchema
*/
public override function getEventSchema():SongEventSchema
{
return new SongEventSchema([{
name: 'target',
title: 'Target',
type: SongEventFieldType.STRING,
defaultValue: DEFAULT_TARGET,
}, {
name: 'rate',
title: 'Rate',
defaultValue: Constants.DEFAULT_PROP_RATE,
min: 0,
step: 0.25,
type: SongEventFieldType.FLOAT,
units: 'beats/dance'
}]);
}
}
+91 -1
View File
@@ -2,6 +2,8 @@ package funkin.play.event;
import funkin.data.song.SongData.SongEventData;
import funkin.data.event.SongEventSchema;
import funkin.modding.IScriptedClass.IPlayStateScriptedClass;
import funkin.modding.events.ScriptEvent;
/**
* Parameters used to initialize a song event.
@@ -22,7 +24,7 @@ typedef SongEventParams =
* It is used by the ScriptedSongEvent class to handle user-defined events,
* and also used by other classes in this package to provide default behavior for built-in events.
*/
class SongEvent
class SongEvent implements IPlayStateScriptedClass
{
/**
* These variables are used in two different events (and may be in more), and in order not to create unnecessary variables, we store them here
@@ -99,4 +101,92 @@ class SongEvent
{
return 'SongEvent(${this.id})';
}
public function onScriptEvent(event:ScriptEvent)
{
}
public function onCreate(event:ScriptEvent)
{
}
public function onDestroy(event:ScriptEvent)
{
}
public function onUpdate(event:UpdateScriptEvent)
{
}
public function onStepHit(event:SongTimeScriptEvent)
{
}
public function onBeatHit(event:SongTimeScriptEvent)
{
}
public function onPause(event:PauseScriptEvent)
{
}
public function onResume(event:ScriptEvent)
{
}
public function onSongStart(event:ScriptEvent)
{
}
public function onSongEnd(event:ScriptEvent)
{
}
public function onGameOver(event:ScriptEvent)
{
}
public function onNoteIncoming(event:NoteScriptEvent)
{
}
public function onNoteHit(event:HitNoteScriptEvent)
{
}
public function onNoteMiss(event:NoteScriptEvent)
{
}
public function onNoteHoldDrop(event:HoldNoteScriptEvent)
{
}
public function onSongEvent(event:SongEventScriptEvent)
{
}
public function onNoteGhostMiss(event:GhostMissNoteScriptEvent)
{
}
public function onCountdownStart(event:CountdownScriptEvent)
{
}
public function onCountdownStep(event:CountdownScriptEvent)
{
}
public function onCountdownEnd(event:CountdownScriptEvent)
{
}
public function onSongLoaded(event:SongLoadScriptEvent)
{
}
public function onSongRetry(event:SongRetryEvent)
{
}
}
+60 -12
View File
@@ -53,6 +53,49 @@ class SongEventHelper
return easeBitmapMap.get(key);
}
static function getEaseRange(func:Dynamic, samples:Int):{min:Float, max:Float}
{
if (func == null || samples <= 0) return {min: 0.0, max: 1.0};
var min:Float = 0.0;
var max:Float = 1.0;
var hasValue:Bool = false;
for (i in 0...samples)
{
var t:Float = if (samples > 1) (i / (samples - 1)) else 0.0;
var raw = func(t);
if (Math.isNaN(raw)) continue;
if (!hasValue)
{
min = raw;
max = raw;
hasValue = true;
}
else
{
if (raw < min) min = raw;
if (raw > max) max = raw;
}
}
if (!hasValue) return {min: 0.0, max: 1.0};
return {min: min, max: max};
}
static function getEaseY(raw:Float, size:Int, range:{min:Float, max:Float}):Int
{
var v:Float = raw;
var span = range.max - range.min;
if (span > 0)
v = (raw - range.min) / span;
v = if (v < 0) 0 else if (v > 1) 1 else v;
return Std.int((1 - v) * (size - 1));
}
static function createBitmapFromFunc(func:Dynamic, key:String, thickness:Int = 2):BitmapData
{
try
@@ -62,6 +105,14 @@ class SongEventHelper
if (key.toLowerCase() == "instant") return bd;
if (thickness < 1) thickness = 1;
var half = Std.int(thickness / 2);
var range = getEaseRange(func, size);
// Draw horizontal lines at y=1 and y=0 for reference.
var y1 = getEaseY(1, size, range);
var y0 = getEaseY(0, size, range);
bd.fillRect(new openfl.geom.Rectangle(0, y1 - half, size, thickness), 0xFF404040);
bd.fillRect(new openfl.geom.Rectangle(0, y0 - half, size, thickness), 0xFF404040);
var lastY:Int = -1;
for (i in 0...size)
{
@@ -69,21 +120,20 @@ class SongEventHelper
var raw = func(t);
if (!Math.isNaN(raw))
{
var v:Float = raw;
if (v < 0) v = 0;
if (v > 1) v = 1;
var y:Int = Std.int((1 - v) * (size - 1));
var y:Int = getEaseY(raw, size, range);
if (lastY == -1)
{
for (yy in (y - half)...(y + half + 1))
if (yy >= 0 && yy < size) bd.setPixel32(i, yy, 0xFFFFFFFF);
for (xx in (i - half)...(i + half + 1))
if (xx >= 0 && xx < size) for (yy in (y - half)...(y + half + 1))
if (yy >= 0 && yy < size) bd.setPixel32(xx, yy, 0xFFFFFFFF);
}
else
{
var a = Std.int(Math.min(y, lastY));
var b = Std.int(Math.max(y, lastY));
for (yy in a - half...b + half + 1)
if (yy >= 0 && yy < size) bd.setPixel32(i, yy, 0xFFFFFFFF);
for (xx in (i - half)...(i + half + 1))
if (xx >= 0 && xx < size) for (yy in a - half...b + half + 1)
if (yy >= 0 && yy < size) bd.setPixel32(xx, yy, 0xFFFFFFFF);
}
lastY = y;
}
@@ -122,6 +172,7 @@ class SongEventHelper
if (baseBd == null) return null;
var easeFunc:Dynamic = resolveEaseFuncForKey(key);
var sizeH:Int = baseBd.height;
var range = getEaseRange(easeFunc, sizeH);
var sprites:Array<FlxSprite> = [];
for (f in 0...frameCount)
{
@@ -136,10 +187,7 @@ class SongEventHelper
raw = FlxEase.linear(t);
}
if (Math.isNaN(raw)) raw = 0.0;
var v:Float = raw;
if (v < 0) v = 0;
if (v > 1) v = 1;
var y:Int = Std.int((1 - v) * (sizeH - 1));
var y:Int = getEaseY(raw, sizeH, range);
var bd:BitmapData = new BitmapData(dotWidth, sizeH, false, 0xFF202223);
var centerX:Int = Std.int(dotWidth / 2);
for (dx in -dotRadius...dotRadius + 1)
+46 -11
View File
@@ -1,6 +1,8 @@
package funkin.play.event;
import flixel.tweens.FlxEase;
import flixel.math.FlxPoint;
import funkin.ui.FullScreenScaleMode;
// Data from the chart
import funkin.data.song.SongData.SongEventData;
// Data from the event schema
@@ -27,9 +29,10 @@ class ZoomCameraSongEvent extends SongEvent
});
}
static final DEFAULT_ZOOM:Float = 1.0;
static final DEFAULT_DURATION:Float = 4.0;
static final DEFAULT_MODE:String = 'direct';
public static final DEFAULT_ZOOM:Float = 1.0;
public static final DEFAULT_WIDESCREEN_SCALE:Float = 0.0;
public static final DEFAULT_DURATION:Float = 4.0;
public static final DEFAULT_MODE:String = 'direct';
public override function handleEvent(data:SongEventData):Void
{
@@ -41,6 +44,11 @@ class ZoomCameraSongEvent extends SongEvent
var zoom:Float = data.getFloat('zoom') ?? DEFAULT_ZOOM;
var widescreenScaleX:Float = data.getFloat('widescreenScaleX') ?? DEFAULT_WIDESCREEN_SCALE;
var widescreenScaleY:Float = data.getFloat('widescreenScaleY') ?? DEFAULT_WIDESCREEN_SCALE;
var scaledZoom:Float = zoom + (zoom * calculateScale(FullScreenScaleMode.wideScale, FlxPoint.get(widescreenScaleX, widescreenScaleY)));
var duration:Float = data.getFloat('duration') ?? DEFAULT_DURATION;
var mode:String = data.getString('mode') ?? DEFAULT_MODE;
@@ -55,7 +63,7 @@ class ZoomCameraSongEvent extends SongEvent
switch (ease)
{
case 'INSTANT':
PlayState.instance.tweenCameraZoom(zoom, 0, isDirectMode);
PlayState.instance.tweenCameraZoom(scaledZoom, 0, isDirectMode);
default:
var durSeconds = Conductor.instance.stepLengthMs * duration / 1000;
var easeFunctionName = '$ease$easeDir';
@@ -66,10 +74,15 @@ class ZoomCameraSongEvent extends SongEvent
return;
}
PlayState.instance.tweenCameraZoom(zoom, durSeconds, isDirectMode, easeFunction);
PlayState.instance.tweenCameraZoom(scaledZoom, durSeconds, isDirectMode, easeFunction);
}
}
function calculateScale(wideScale:FlxPoint, scale:FlxPoint)
{
return (wideScale.x - 1) * scale.x + (wideScale.y - 1) * scale.y;
}
public override function getTitle():String
{
return 'Zoom Camera';
@@ -104,12 +117,6 @@ class ZoomCameraSongEvent extends SongEvent
step: 0.5,
type: SongEventFieldType.FLOAT,
units: 'steps'
}, {
name: 'mode',
title: 'Mode',
defaultValue: DEFAULT_MODE,
type: SongEventFieldType.ENUM,
keys: ['Stage zoom' => 'stage', 'Absolute zoom' => 'direct']
}, {
name: 'ease',
title: 'Easing Type',
@@ -122,6 +129,34 @@ class ZoomCameraSongEvent extends SongEvent
defaultValue: SongEvent.DEFAULT_EASE_DIR,
type: SongEventFieldType.ENUM,
keys: ['In' => 'In', 'Out' => 'Out', 'In/Out' => 'InOut']
}, {
name: 'advanced',
title: 'Advanced',
type: SongEventFieldType.FRAME,
collapsible: true,
children: [{
name: 'mode',
title: 'Mode',
defaultValue: DEFAULT_MODE,
type: SongEventFieldType.ENUM,
keys: ['Stage zoom' => 'stage', 'Absolute zoom' => 'direct']
}, {
name: 'widescreenScaleX',
title: 'Widescreen Scale X',
defaultValue: DEFAULT_WIDESCREEN_SCALE,
min: 0,
max: 1,
type: SongEventFieldType.FLOAT,
units: 'x'
}, {
name: 'widescreenScaleY',
title: 'Widescreen Scale Y',
defaultValue: DEFAULT_WIDESCREEN_SCALE,
min: 0,
max: 1,
type: SongEventFieldType.FLOAT,
units: 'x'
}]
}]);
}
}
+8 -1
View File
@@ -972,7 +972,8 @@ class Strumline extends FlxSpriteGroup
note.holdNoteSprite.hitNote = true;
note.holdNoteSprite.missedNote = false;
note.holdNoteSprite.sustainLength = (note.holdNoteSprite.strumTime + note.holdNoteSprite.fullSustainLength) - conductorInUse.songPosition;
note.holdNoteSprite.sustainLength = Math.min(note.holdNoteSprite.fullSustainLength,
(note.holdNoteSprite.strumTime + note.holdNoteSprite.fullSustainLength) - conductorInUse.songPosition);
}
#if FEATURE_GHOST_TAPPING
@@ -1094,6 +1095,8 @@ class Strumline extends FlxSpriteGroup
splash.y = this.y;
splash.y -= INITIAL_OFFSET;
splash.y += noteStyle.getSplashOffsets()[1] * splash.scale.y;
splash.graphic.destroyOnNoUse = false;
}
}
@@ -1171,6 +1174,8 @@ class Strumline extends FlxSpriteGroup
noteSprite.x -= NUDGE;
noteSprite.y = -9999;
noteSprite.graphic.destroyOnNoUse = false;
if (noteKind != null) noteSprite.scoreable = noteKind.scoreable;
}
@@ -1212,6 +1217,8 @@ class Strumline extends FlxSpriteGroup
holdNoteSprite.x -= holdNoteSprite.width / 2;
holdNoteSprite.y = -9999;
holdNoteSprite.graphic.destroyOnNoUse = false;
if (noteKind != null) holdNoteSprite.scoreable = noteKind.scoreable;
}
+3 -4
View File
@@ -120,14 +120,14 @@ class SustainTrail extends FlxSprite
{
super(0, 0);
setupHoldNoteGraphic(noteStyle);
noteStyleOffsets = noteStyle.getHoldNoteOffsets();
// BASIC SETUP
this.sustainLength = sustainLength;
this.fullSustainLength = sustainLength;
this.noteDirection = noteDirection;
setupHoldNoteGraphic(noteStyle);
noteStyleOffsets = noteStyle.getHoldNoteOffsets();
setIndices(TRIANGLE_VERTEX_INDICES);
this.active = true; // This NEEDS to be true for the note to be drawn!
@@ -262,7 +262,6 @@ class SustainTrail extends FlxSprite
{
if (s < 0.0) s = 0.0;
if (sustainLength == s) return s;
this.sustainLength = s;
triggerRedraw();
return this.sustainLength;
@@ -9,6 +9,6 @@ class NoAnimNoteKind extends NoteKind
public function new()
{
super("noanim", "Disables singing animations", null, [], DISABLE_ANIMATIONS, null);
super('noanim', 'No Animation', null, [], DISABLE_ANIMATIONS, null);
}
}
@@ -1,6 +1,5 @@
package funkin.play.notes.notekind;
import funkin.modding.events.ScriptEvent.HitNoteScriptEvent;
import funkin.modding.events.ScriptEvent.NoteScriptEvent;
/**
@@ -10,7 +9,7 @@ class NonScoreableNoteKind extends NoteKind
{
public function new()
{
super("non_scoreable", "Non-scoreable");
super('non_scoreable', 'Non-scoreable');
scoreable = false;
}
@@ -35,6 +35,15 @@ class NoteKindManager
return noteKinds.get(noteKind);
}
/**
* Retrieve a list of known valid note kinds.
* @return A list of note kinds
*/
public static function listNoteKinds():Array<String>
{
return noteKinds.keyValues();
}
/**
* Initialize custom behavior for note kinds.
*/
@@ -61,7 +70,7 @@ class NoteKindManager
if (noteKindClsName == 'funkin.play.notes.notekind.NoteKind'
|| noteKindClsName == 'funkin.play.notes.notekind.ScriptedNoteKind') continue;
var kind:NoteKind = Type.createInstance(noteKindCls, ["UNKNOWN"]);
var kind:NoteKind = Type.createInstance(noteKindCls, ['UNKNOWN']);
if (kind != null)
{
@@ -88,12 +97,9 @@ class NoteKindManager
{
try
{
var script:NoteKind = ScriptedNoteKind.scriptInit(scriptedClass, "unknown");
var script:NoteKind = ScriptedNoteKind.scriptInit(scriptedClass, 'unknown');
trace(' Initialized scripted note kind: ${script.noteKind}');
noteKinds.set(script.noteKind, script);
#if FEATURE_CHART_EDITOR
ChartEditorDropdowns.NOTE_KINDS.set(script.noteKind, script.description);
#end
}
catch (e)
{
+21 -19
View File
@@ -176,15 +176,15 @@ class Scoring
return switch (absTiming)
{
// case(_ < PBOT1_KILLER_THRESHOLD) => true:
// case(_ <= PBOT1_KILLER_THRESHOLD) => true:
// 'killer';
case(_ < PBOT1_SICK_THRESHOLD) => true:
case(_ <= PBOT1_SICK_THRESHOLD) => true:
'sick';
case(_ < PBOT1_GOOD_THRESHOLD) => true:
case(_ <= PBOT1_GOOD_THRESHOLD) => true:
'good';
case(_ < PBOT1_BAD_THRESHOLD) => true:
case(_ <= PBOT1_BAD_THRESHOLD) => true:
'bad';
case(_ < PBOT1_SHIT_THRESHOLD) => true:
case(_ <= PBOT1_SHIT_THRESHOLD) => true:
'shit';
default:
FlxG.log.warn('Missed note: Bad timing ($absTiming < $PBOT1_SHIT_THRESHOLD)');
@@ -272,13 +272,13 @@ class Scoring
return switch (absTiming)
{
case(_ < LEGACY_HIT_WINDOW * LEGACY_SICK_THRESHOLD) => true:
case(_ <= LEGACY_HIT_WINDOW * LEGACY_SICK_THRESHOLD) => true:
'sick';
case(_ < LEGACY_HIT_WINDOW * LEGACY_GOOD_THRESHOLD) => true:
case(_ <= LEGACY_HIT_WINDOW * LEGACY_GOOD_THRESHOLD) => true:
'good';
case(_ < LEGACY_HIT_WINDOW * LEGACY_BAD_THRESHOLD) => true:
case(_ <= LEGACY_HIT_WINDOW * LEGACY_BAD_THRESHOLD) => true:
'bad';
case(_ < LEGACY_HIT_WINDOW * LEGACY_SHIT_THRESHOLD) => true:
case(_ <= LEGACY_HIT_WINDOW * LEGACY_SHIT_THRESHOLD) => true:
'shit';
default:
FlxG.log.warn('Missed note: Bad timing ($absTiming < $LEGACY_SHIT_THRESHOLD)');
@@ -344,19 +344,19 @@ class Scoring
static function judgeNoteWEEK7(msTiming:Float):String
{
var absTiming = Math.abs(msTiming);
if (absTiming < WEEK7_HIT_WINDOW * WEEK7_SICK_THRESHOLD)
if (absTiming <= WEEK7_HIT_WINDOW * WEEK7_SICK_THRESHOLD)
{
return 'sick';
}
else if (absTiming < WEEK7_HIT_WINDOW * WEEK7_GOOD_THRESHOLD)
else if (absTiming <= WEEK7_HIT_WINDOW * WEEK7_GOOD_THRESHOLD)
{
return 'good';
}
else if (absTiming < WEEK7_HIT_WINDOW * WEEK7_BAD_THRESHOLD)
else if (absTiming <= WEEK7_HIT_WINDOW * WEEK7_BAD_THRESHOLD)
{
return 'bad';
}
else if (absTiming < WEEK7_HIT_WINDOW)
else if (absTiming <= WEEK7_HIT_WINDOW)
{
return 'shit';
}
@@ -529,7 +529,7 @@ enum abstract ScoringRank(String)
case SHIT:
return 2 / 24;
default:
return 3.5;
return 2 / 24;
}
}
@@ -549,7 +549,7 @@ enum abstract ScoringRank(String)
case SHIT:
return 95 / 24;
default:
return 3.5;
return 95 / 24;
}
}
@@ -569,7 +569,7 @@ enum abstract ScoringRank(String)
case SHIT:
return 186 / 24;
default:
return 3.5;
return 186 / 24;
}
}
@@ -589,7 +589,7 @@ enum abstract ScoringRank(String)
case SHIT:
return 207 / 24;
default:
return 3.5;
return 207 / 24;
}
}
@@ -631,7 +631,7 @@ enum abstract ScoringRank(String)
case SHIT:
return 'resultScreen/rankText/rankScrollLOSS';
default:
return 'resultScreen/rankText/rankScrollGOOD';
return 'resultScreen/rankText/rankScrollLOSS';
}
}
@@ -652,7 +652,7 @@ enum abstract ScoringRank(String)
case SHIT:
return 'resultScreen/rankText/rankTextLOSS';
default:
return 'resultScreen/rankText/rankTextGOOD';
return 'resultScreen/rankText/rankTextLOSS';
}
}
@@ -672,6 +672,8 @@ enum abstract ScoringRank(String)
0xFFFF58B4;
case PERFECT_GOLD:
0xFFFFB619;
default:
0xFF6044FF;
}
}
+18 -4
View File
@@ -1018,11 +1018,25 @@ class SongDifficulty
result.addOpponentVoice(FunkinSound.load(opponentVoice, 1.0, false, false, false, false, null, null, true));
}
// Sometimes the sounds don't set their important value to true, so we have to do this manually.
result.forEach(function(snd:FunkinSound)
if (result.members.length == 0)
{
snd.important = true;
});
var suffix:String = (variation != null && variation != '' && variation != 'default') ? '-$variation' : '';
// Try to use `Voices.ogg` if no other voices are found.
var legacyPath = Paths.voices(this.song.id, '$suffix');
if (Assets.exists(legacyPath))
{
result.addPlayerVoice(FunkinSound.load(legacyPath, 1.0, false, false, false, false, null, null, true));
}
}
if (result.members.length == 1) // It's legacy'ing somewhere and i can prove it
{
result.legacyVoiceSystem = true;
result.legacyVoiceUsesPlayer = result.getPlayerVoice(0) != null;
}
// Sometimes the sounds don't set their important value to true, so we have to do this manually.
result.forEach((snd:FunkinSound) -> snd.important = true);
result.playerVoicesOffset = offsets.getVocalOffset(characters.player, instId);
result.opponentVoicesOffset = offsets.getVocalOffset(characters.opponent, instId);
+6
View File
@@ -145,6 +145,12 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
prop.x = dataProp.position[0];
prop.y = dataProp.position[1];
prop.zIndex = dataProp.zIndex;
// Reset danceEvery value.
if (Std.isOfType(prop, Bopper))
{
cast(prop, Bopper).danceEvery = dataProp.danceEvery;
}
}
}
+2 -2
View File
@@ -245,7 +245,7 @@ class Save implements ConsoleClass
/// MODS
///
@:saveProperty(data.mods.enabledMods)
public var enabledModIds:SaveProperty<Array<String>>;
public var enabledModDirs:SaveProperty<Array<String>>;
///
/// CHART EDITOR OPTIONS
@@ -1223,7 +1223,7 @@ typedef SaveDataOptions =
var audioVisualOffset:Int;
/**
* If we want the framerate to be unlocked on HTML5.
* If we want the framerate to be unlocked.
* @default `false`
*/
var unlockedFramerate:Bool;
-2
View File
@@ -31,8 +31,6 @@ class SaveProperty<T>
if (_autoFlush) Save.system.flush();
trace('[Save Property]: Changing value of $oldValue TO $newValue');
onChange.dispatch(newValue);
}
+1 -6
View File
@@ -385,7 +385,7 @@ class FullScreenScaleMode extends flixel.system.scaleModes.BaseScaleMode
}
#if mobile
private function updateDeviceNotch(notch:lime.math.Rectangle):Void
private function updateDeviceNotch(notch:openfl.geom.Rectangle):Void
{
notchPosition.set(enabled ? notch.x : 0, enabled ? notch.y : 0);
notchSize.set(enabled ? notch.width : 0, enabled ? notch.height : 0);
@@ -403,11 +403,6 @@ class FullScreenScaleMode extends flixel.system.scaleModes.BaseScaleMode
gameNotchPosition *= scale;
gameNotchSize *= scale;
}
#if ios
gameNotchPosition /= 2;
gameNotchSize /= 2;
#end
}
#end
+6 -8
View File
@@ -1,13 +1,13 @@
package funkin.ui;
import funkin.graphics.FlxFilteredSprite;
import funkin.graphics.FunkinSprite;
/**
* The icon that gets used for Freeplay capsules and char select
* NOT to be confused with the CharIcon class, which is for the in-game icons
* NOT to be confused with the HealthIcon class, which is for the in-game icons
*/
@:nullSafety
class PixelatedIcon extends FlxFilteredSprite
class PixelatedIcon extends FunkinSprite
{
public var char:String;
@@ -25,7 +25,6 @@ class PixelatedIcon extends FlxFilteredSprite
public function setCharacter(char:String):Void
{
if (this.char == char) return;
this.char = char;
var charPath:String = "freeplay/icons/";
@@ -52,10 +51,9 @@ class PixelatedIcon extends FlxFilteredSprite
this.visible = false;
return;
}
else
{
this.visible = true;
}
this.visible = true;
this.char = char; // if we went past this its safe to assume the icon exists so we can assign it
var isAnimated = Assets.exists(Paths.file('images/$charPath.xml'));
@@ -48,6 +48,10 @@ class IntroSubState extends MusicBeatSubState
playVideoNative(LIGHTS_VIDEO_PATH);
#end
#if NO_FEATURE_VIDEO_PLAYBACK
onLightsEnd();
#end
// // Im TOO lazy to even care, so uh, yep
// FlxG.camera.zoom = 0.66666666666666666666666666666667;
// vid.x = -(FlxG.width - (FlxG.width * FlxG.camera.zoom));
+24 -16
View File
@@ -41,29 +41,37 @@ class Nametag extends FlxSprite
public function switchChar(str:String, playMosaicSequence:Bool = true):Void
{
if (playMosaicSequence) shaderEffect();
var path:String = (str == "bf") ? "boyfriend" : "bf";
if (str != "bf") path = str;
new FlxTimer().start(4 / 30, _ ->
loadGraphic(Paths.image("charSelect/" + path + "Nametag"));
updateHitbox();
scale.set(0.77, 0.77);
updatePosition();
// Reset shader to ensure the nametag doesn't get stuck
if (playMosaicSequence)
{
var path:String = str;
switch str
mosaicShader.setBlockSize(1, 1);
shaderEffect();
// Delay the shader effect by a bit to prevent lag.
new FlxTimer().start(2 / 30, _ ->
{
case "bf":
path = "boyfriend";
}
loadGraphic(Paths.image('charSelect/' + path + "Nametag"));
updateHitbox();
scale.x = scale.y = 0.77;
updatePosition();
if (playMosaicSequence) shaderEffect(true);
});
shaderEffect(true);
});
}
else
{
mosaicShader.setBlockSize(1, 1);
}
}
function shaderEffect(fadeOut:Bool = false):Void
{
// Skip the shader effect if the width is too small.
if (width <= 1) return;
if (currentMosaicSequence != null)
{
// Forcibly reset the shader to prevent overlapping blur sequences
+64 -55
View File
@@ -7,13 +7,11 @@ import openfl.display.Shape;
import openfl.display.Sprite;
import openfl.text.TextField;
import openfl.text.TextFormat;
import openfl.Lib;
/**
* A debug overlay showing useful info.
*/
#if cpp
@:access(lime._internal.backend.native.NativeCFFI)
#end
class FunkinDebugDisplay extends Sprite
{
static final UPDATE_DELAY:Int = 100;
@@ -31,18 +29,16 @@ class FunkinDebugDisplay extends Sprite
*/
public var backgroundOpacity(default, set):Float = 0.5;
var currentFPS:Int;
var deltaTimeout:Float;
var times:Array<Float>;
var color:Int;
#if !html5
var fps:Int;
var fpsPeak:Int;
var gcMem:Float;
var gcMemPeak:Float;
var taskMem:Float;
var taskMemPeak:Float;
#end
var background:Shape;
@@ -58,16 +54,18 @@ class FunkinDebugDisplay extends Sprite
this.x = x;
this.y = y;
this.currentFPS = 0;
this.deltaTimeout = 0.0;
#if !html5
this.times = [];
this.color = color;
this.fps = 0;
this.fpsPeak = 0;
this.gcMem = 0.0;
this.gcMemPeak = 0.0;
this.taskMem = 0.0;
this.taskMemPeak = 0.0;
#end
this.times = [];
this.color = color;
this.backgroundOpacity = 0;
this.isAdvanced = false;
}
@@ -76,13 +74,23 @@ class FunkinDebugDisplay extends Sprite
{
removeChildren(0, numChildren);
final BG_WIDTH_MULTIPLIER:Float = #if html5 advanced ? 1 : 0.3 #else 1 #end;
var BG_WIDTH_MULTIPLIER:Float = advanced ? 1 : 0.3;
#if html5
final BG_HEIGHT_MULTIPLIER:Float = advanced ? 0.45 : 0.15;
#else
final BG_HEIGHT_MULTIPLIER:Float = advanced ? 1 : (MemoryUtil.supportsTaskMem()) ? 0.3 : 0.2;
#end
if (MemoryUtil.supportsGCMem() || MemoryUtil.supportsTaskMem())
{
BG_WIDTH_MULTIPLIER = 1;
}
var BG_HEIGHT_MULTIPLIER:Float = advanced ? 0.45 : 0.15;
if (MemoryUtil.supportsGCMem() && MemoryUtil.supportsTaskMem())
{
BG_HEIGHT_MULTIPLIER = advanced ? 1 : 0.3;
}
else if (MemoryUtil.supportsGCMem() || MemoryUtil.supportsTaskMem())
{
BG_HEIGHT_MULTIPLIER = advanced ? 0.7 : 0.2;
}
background = new Shape();
background.graphics.beginFill(0x3d3f41, 1);
@@ -118,10 +126,12 @@ class FunkinDebugDisplay extends Sprite
fpsGraph.minValue = 0;
addChild(fpsGraph);
#if !html5
gcMemGraph = new FunkinStatsGraph(OTHERS_OFFSET, Math.floor(OTHERS_OFFSET + (fpsGraph.y + fpsGraph.axisHeight) + 22), graphsWidth, graphsHeight, color);
gcMemGraph.minValue = 0;
addChild(gcMemGraph);
if (MemoryUtil.supportsGCMem())
{
gcMemGraph = new FunkinStatsGraph(OTHERS_OFFSET, Math.floor(OTHERS_OFFSET + (fpsGraph.y + fpsGraph.axisHeight) + 22), graphsWidth, graphsHeight, color);
gcMemGraph.minValue = 0;
addChild(gcMemGraph);
}
if (MemoryUtil.supportsTaskMem())
{
@@ -130,7 +140,6 @@ class FunkinDebugDisplay extends Sprite
taskMemGraph.minValue = 0;
addChild(taskMemGraph);
}
#end
}
function createSimpleElements():Void
@@ -147,15 +156,9 @@ class FunkinDebugDisplay extends Sprite
addChild(infoDisplay);
}
override function __enterFrame(deltaTime:Int):Void
override function __enterFrame(deltaTime:Float):Void
{
#if cpp
final currentTime:Float = lime._internal.backend.native.NativeCFFI.lime_sdl_get_ticks();
#elseif html5
final currentTime:Float = js.Browser.window.performance.now();
#else
final currentTime:Float = haxe.Timer.stamp() * 1000;
#end
final currentTime:Float = Lib.getTimer();
times.push(currentTime);
@@ -170,12 +173,16 @@ class FunkinDebugDisplay extends Sprite
return;
}
currentFPS = times.length;
fps = times.length;
#if !html5
gcMem = MemoryUtil.getGCMemory();
if (fps > fpsPeak) fpsPeak = fps;
if (gcMem > gcMemPeak) gcMemPeak = gcMem;
if (MemoryUtil.supportsGCMem())
{
gcMem = MemoryUtil.getGCMemory();
if (gcMem > gcMemPeak) gcMemPeak = gcMem;
}
if (MemoryUtil.supportsTaskMem())
{
@@ -183,7 +190,6 @@ class FunkinDebugDisplay extends Sprite
if (taskMem > taskMemPeak) taskMemPeak = taskMem;
}
#end
if (isAdvanced)
{
@@ -200,25 +206,24 @@ class FunkinDebugDisplay extends Sprite
function updateAdvancedDisplay():Void
{
updateFPSGraph();
#if !html5
updateGcMemGraph();
updateTaskMemGraph();
#end
final info:Array<String> = [];
info.push('FPS: $currentFPS');
info.push('FPS: $fps');
info.push('AVG FPS: ${Math.floor(fpsGraph.average())}');
info.push('1% LOW FPS: ${Math.floor(fpsGraph.lowest())}');
fpsGraph.textDisplay.text = info.join('\n');
#if !html5
gcMemGraph.textDisplay.text = 'GC MEM: ${FlxStringUtil.formatBytes(gcMem).toLowerCase()} / ${FlxStringUtil.formatBytes(gcMemPeak).toLowerCase()}';
if (gcMemGraph != null)
{
gcMemGraph.textDisplay.text = 'GC MEM: ${FlxStringUtil.formatBytes(gcMem).toLowerCase()} / ${FlxStringUtil.formatBytes(gcMemPeak).toLowerCase()}';
}
if (taskMemGraph != null)
{
taskMemGraph.textDisplay.text = 'TASK MEM: ${FlxStringUtil.formatBytes(taskMem).toLowerCase()} / ${FlxStringUtil.formatBytes(taskMemPeak).toLowerCase()}';
}
#end
}
function updateSimpleDisplay():Void
@@ -227,33 +232,38 @@ class FunkinDebugDisplay extends Sprite
{
final info:Array<String> = [];
info.push('FPS: $currentFPS');
info.push('FPS: $fps');
#if !html5
info.push('GC MEM: ${FlxStringUtil.formatBytes(gcMem).toLowerCase()} / ${FlxStringUtil.formatBytes(gcMemPeak).toLowerCase()}');
if (MemoryUtil.supportsGCMem())
{
info.push('GC MEM: ${FlxStringUtil.formatBytes(gcMem).toLowerCase()} / ${FlxStringUtil.formatBytes(gcMemPeak).toLowerCase()}');
}
if (MemoryUtil.supportsTaskMem())
{
info.push('TASK MEM: ${FlxStringUtil.formatBytes(taskMem).toLowerCase()} / ${FlxStringUtil.formatBytes(taskMemPeak).toLowerCase()}');
#end
}
infoDisplay.text = info.join('\n');
}
}
function updateFPSGraph(?currentFPS:Int = 0):Void
function updateFPSGraph():Void
{
fpsGraph.maxValue = FlxG.drawFramerate;
fpsGraph.update(times.length);
fpsGraph.maxValue = fpsPeak;
fpsGraph.update(fps);
}
#if !html5
function updateGcMemGraph(?currentFPS:Int = 0):Void
function updateGcMemGraph():Void
{
gcMemGraph.maxValue = gcMemPeak;
gcMemGraph.update(gcMem);
if (gcMemGraph != null)
{
gcMemGraph.maxValue = gcMemPeak;
gcMemGraph.update(gcMem);
}
}
function updateTaskMemGraph(?currentFPS:Int = 0):Void
function updateTaskMemGraph():Void
{
if (taskMemGraph != null)
{
@@ -261,7 +271,6 @@ class FunkinDebugDisplay extends Sprite
taskMemGraph.update(taskMem);
}
}
#end
function set_isAdvanced(value:Bool):Bool
{
@@ -93,10 +93,7 @@ class DebugBoundingState extends FlxState
var viewDropdown:DropDown = offsetEditorDialog.findComponent("swapper", DropDown);
viewDropdown.onChange = function(e:UIEvent)
{
trace(e.type);
curView = cast e?.data?.curView;
trace(e.data);
// trace(e.data);
};
offsetAnimationDropdown = offsetEditorDialog.findComponent("animationDropdown", DropDown);
@@ -451,7 +448,8 @@ class DebugBoundingState extends FlxState
// Plays the idle animation
if (FlxG.keys.justPressed.SPACE)
{
offsetAnimationDropdown.value = {id: 'idle', text: 'idle'};
if (swagChar?.hasAnimation('danceLeft')) offsetAnimationDropdown.value = {id: 'danceLeft', text: 'danceLeft'};
else offsetAnimationDropdown.value = {id: 'idle', text: 'idle'};
playCharacterAnimation(currentAnimationName, true);
}
@@ -568,21 +566,18 @@ class DebugBoundingState extends FlxState
for (i in swagChar.animationOffsets.keys())
{
characterAnimNames.push(i);
trace(i);
trace(swagChar.animationOffsets[i]);
}
offsetAnimationDropdown.dataSource.clear();
for (charAnim in characterAnimNames)
{
trace('Adding ${charAnim} to HaxeUI dropdown');
offsetAnimationDropdown.dataSource.add({id: charAnim, text: charAnim});
}
offsetAnimationDropdown.selectedIndex = 0;
trace('Added ${offsetAnimationDropdown.dataSource.size} to HaxeUI dropdown');
trace('Added ${offsetAnimationDropdown.dataSource.size} animations to HaxeUI dropdown');
offsetAnimationDropdown.onChange = function(event:UIEvent)
{
@@ -607,7 +602,6 @@ class DebugBoundingState extends FlxState
// var animName = characterAnimNames[Std.parseInt(str)];
var animName = str;
swagChar.playAnimation(animName, true); // trace();
trace(swagChar.animationOffsets.get(animName));
txtOffsetShit.text = 'Offset: ' + swagChar.animOffsets;
txtOffsetShit.y = FlxG.height - 20 - txtOffsetShit.height;
@@ -58,6 +58,7 @@ import funkin.ui.debug.charting.commands.ChartEditorCommand;
import funkin.ui.debug.charting.commands.CopyItemsCommand;
import funkin.ui.debug.charting.commands.CutItemsCommand;
import funkin.ui.debug.charting.commands.DeselectAllItemsCommand;
import funkin.ui.debug.charting.commands.DeselectAllItemsBetweenTimeCommand;
import funkin.ui.debug.charting.commands.DeselectItemsCommand;
import funkin.ui.debug.charting.commands.ExtendNoteLengthCommand;
import funkin.ui.debug.charting.commands.FlipNotesCommand;
@@ -72,6 +73,7 @@ import funkin.ui.debug.charting.commands.RemoveItemsCommand;
import funkin.ui.debug.charting.commands.RemoveNotesCommand;
import funkin.ui.debug.charting.commands.RemoveStackedNotesCommand;
import funkin.ui.debug.charting.commands.SelectAllItemsCommand;
import funkin.ui.debug.charting.commands.SelectAllItemsBetweenTimeCommand;
import funkin.ui.debug.charting.commands.SelectItemsCommand;
import funkin.ui.debug.charting.commands.SetItemSelectionCommand;
import funkin.ui.debug.charting.commands.SwitchDifficultyCommand;
@@ -292,6 +294,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
{
this.songLengthInMs = value;
resetPreviewTimes();
updateGridHeight();
return this.songLengthInMs;
@@ -789,6 +792,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
return hitsoundVolumePlayer + hitsoundVolumeOpponent > 0;
}
var stretchySound1:Null<FunkinSound> = null;
var stretchySound2:Null<FunkinSound> = null;
// Auto-save
/**
@@ -1144,6 +1150,22 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/
var commandHistoryDirty:Bool = true;
/**
* Whether the selection has changed and the edit buttons need to be updated.
*/
var editButtonsDirty:Bool = true;
/**
* Whether the clipboard has changed and the paste buttons need to be updated.
*/
var clipboardDirty:Bool = true;
/**
* Whether the clipboard is valid and contains a json of notes and events.
*/
var clipboardValid:Bool = true;
/**
* If true, we are currently in the process of quitting the chart editor.
* Skip any update functions as most of them will call a crash.
@@ -1413,6 +1435,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
function set_currentSongMetadata(value:SongMetadata):SongMetadata
{
songMetadata.set(selectedVariation, value);
resetPreviewTimes();
return value;
}
@@ -1558,28 +1583,28 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
return currentSongMetadata.playData.noteStyle = value;
}
var currentSongFreeplayPreviewStart(get, set):Int;
var currentSongFreeplayPreviewStart(get, set):Float;
function get_currentSongFreeplayPreviewStart():Int
function get_currentSongFreeplayPreviewStart():Float
{
return currentSongMetadata.playData.previewStart;
}
function set_currentSongFreeplayPreviewStart(value:Int):Int
function set_currentSongFreeplayPreviewStart(value:Float):Float
{
return currentSongMetadata.playData.previewStart = value;
return currentSongMetadata.playData.previewStart = value * (value < 1 ? songLengthInMs : 1);
}
var currentSongFreeplayPreviewEnd(get, set):Int;
var currentSongFreeplayPreviewEnd(get, set):Float;
function get_currentSongFreeplayPreviewEnd():Int
function get_currentSongFreeplayPreviewEnd():Float
{
return currentSongMetadata.playData.previewEnd;
}
function set_currentSongFreeplayPreviewEnd(value:Int):Int
function set_currentSongFreeplayPreviewEnd(value:Float):Float
{
return currentSongMetadata.playData.previewEnd = value;
return currentSongMetadata.playData.previewEnd = value * (value < 1 ? songLengthInMs : 1);
}
var currentSongStage(get, set):String;
@@ -1959,14 +1984,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
var menubarItemSelectRegion:MenuItem;
/**
* The `Edit -> Select Before Cursor` menu item.
* The `Edit -> Select Before Playhead` menu item.
*/
var menubarItemSelectBeforeCursor:MenuItem;
var menubarItemSelectBeforePlayhead:MenuItem;
/**
* The `Edit -> Select After Cursor` menu item.
* The `Edit -> Select After Playhead` menu item.
*/
var menubarItemSelectAfterCursor:MenuItem;
var menubarItemSelectAfterPlayhead:MenuItem;
/**
* The `Edit -> Decrease Note Snap Precision` menu item.
@@ -2525,6 +2550,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
this.welcomeMusic.looped = true;
}
public function resetPreviewTimes() {
currentSongFreeplayPreviewStart = (currentSongMetadata?.playData?.previewStart ?? Constants.DEFAULT_PREVIEW_START_TIME);
currentSongFreeplayPreviewEnd = (currentSongMetadata?.playData?.previewEnd ?? Constants.DEFAULT_PREVIEW_END_TIME);
}
public function loadPreferences():Void
{
var save:Save = Save.instance;
@@ -2555,7 +2585,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
menubarItemVolumeInstrumental.value = Std.int(save.chartEditorInstVolume.value * 100);
menubarItemVolumeVocalsPlayer.value = Std.int(save.chartEditorPlayerVoiceVolume.value * 100);
menubarItemVolumeVocalsOpponent.value = Std.int(save.chartEditorOpponentVoiceVolume.value * 100);
menubarItemPlaybackSpeed.value = Std.int(save.chartEditorPlaybackSpeed.value * 100.0);
menubarItemPlaybackSpeed.value = Math.round(save.chartEditorPlaybackSpeed.value * 100.0);
}
public function writePreferences(hasBackup:Bool):Void
@@ -3027,7 +3057,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
{
if (!isCursorOverHaxeUI && !isPlaytesting)
{
this.openCharacterDropdown(CharacterType.DAD, true);
if (pressingControl())
{
this.setToolboxState(CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT, true);
}
else
{
this.openCharacterDropdown(CharacterType.DAD, true);
}
}
});
@@ -3035,7 +3072,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
{
if (!isCursorOverHaxeUI && !isPlaytesting)
{
this.openCharacterDropdown(CharacterType.BF, true);
if (pressingControl())
{
this.setToolboxState(CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT, true);
}
else
{
this.openCharacterDropdown(CharacterType.BF, true);
}
}
});
@@ -3287,6 +3331,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
menubarItemSelectNone.onClick = _ -> performCommand(new DeselectAllItemsCommand());
menubarItemSelectBeforePlayhead.onClick = _ -> performCommand(new SelectAllItemsBetweenTimeCommand(scrollPositionInMs + playheadPositionInMs, true, true, true));
menubarItemSelectAfterPlayhead.onClick = _ -> performCommand(new SelectAllItemsBetweenTimeCommand(scrollPositionInMs + playheadPositionInMs, false, true, true));
menubarItemPlaytestFull.onClick = _ -> testSongInPlayState(false);
menubarItemPlaytestMinimal.onClick = _ -> testSongInPlayState(true);
@@ -3442,7 +3490,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
if (audioInstTrack != null) audioInstTrack.volume = volume;
menubarLabelVolumeInstrumental.text = 'Instrumental - ${Std.int(event.value)}%';
};
previousAudioVolumes[3] = menubarLabelVolumeInstrumental.value;
previousAudioVolumes[3] = menubarItemVolumeInstrumental.value;
menubarItemVolumeVocalsPlayer.onChange = event ->
{
@@ -3450,7 +3498,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
audioVocalTrackGroup.playerVolume = volume;
menubarLabelVolumeVocalsPlayer.text = 'Player - ${Std.int(event.value)}%';
};
previousAudioVolumes[4] = menubarLabelVolumeVocalsPlayer.value;
previousAudioVolumes[4] = menubarItemVolumeVocalsPlayer.value;
menubarItemVolumeVocalsOpponent.onChange = event ->
{
@@ -3458,7 +3506,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
audioVocalTrackGroup.opponentVolume = volume;
menubarLabelVolumeVocalsOpponent.text = 'Enemy - ${Std.int(event.value)}%';
};
previousAudioVolumes[5] = menubarLabelVolumeVocalsOpponent.value;
previousAudioVolumes[5] = menubarItemVolumeVocalsOpponent.value;
menubarItemPlaybackSpeed.onChange = event ->
{
@@ -3497,6 +3545,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
function copySelection():Void
{
clipboardDirty = true;
clipboardValid = true;
// Doesn't use a command because it's not undoable.
// Calculate a single time offset for all the notes and events.
@@ -3593,7 +3643,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
displayAutosavePopup = false;
var absoluteBackupsPath:String = Path.join([Sys.getCwd(), ChartEditorImportExportHandler.BACKUPS_PATH]);
this.infoWithActions('Auto-Save', 'Chart auto-saved to ${absoluteBackupsPath}.', [{
text: "Take Me There",
text: "Open In Folder",
callback: openBackupsFolder,
}]);
}
@@ -4058,7 +4108,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
eventSprite.parentState = this;
// trace('Creating new Event... (${renderedEvents.members.length})');
if (eventData?.value != null && (eventData?.value?.ease != null && eventData?.value?.easeDir == null))
if (eventData?.value != null && (eventData.getString("ease") != null && eventData.getInt("easeDir") == null))
{
eventData.value = migrateEventEaseDirectionFields(eventData.value);
}
@@ -4505,7 +4555,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
}
// HOME = Scroll to Top
if (FlxG.keys.justPressed.HOME)
if (!FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.HOME)
{
// Scroll amount is the difference between the current position and the top.
scrollAmount = 0 - this.scrollPositionInPixels;
@@ -4521,7 +4571,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
}
// END = Scroll to Bottom
if (FlxG.keys.justPressed.END)
if (!FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.END)
{
// Scroll amount is the difference between the current position and the bottom.
scrollAmount = this.songLengthInPixels - this.scrollPositionInPixels;
@@ -5169,8 +5219,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
{
if (dragLengthCurrent != dragLengthSteps)
{
stretchySounds = !stretchySounds;
this.playSound(Paths.sound('chartingSounds/stretch' + (stretchySounds ? '1' : '2') + '_UI'));
this.playStretchySound();
dragLengthCurrent = dragLengthSteps;
}
@@ -5847,8 +5896,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
if (playheadDragLengthCurrent[column] != targetNoteLengthStepsInt)
{
stretchySounds = !stretchySounds;
this.playSound(Paths.sound('chartingSounds/stretch' + (stretchySounds ? '1' : '2') + '_UI'));
this.playStretchySound();
playheadDragLengthCurrent[column] = targetNoteLengthStepsInt;
}
ghostHold.visible = true;
@@ -6213,6 +6261,26 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Deselect all items.
performCommand(new DeselectAllItemsCommand());
}
// SHIFT + Home = Select all above playhead
if (FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.HOME)
{
// CTRL + SHIFT + Home = Inverse - deselect all above playhead
if (FlxG.keys.pressed.CONTROL)
performCommand(new DeselectAllItemsBetweenTimeCommand(scrollPositionInMs + playheadPositionInMs, true, true, true));
else
performCommand(new SelectAllItemsBetweenTimeCommand(scrollPositionInMs + playheadPositionInMs, true, true, true));
}
// SHIFT + End = Select all below playhead
if (FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.END)
{
// CTRL + SHIFT + Home = Inverse - deselect all below playhead
if (FlxG.keys.pressed.CONTROL)
performCommand(new DeselectAllItemsBetweenTimeCommand(scrollPositionInMs + playheadPositionInMs, false, true, true));
else
performCommand(new SelectAllItemsBetweenTimeCommand(scrollPositionInMs + playheadPositionInMs, false, true, true));
}
}
/**
@@ -6479,6 +6547,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
LoadingState.loadPlayState(targetStateParams, false, true, function(targetState)
{
if (playbarHeadDragging) playbarHeadDragging = false;
// Apply volume settings.
if (playtestAudioSettings)
{
@@ -6924,7 +6993,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
{
var absoluteBackupsPath:String = Path.join([Sys.getCwd(), ChartEditorImportExportHandler.BACKUPS_PATH]);
this.infoWithActions('Auto-Save', 'Chart auto-saved to ${absoluteBackupsPath}.', [{
text: "Take Me There",
text: "Open In Folder",
callback: openBackupsFolder,
}]);
});
@@ -7066,6 +7135,50 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
menubarItemRedo.text = 'Redo ${redoHistory[redoHistory.length - 1].toString()}';
}
}
if (clipboardDirty)
{
clipboardDirty = false;
if (funkin.util.ClipboardUtil.getClipboard() == null || !clipboardValid)
{
menubarItemPaste.disabled = true;
menubarItemPasteUnsnapped.disabled = true;
clipboardValid = false;
}
else if (clipboardValid)
{
menubarItemPaste.disabled = false;
menubarItemPasteUnsnapped.disabled = false;
}
}
if (editButtonsDirty)
{
editButtonsDirty = false;
if (currentEventSelection.length > 0 || currentNoteSelection.length > 0)
{
menubarItemCopy.disabled = false;
menubarItemCut.disabled = false;
menubarItemDelete.disabled = false;
menubarItemSelectNone.disabled = false;
}
else
{
menubarItemCopy.disabled = true;
menubarItemCut.disabled = true;
menubarItemDelete.disabled = true;
menubarItemSelectNone.disabled = true;
}
if (currentNoteSelection.length > 0)
{
menubarItemFlipNotes.disabled = false;
}
else
{
menubarItemFlipNotes.disabled = true;
}
}
}
/**
@@ -42,6 +42,7 @@ class AddEventsCommand implements ChartEditorCommand
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.editButtonsDirty = true;
state.sortChartData();
}
@@ -52,10 +53,12 @@ class AddEventsCommand implements ChartEditorCommand
state.currentNoteSelection = [];
state.currentEventSelection = [];
state.playSound(Paths.sound('chartingSounds/undo'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.editButtonsDirty = true;
state.sortChartData();
}
@@ -42,6 +42,7 @@ class AddNotesCommand implements ChartEditorCommand
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.editButtonsDirty = true;
state.sortChartData();
}
@@ -56,6 +57,7 @@ class AddNotesCommand implements ChartEditorCommand
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.editButtonsDirty = true;
state.sortChartData();
}
@@ -42,6 +42,8 @@ class CopyItemsCommand implements ChartEditorCommand
});
performVisuals(state);
state.clipboardDirty = true;
state.clipboardValid = true;
}
function performVisuals(state:ChartEditorState):Void
@@ -39,6 +39,9 @@ class CutItemsCommand implements ChartEditorCommand
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.editButtonsDirty = true;
state.clipboardDirty = true;
state.clipboardValid = true;
state.sortChartData();
}
@@ -53,6 +56,7 @@ class CutItemsCommand implements ChartEditorCommand
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.editButtonsDirty = true;
state.sortChartData();
}
@@ -0,0 +1,135 @@
package funkin.ui.debug.charting.commands;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongDataUtils;
/**
* Command that deselects all selected notes and/or events above or past the time given in the chart editor.
*/
@:nullSafety
@:access(funkin.ui.debug.charting.ChartEditorState)
class DeselectAllItemsBetweenTimeCommand implements ChartEditorCommand
{
var time:Float;
var above:Bool;
var notes:Array<SongNoteData>;
var events:Array<SongEventData>;
var shouldDeselectNotes:Bool;
var shouldDeselectEvents:Bool;
public function new(time:Float, above:Bool, shouldDeselectNotes:Bool, shouldDeselectEvents:Bool)
{
this.time = time;
this.above = above;
this.notes = [];
this.events = [];
this.shouldDeselectNotes = shouldDeselectNotes;
this.shouldDeselectEvents = shouldDeselectEvents;
}
public function execute(state:ChartEditorState):Void
{
if (above)
{
if (shouldDeselectNotes)
{
for (i in 0...state.currentSongChartNoteData.length)
{
if (state.currentSongChartNoteData[i].time < time)
notes.push(state.currentSongChartNoteData[i]);
else
// We've reached the end of the notes above this time,
// there's no reason to waste our time running this loop to completion
break;
}
}
if (shouldDeselectEvents)
{
for (i in 0...state.currentSongChartEventData.length)
{
if (state.currentSongChartEventData[i].time < time)
events.push(state.currentSongChartEventData[i]);
else
break;
}
}
}
else // Deselecting below the time given
{
if (shouldDeselectNotes)
{
for (i in 0...state.currentSongChartNoteData.length)
{
// Backwards for loop (kinda). Neat!
if (state.currentSongChartNoteData[state.currentSongChartNoteData.length - i - 1].time > time)
notes.push(state.currentSongChartNoteData[state.currentSongChartNoteData.length - i - 1]);
else
// We've reached the end of the notes below this time,
// there's no reason to waste our time running this loop to completion
break;
}
}
if (shouldDeselectEvents)
{
for (i in 0...state.currentSongChartEventData.length)
{
if (state.currentSongChartEventData[state.currentSongChartEventData.length - i - 1].time > time)
events.push(state.currentSongChartEventData[state.currentSongChartEventData.length- i - 1]);
else
break;
}
}
}
state.currentNoteSelection = SongDataUtils.subtractNotes(state.currentNoteSelection, this.notes);
state.currentEventSelection = SongDataUtils.subtractEvents(state.currentEventSelection, this.events);
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
}
public function undo(state:ChartEditorState):Void
{
for (note in this.notes)
{
state.currentNoteSelection.push(note);
}
for (event in this.events)
{
state.currentEventSelection.push(event);
}
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
}
public function shouldAddToHistory(state:ChartEditorState):Bool
{
// This command is undoable. Add to the history if we actually performed an action.
return (notes.length > 0 || events.length > 0);
}
public function toString():String
{
var isPlural = (notes.length + events.length) > 1;
var notesOnly = (notes.length > 0 && events.length == 0);
var eventsOnly = (notes.length == 0 && events.length > 0);
if (notesOnly)
{
return 'Deselect ${notes.length} ${isPlural ? 'Notes' : 'Note'}';
}
else if (eventsOnly)
{
return 'Deselect ${events.length} ${isPlural ? 'Events' : 'Event'}';
}
return 'Deselect ${notes.length + events.length} Items';
}
}
@@ -27,6 +27,7 @@ class DeselectAllItemsCommand implements ChartEditorCommand
state.currentEventSelection = [];
state.noteDisplayDirty = true;
state.editButtonsDirty = true;
}
public function undo(state:ChartEditorState):Void
@@ -35,6 +36,7 @@ class DeselectAllItemsCommand implements ChartEditorCommand
state.currentEventSelection = previousEventSelection;
state.noteDisplayDirty = true;
state.editButtonsDirty = true;
}
public function shouldAddToHistory(state:ChartEditorState):Bool
@@ -28,6 +28,7 @@ class DeselectItemsCommand implements ChartEditorCommand
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.editButtonsDirty = true;
}
public function undo(state:ChartEditorState):Void
@@ -44,6 +45,7 @@ class DeselectItemsCommand implements ChartEditorCommand
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.editButtonsDirty = true;
}
public function shouldAddToHistory(state:ChartEditorState):Bool
@@ -29,6 +29,7 @@ class InvertSelectedItemsCommand implements ChartEditorCommand
state.currentEventSelection = SongDataUtils.subtractEvents(state.currentSongChartEventData, previousEventSelection);
state.noteDisplayDirty = true;
state.editButtonsDirty = true;
}
public function undo(state:ChartEditorState):Void
@@ -37,6 +38,7 @@ class InvertSelectedItemsCommand implements ChartEditorCommand
state.currentEventSelection = previousEventSelection;
state.noteDisplayDirty = true;
state.editButtonsDirty = true;
}
public function shouldAddToHistory(state:ChartEditorState):Bool
@@ -15,12 +15,16 @@ class MoveEventsCommand implements ChartEditorCommand
var events:Array<SongEventData>;
var movedEvents:Array<SongEventData>;
var offset:Float;
var setPos:Bool;
public function new(events:Array<SongEventData>, offset:Float)
public function new(events:Array<SongEventData>, offset:Float, setPos:Bool = false, offsetInSteps:Bool = false)
{
// Clone the notes to prevent editing from affecting the history.
// Clone the events to prevent editing from affecting the history.
this.events = [for (event in events) event.clone()];
if (offsetInSteps) this.offset = Conductor.instance.getStepTimeInMs(offset);
else
this.offset = offset;
this.setPos = setPos;
this.movedEvents = [];
}
@@ -32,8 +36,11 @@ class MoveEventsCommand implements ChartEditorCommand
for (event in events)
{
// Clone the notes to prevent editing from affecting the history.
// Clone the events to prevent editing from affecting the history.
var resultEvent = event.clone();
// If setting position, use the offset as the resulting time
if (setPos) resultEvent.time = offset.clamp(0, Conductor.instance.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio)));
else
resultEvent.time = (resultEvent.time + offset).clamp(0, Conductor.instance.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio)));
movedEvents.push(resultEvent);
@@ -19,12 +19,15 @@ class MoveItemsCommand implements ChartEditorCommand
var offset:Float;
var columns:Int;
public function new(notes:Array<SongNoteData>, events:Array<SongEventData>, offset:Float, columns:Int)
public function new(notes:Array<SongNoteData>, events:Array<SongEventData>, offset:Float, columns:Int, offsetInSteps:Bool = false,
roundStepTimeToNearestStep:Bool = false)
{
// Clone the notes to prevent editing from affecting the history.
this.notes = notes.clone();
this.events = events.clone();
this.offset = offset;
if (offsetInSteps) this.offset = Conductor.instance.getStepTimeInMs(offset);
else
this.offset = offset;
this.columns = columns;
this.movedNotes = [];
this.movedEvents = [];
@@ -15,13 +15,17 @@ class MoveNotesCommand implements ChartEditorCommand
var movedNotes:Array<SongNoteData>;
var offset:Float;
var columns:Int;
var setPos:Bool;
public function new(notes:Array<SongNoteData>, offset:Float, columns:Int)
public function new(notes:Array<SongNoteData>, offset:Float, columns:Int, setPos:Bool = false, offsetInSteps:Bool = false)
{
// Clone the notes to prevent editing from affecting the history.
this.notes = [for (note in notes) note.clone()];
this.offset = offset;
if (offsetInSteps) this.offset = Conductor.instance.getStepTimeInMs(offset);
else
this.offset = offset;
this.columns = columns;
this.setPos = setPos;
this.movedNotes = [];
}
@@ -35,6 +39,9 @@ class MoveNotesCommand implements ChartEditorCommand
{
// Clone the notes to prevent editing from affecting the history.
var resultNote = note.clone();
// If setting position, use the offset as the resulting time
if (setPos) resultNote.time = offset.clamp(0, Conductor.instance.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio)));
else
resultNote.time = (resultNote.time + offset).clamp(0, Conductor.instance.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio)));
resultNote.data = ChartEditorState.gridColumnToNoteData((ChartEditorState.noteDataToGridColumn(resultNote.data) + columns).clamp(0,
ChartEditorState.STRUMLINE_SIZE * 2 - 1));
@@ -22,18 +22,27 @@ class PasteItemsCommand implements ChartEditorCommand
var removedNotes:Array<SongNoteData> = [];
var isRedo:Bool = false;
var currentClipboard:SongClipboardItems =
{
valid: false,
notes: [],
events: []
};
public function new(targetTimestamp:Float)
{
this.targetTimestamp = targetTimestamp;
// Doing this here so that clearing or changing the clipboard doesn't break the redo and string.
this.currentClipboard = SongDataUtils.readItemsFromClipboard();
}
public function execute(state:ChartEditorState):Void
{
var currentClipboard:SongClipboardItems = SongDataUtils.readItemsFromClipboard();
if (currentClipboard.valid != true)
{
state.error('Failed to Paste', 'Could not parse clipboard contents.');
state.clipboardDirty = true;
state.clipboardValid = false;
return;
}
@@ -57,6 +66,7 @@ class PasteItemsCommand implements ChartEditorCommand
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.editButtonsDirty = true;
state.sortChartData();
@@ -93,6 +103,7 @@ class PasteItemsCommand implements ChartEditorCommand
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.editButtonsDirty = true;
state.sortChartData();
@@ -107,8 +118,6 @@ class PasteItemsCommand implements ChartEditorCommand
public function toString():String
{
var currentClipboard:SongClipboardItems = SongDataUtils.readItemsFromClipboard();
var len:Int = currentClipboard.notes.length + currentClipboard.events.length;
if (currentClipboard.notes.length == 0) return 'Paste $len Events';

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