1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2025-09-01 11:25:12 +00:00

Compare commits

..

136 commits

Author SHA1 Message Date
Til 7392371a39
Merge 11bb30cd67 into 2a38359443 2025-07-19 23:58:19 -05:00
Abnormal 2a38359443 we give the people the week 7 tankman fix 2025-07-19 23:56:00 -05:00
TechnikTil 11bb30cd67
change asset commit to be the same as develop 2025-07-18 09:48:04 -06:00
TechnikTil b772bcfb2f
fully remake pausing during a cutscene 2025-07-18 09:44:44 -06:00
Abnormal 203e8eb372 bump da polymod woohoo 2025-07-17 00:22:09 -05:00
Eric e097553f7d Bump polymod again 2025-07-17 00:22:09 -05:00
Eric a15e3810fe Bump Polymod version 2025-07-17 00:22:09 -05:00
Eric 039e1b07c9 Update hmm.json 2025-07-17 00:22:09 -05:00
EliteMasterEric 250c661576 Update Polymod and HScript to their latest experimental builds. 2025-07-17 00:22:09 -05:00
TechnikTil ec464969b4 make currentCharacterId the actual current character ID. also get the style from the style id instead of the character 2025-07-17 04:27:10 +08:00
Smokey 0cab4ae169
openalsoft audio fix (#3318)
Co-authored-by: cyn0x8 <cyn0x8+git@gmail.com>
Co-authored-by: Cobalt Bar <79053181+CobaltBar@users.noreply.github.com>
Co-authored-by: Hundrec <hundrecard@gmail.com>
2025-07-17 03:30:57 +08:00
Abnormal 41984e78af fix: Fix a bug preventing FNF Legacy files from being imported on macOS 2025-07-09 05:28:08 -05:00
Hundrec 467a54e212 assets submod bump 2025-07-09 18:19:22 +08:00
cyn0x8 b5bfb27185 timer sequence 2025-07-09 01:03:20 -05:00
Abnormal 229803dc05 remove a useless boolean from freeplay 2025-07-08 18:47:12 +08:00
Lasercar c6c5f0c028 Selection context menu fix
Can someone please tell me why there's a button to select everything in the selection context menu????
2025-07-08 16:18:05 +08:00
Kolo 7508e4e1a6 maz................................................................. 2025-07-08 16:04:36 +08:00
Lasercar 03e8ef80c2 Open hold note context menu if note is hold note 2025-07-08 15:57:32 +08:00
VioletSnowLeopard 91cb1da992 start gameover retry music after stopping death quotes 2025-07-07 23:39:26 +08:00
Lasercar dff8bc623d Sort the default difficulties 2025-07-07 22:55:40 +08:00
Abnormal 159e3b8f29
i merged while looking i promise please please please don't execute me 2025-07-07 14:50:46 +00:00
Kolo 719d115616 spare some script class pie for sparrow dawg 2025-07-07 09:47:50 -05:00
Keoiki 6674c1583b Load FreeplayStyles on asset reload. 2025-07-07 20:16:46 +08:00
Kolo 1937a4a04e cancel tweens of txtCopyNotif 2025-07-07 04:07:32 -05:00
T5mpler 5fb445b99b Fix a bug where holding down a note after dropping a previous one would sometimes not make its cover show. 2025-06-29 22:30:59 -05:00
lemz 911c70311b squashed commits 2025-06-28 15:16:48 -05:00
cyn0x8 fe335be607 MathUtil additions + lerp fix 2025-06-28 14:17:38 +08:00
Abnormal 33e8eed060 submod update.... 2025-06-27 20:08:11 -05:00
Abnormal f35bc4e944 another day another submod update 2025-06-26 23:43:19 -05:00
Abnormal 4b82b001c6 yet another submod update 2025-06-26 22:41:45 -05:00
Lasercar fad7528372 Fix chart(er)
The charter is literally never ever set in the new or clone function of the songMetadata. HOW????
2025-06-26 22:40:11 -05:00
VioletSnowLeopard 85cce30ec8 fix null safety for CompiledClassList.get
Co-Authored-By: Hyper_ <40342021+NotHyper-474@users.noreply.github.com>
2025-06-26 16:49:58 +08:00
VioletSnowLeopard b52a7d08ff null safe registries in funkin.data 2025-06-26 16:49:58 +08:00
Hyper_ 137ffb91c9 fix: (and refactor) Fix some issues with chart editor sustain trails
Fixes the sustain height not being updated when undoing/redoing length commands
Fixes hanging duplicate sustain trail when dragging the notes
Refactored the displayed hold note sprite kill checks and removed an unnecessary check
2025-06-26 11:35:06 +08:00
Lasercar f531cf3141 mute/ zero volume disables visualiser 2025-06-25 22:00:27 -05:00
MrMadera 8179beca84
[ENHANCEMENT] Set Default Gamepad Controls for Freeplay Menu, including the new bindings added in 0.6.0 (#4559) 2025-06-26 10:19:40 +08:00
Lasercar 909669f0dc Null safe some graphics classes 2025-06-25 21:35:21 +08:00
Hyper_ 81aca6a045 chore: Add null-safety for some classes in funkin.ui 2025-06-25 21:33:47 +08:00
Kolo 2ed1ebb8bc hundrec's req changes 2025-06-24 16:40:27 -05:00
Kolo 508540e117 remove the greed from weekend 1's title 2025-06-24 16:40:27 -05:00
Abnormal dca47d382c the evil is defeated 2025-06-24 16:18:11 -05:00
Abnormal d12d01c402 fix: Check if TERM starts with xterm instead of directly checking 2025-06-24 16:18:11 -05:00
VioletSnowLeopard d10f2a4b82 refresh song list when changing difficulties 2025-06-25 04:36:13 +08:00
Abnormal 5700eba599 feat: add perfect (gold) to results debug 2025-06-24 14:46:05 -05:00
anysad 85de2a9246 bye combomilestone comments 2025-06-23 23:32:16 -05:00
Abnormal d9ffe55d06 update submod 2025-06-23 21:42:45 -05:00
Hyper_ e28fc1478d Add labels for current beat and step in ChartEditorState (replaces previous display) 2025-06-23 21:40:47 -05:00
Hyper_ a772fd2a20 Fix Debug Results not showing proper rank 2025-06-23 21:26:11 -05:00
Hyper_ 0d32f15658 i hate these yanderedev ass if-elses but I just wanna fix this bug 2025-06-23 18:33:45 -05:00
Hyper_ bd3cd1fd3b chore: Add null-safety for Leaderboards and Medals 2025-06-24 05:34:10 +08:00
cyn0x8 9e4365e43f scriptable class changes 2025-06-23 16:25:48 -05:00
Kolo cacd044467 requested changes
frankly i have no idea if this works or not because my computer crashed trying to compile this
2025-06-23 16:13:34 -05:00
Kolo 589c01972d easy pico shall not be easy default....... 2025-06-23 16:13:34 -05:00
VioletSnowLeopard 460536beba null safety for legacy importer 2025-06-24 04:35:34 +08:00
Abnormal 98c9cee395 chore: Add null safety for most of the classes in funkin.audio.* 2025-06-24 04:23:05 +08:00
Lasercar f17aa92f60 NuN safety 2025-06-24 04:08:07 +08:00
Lasercar a36a3f0576 Null safety
For real this time!
2025-06-24 03:55:58 +08:00
Lasercar a4ee5f9048 Text Null Safety 2025-06-24 03:31:40 +08:00
anysad 1c68fbac61 goodbye nulls! 2025-06-24 03:15:39 +08:00
Hyper_ f1346feffb chore: Add null safety to various utility and plugin classes
And add a bit of error handling to CharSelectGF & CharSelectSubState

Co-Authored-By: Linus Torvalds <torvalds@linux-foundation.org>
2025-06-23 13:22:24 -05:00
Lasercar b1019b822f funkin null safe 2025-06-23 12:50:59 -05:00
Hyper_ fbb07501d2 last adjustment before I'm brutally murdered by the Funkin'™ GitHub mods
this changes it so the editor is more responsible for updating the hold notes, to make it similar to how it handles tap notes.
I still kept the position update on overrideStepTime to match the tap note behavior.
Also now the hold note's direction is updated along with the position

And finally replaced an bigass if-else expression with an utility function that's also used by the tap note sprite code.
2025-06-23 03:36:06 +08:00
Hyper_ 8018cf6ff0 for some reason this fixes the "hanging" sustain trail gaaahh there's some fucked up shit going on here 2025-06-23 03:36:06 +08:00
Hyper_ 2d14304461 Realizing I could've simply done this instead 2025-06-23 03:36:06 +08:00
Hyper_ e31ef3665a Reduce calls to hold note trail graphic update 2025-06-23 03:36:06 +08:00
Kolo 100a97ba97 clear up styleSheet be4 entering 2025-06-23 03:05:56 +08:00
Hyper_ 1ccd12cfcd
lasercar............................................................ (#5258) 2025-06-20 19:11:53 +00:00
Kolo a3755d113d
the grand reopening (#5233)
Co-authored-by: Kolo <67389779+JustKolosaki@users.noreply.github.com>
2025-06-20 05:46:11 +00:00
Lasercar 451ee3b008
Intro done right (#5248) 2025-06-20 03:19:30 +00:00
CrusherNotDrip 8625f5807a
This should be 0.0 (#5182) 2025-06-19 03:46:34 +00:00
Lasercar 79425bfd62 New highscore plays twice fix 2025-06-19 10:43:02 +08:00
zackaryowo 564b72f90b
[ENHANCEMENT + BUGFIX] Better dialogue wrapping, fix empty text hang (#4671) 2025-06-19 10:37:05 +08:00
JackXson c1ad4426f2
"Auto Pause" -> "Pause on Unfocus" (#5050) 2025-06-18 06:54:52 +00:00
Lasercar f597a87d10 I.. quit (to the menu)! 2025-06-18 11:13:12 +08:00
Lasercar 15f07e8e26 There's a reason it's called the MAIN menu 2025-06-18 11:13:12 +08:00
Lasercar cb60bb9b68 Stage editor - Ctrl+N new stage
Also windows target configuration preset for straight to stage editor (not to be confused with the stage builder)
2025-06-18 11:13:12 +08:00
Kolo e563c2c59d
2bugs2fix (#5245)
Co-authored-by: Kolo <67389779+JustKolosaki@users.noreply.github.com>
2025-06-17 23:18:26 +00:00
Lasercar 65a2673e38
[ENHANCEMENT] Stage Editor - Closeable toolboxes (#5238)
* Close the toolboxes

* Get deselected
2025-06-17 21:52:10 +00:00
Lasercar 8fcc841e79 Fix Are you sure?
Also they now dance to Artistic Expression
2025-06-18 04:50:56 +08:00
Hyper_ 9626f354ef
The sounds PR fixening (#5235)
* aaaaAAAAAAAAAAAAAAAA

Actually fix vocals (and other sounds) playing in the chart editor......
...I think

* forgot vocals aren't included
2025-06-17 18:50:44 +00:00
Abnormal 57ee401f8e now it builds??????????????????? what the fUCKKKKKKKK 2025-06-16 23:44:00 -05:00
Abnormal a4ef2514c1 hyper........................................................................ 2025-06-16 23:37:33 -05:00
Abnormal dcc989782e fix a build error 2025-06-16 23:15:25 -05:00
Abnormal 6b712e71e6 update assets submod for stage editor files 2025-06-16 23:10:49 -05:00
100ec bbcc5ab81e Bump assets submod for develop testing 2025-06-17 11:38:39 +08:00
VioletSnowLeopard 1c59256871
[BUGFIX] Fix Two Issues With Song Text on Freeplay Capsules (#5036)
* fix song text remaining highlighted

unintentionally fixes song text squishing

* Fix squashed text for real this time
2025-06-17 01:14:32 +00:00
CrusherNotDrip 3bb5d7c12f
Fix crash when mashing I or D on title state. (#5160) 2025-06-17 01:14:21 +00:00
Hyper_ cb9e335d79
fix: Cleanup on LatencyState not being performed when closed/destroyed by state switch (#5085) 2025-06-17 01:14:08 +00:00
Lasercar c59fb53142
no one was around to fix this bug (#4128) 2025-06-17 01:13:56 +00:00
Lasercar afb90615e5
Cancel state change on debug menu (#4211) 2025-06-17 01:13:36 +00:00
Lasercar 5333cdf0bc
ctrl click on hold note null reference fix (#4203) 2025-06-17 01:13:07 +00:00
Hyper_ 36cef9f915
Fix possible crash when trying to open nonexistent folders (#4940) 2025-06-17 01:12:50 +00:00
Lasercar cc8aff94a8
Chart editor state: open target song difficulty & variation (#4116)
* chart editor target song difficulty parameter

* chart editor target song variation parameter

* fixed success message (also forgor playstate change)

* Difficulty no longer always set to normal
2025-06-17 01:12:38 +00:00
Hundrec 9420842f5e
[ENHANCEMENT] Raise maximum FPS cap to 500 (#5044)
* Raise max fps cap to 360

* Make it 500, actually
2025-06-17 01:11:20 +00:00
Hundrec 63da7fcab1
Fix chart editor playbar ms display (#4257) 2025-06-17 01:11:02 +00:00
anysad 456596a34b
fix playhead width (#5090) 2025-06-17 01:10:40 +00:00
Hundrec e640ba1274
Prevent the playhead from scrolling before song start (#5024) 2025-06-17 01:10:29 +00:00
Lasercar 42226e32f2
Remove JPEG (#4895) 2025-06-17 01:10:01 +00:00
charlesisfeline 08d1eff7af
silly typo... (#4773) 2025-06-17 01:09:33 +00:00
anysad 3d65a18c00
bye bye trace! (#5097) 2025-06-17 01:09:23 +00:00
Hyper_ 568e57c32f
Remove V-Sync option from PreferencesMenu on web builds (#5062) 2025-06-17 01:09:06 +00:00
Abnormal c0f9281ee4
chore: Format the project.hxp file (#4762) 2025-06-17 01:08:28 +00:00
Hyper_ 621b4b8ccc
Polymod: Blacklist funkin.util.macro.* (#5185)
It has `CompiledClassList` which allows access to `sys` and Newgrounds API functions.
2025-06-17 01:08:14 +00:00
JackXson 940ece9c07
[BUGFIX] Fix Latency State Exiting to Main Menu Instead of Options Menu (#5076)
* latency state exits to options state

* OptionsState now remembers selection

---------

Co-authored-by: Abnormal <86753001+AbnormalPoof@users.noreply.github.com>
2025-06-17 01:07:37 +00:00
Hundrec c9405ec609
Always display charSelectHint after unlocking character (#5023) 2025-06-17 01:05:38 +00:00
VioletSnowLeopard 62058ee971
Remove this unnecessary line (#5043)
`curSelected` is always set to 0 directly above this
2025-06-17 01:04:52 +00:00
Lasercar 9a01083d9f
no character? (#5008) 2025-06-17 01:03:23 +00:00
Lasercar 64f25395c5
Fake rank now visible (#4986)
Also fixes the song text clipping the rank area when getting a new rank
2025-06-17 01:01:56 +00:00
VioletSnowLeopard bdc044ce04
Fix combo drop animations (#4968) 2025-06-17 01:01:42 +00:00
Abnormal 9cffb3fd6b
[BUGFIX] Fix the game crashing when hot-reloading with F5 (#5065)
* fix: Fix the game crashing when hot reloading with F5

* NOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO

Co-authored-by: Hundrec <hundrecard@gmail.com>

---------

Co-authored-by: Hundrec <hundrecard@gmail.com>
2025-06-17 01:01:19 +00:00
Lasercar 50a8e47293
Clear un/redo history on song load (#4308) 2025-06-17 00:52:23 +00:00
Hyper_ 923078bd57
Remove unnecessary Tracy frame mark (#5004)
This is already done by OpenFL
2025-06-17 00:51:16 +00:00
Lasercar 7c136ff38f
Add all tween types (#4249) 2025-06-17 00:50:45 +00:00
zackaryowo 01b8f69553
[BUGFIX] Fix Audio/Visual Offset causing skips on song start (#3732)
* Fix audio offset interactions with song start

Don't use combined offset here-- using it will cause the instrumental to skip forwards due to your offset. Just use instrumental offset, and don't play it when the song starts-- let resyncVocals do that

* Alter countdown + conductor behavior

Conductor's minimum songPosition when music is playing is now combinedOffset.

resyncVocals is also no longer used when the song starts, as it complicates matters and causes weird double-upping whatever due to the song being played, paused, and then played again

* Update source/funkin/Conductor.hx

thank you https://github.com/cyn0x8 for reminding me FlxMath.bound exists

Co-Authored-By: cyn <cyn0x8+git@gmail.com>

* Oops, don't need this here

Thank you @NotHyper-474!

* Fixed instrumentalOffset goofiness :D

---------

Co-authored-by: cyn <cyn0x8+git@gmail.com>
Co-authored-by: Abnormal <86753001+AbnormalPoof@users.noreply.github.com>
2025-06-17 00:48:23 +00:00
Lasercar fd53ff89a4
Open chart editor on selected song in freeplay state (#4114)
* Open chart editor in freeplay

* idk

* Load a random song when opened on random
2025-06-17 00:44:05 +00:00
Kolo 657564322d
another 20 trillion sandboxed classes (#5040)
Co-authored-by: Kolo <67389779+JustKolosaki@users.noreply.github.com>
2025-06-17 00:42:46 +00:00
Hundrec 605b5ba5c2
Remove alphabetical sort from Favorites (#3609) 2025-06-17 00:42:28 +00:00
Hundrec 5a4d8344d2
Hide cursor when Title Screen starts (#4520) 2025-06-17 00:42:18 +00:00
Hundrec 1794a9e57e
[CHORE] Fix some typos in PlayState.hx (#4333)
* change -> chance

* Remove extra "lose"

* Fix two more misspellings in PlayState

Co-Authored-By: VioletSnowLeopard <202548129+violetsnowleopard@users.noreply.github.com>

* Finish Eric's sentences in PlayState.hx

---------

Co-authored-by: VioletSnowLeopard <202548129+violetsnowleopard@users.noreply.github.com>
Co-authored-by: Abnormal <86753001+AbnormalPoof@users.noreply.github.com>
2025-06-17 00:41:11 +00:00
Ralty 39c2e7136d
Fix Lit Up being impossible to submit score into (#4577) 2025-06-17 00:40:18 +00:00
Kolo d1d96f58e2
move da hold note trails (#4127)
Co-authored-by: Kolo <67389779+JustKolosaki@users.noreply.github.com>
2025-06-17 00:40:09 +00:00
Kolo 3b667044e1
save ALL params grrrrr (#4956)
Co-authored-by: Kolo <67389779+JustKolosaki@users.noreply.github.com>
2025-06-17 00:38:28 +00:00
Lasercar 726cda48c9
Use first difficulty as fallback (#4949) 2025-06-17 00:38:09 +00:00
Hyper_ e736229024
[BUGFIX] Fix Countdown Stacking upon Restarting (#4875)
* run checks if timer's running

* Improve vwoosh timer behaviour (no freezing the whole game this time)

* Prevent vwoosh timer from running outside PlayState

---------

Co-authored-by: Kolo <67389779+JustKolosaki@users.noreply.github.com>
2025-06-17 00:35:19 +00:00
Lasercar 1fecb6ea3e
Chart Editor - Save all audio settings (#4149)
* Save chart editor vocal volume and playback speed
Also opponent hitsounds

* Whoops, didn't save and load the stuff properly

* I sawed this playbackspeed in half!
2025-06-17 00:35:03 +00:00
VioletSnowLeopard 4294e9a6ec
set selected after changing difficulties or unfavoriting a song (#4677) 2025-06-17 00:34:40 +00:00
Abnormal a73be72ba0
fix: Remove spammy trace calls from DiscordClient (#4207) 2025-06-17 00:33:33 +00:00
Hyper_ 7947ecaa00
Fix implementation of fastIndexOf causing duplicate notes in displayedNoteData (#5073) 2025-06-17 00:33:18 +00:00
Abnormal 5801411be0
feat: Options Menu scrolling (#4706) 2025-06-17 00:28:03 +00:00
Hyper_ f16e42368b
Make sound effects pause with the game (#4058) 2025-06-17 00:27:52 +00:00
Hyper_ a9bd80d5ec
[ENHANCEMENT] Prevent Stacked Notes (#3574)
* Initial commit

* use chunking

i tested it out, and it is faster than the previous implementation

* WIP implementation of no stacking when pasting

* Better method name and remove unnecessary variable

* Fix wrong notes being removed from notesB

* Hopefully temporary implementation of note overwrite

* Small cleanup and refactors

* Note overwrite on paste again

* Small refactor; reduce default threshold

* Hopefully should be good to go now.

* Add support for overlapping notes in chart editor preview
Also fixes some inconsistencies in the documentation

* Respect style guide

Optional parameters and parameters with default values should be mutually exclusive.

* Fix scrolling causing performance loss

* Switch to snap-based threshold instead
(No idea if this was implemented correctly)

* Shift + Delete for deleting all stacked notes in chart or selection

* Ignore events when wanting to delete stacked notes

* aaa this just feels wrong

* Don't make event selection box red if there's a stacked note selected

* Fix deleting all stacked notes action not working

* Add 'Edit -> Delete Stacked' menu item and some fixes

* Make undo feel less weird
Actually correct implementation of  threshold (maybe)
Minor bugfixes

* Some stuff I forgor 💀

* use only when ONLY only is only

Co-authored-by: Hundrec <hundrecard@gmail.com>

* This minor spelling mistake will be the bane of me

Co-authored-by: Hundrec <hundrecard@gmail.com>

* Hundrecard's paste note text suggestion

* Fix a minor bug with RemoveStackedNotesCommand

* Singular text for singular note

* Use dropdown for threshold (thanks Hundrec for the suggestion!)

Also R.I.P True...

* - Revert snaps order
- Actually change default threshold to match default dropdown value
- Increase epsilon on threshold check

* Clarify how threshold works differently in the UI

Co-authored-by: Hundrec <hundrecard@gmail.com>

* Fix `Exact` threshold check

* Fix a bug with RemoveStackedNotesCommand, properly this time
Also fixes bug where listStackedNotes could include a stacked note twice
Also minor optimization?

* Warn user about stacked notes when opening song

* Change back to fastContains since the PR that fixes `fastIndexOf` was accepted

---------

Co-authored-by: lemz1 <ismael.amjad07@gmail.com>
Co-authored-by: Hundrec <hundrecard@gmail.com>
2025-06-17 00:27:30 +00:00
Lasercar 908d6ca834
Chart editor - Load meta/chartdata fix (#4278)
* Load meta/chartdata fix
Deletes the song serialiser class, it has no use anymore
Also fixes the BPM changes
Note style is properly set when metadata toolbox is refreshed
Add variation dialog now fills note style dropdown with note styles and sets it properly afterwards
Also makes a few optimisations for the chart editor

* Try push unique the difficulties for the notes
2025-06-17 00:27:08 +00:00
Kolo 7be65fc2bd
[BUGFIX/ENHANCEMENT] Miscellaneous Stage Editor Bugfixes and Missing Features (#3974)
* stage editor bugfixes + features :D

* Merge branch 'develop' of https://github.com/FunkinCrew/Funkin into stage-editor-bugfix-n-stuff

* even more fixes and missing features

* delete logic fix + 2 new feats

feat 1: new objects now have the zIndex 1 higher than the last one (thanks hundrec)
feat 2: chars to test as are now saved (thanks imverybad)

* compilation fix

---------

Co-authored-by: Kolo <67389779+JustKolosaki@users.noreply.github.com>
2025-06-17 00:26:06 +00:00
KutikiPlayz 62b2815d04
notes move freaking normally (#3544) 2025-06-17 00:25:39 +00:00
Hyper_ caf56d496c
fix: Clear waveform data when destroying audio (#5231)
This fixes an issue where recycled sounds would use the previous sound's waveform data.
2025-06-17 00:25:24 +00:00
Lasercar 2a815e91db
Charselect remember character (#4072) 2025-06-17 00:24:54 +00:00
218 changed files with 4587 additions and 15869 deletions

View file

@ -25,8 +25,6 @@ body:
- Itch.io (Downloadable Build) - Windows
- Itch.io (Downloadable Build) - MacOS
- Itch.io (Downloadable Build) - Linux
- Google Playstore - Android
- App Store - iOS
- Compiled from GitHub Source Code
validations:
required: true

View file

@ -1,6 +1,3 @@
# Builds the game on all platforms, to ensure it compiles on all target platforms.
# This helps to ensure workers focus on the master branch.
name: Build and Upload nightly game builds
on:
@ -24,7 +21,7 @@ jobs:
trigger-build: ${{ steps.should-trigger.outputs.result }}
steps:
- name: Checkout repo
uses: funkincrew/ci-checkout@v7.3.3
uses: funkincrew/ci-checkout@v6
with:
submodules: false
- uses: dorny/paths-filter@v3
@ -96,7 +93,7 @@ jobs:
packages: write
steps:
- name: Checkout repo
uses: funkincrew/ci-checkout@v7.3.3
uses: funkincrew/ci-checkout@v6
with:
submodules: false
- name: Log into GitHub Container Registry
@ -128,9 +125,6 @@ jobs:
runs-on: windows
- target: macos
runs-on: macos
# TODO: Install XCode to build iOS
# - target: ios
# runs-on: macos
runs-on:
- ${{ matrix.runs-on }}
defaults:
@ -150,14 +144,11 @@ jobs:
private-key: ${{ secrets.APP_PEM }}
owner: ${{ github.repository_owner }}
- name: Checkout repo
uses: funkincrew/ci-checkout@v7.3.3
uses: funkincrew/ci-checkout@v6
with:
submodules: 'recursive'
token: ${{ steps.app_token.outputs.token }}
persist-credentials: false
submodule-aliases: |
https://github.com/FunkinCrew/Funkin.assets > https://github.com/FunkinCrew/Funkin-assets-secret
https://github.com/FunkinCrew/Funkin.art > https://github.com/FunkinCrew/Funkin-art-secret
- name: Setup build environment
uses: ./.github/actions/setup-haxe
with:
@ -199,7 +190,6 @@ jobs:
include:
- target: linux
- target: html5
# - target: android
defaults:
run:
shell: bash
@ -214,14 +204,11 @@ jobs:
private-key: ${{ secrets.APP_PEM }}
owner: ${{ github.repository_owner }}
- name: Checkout repo
uses: funkincrew/ci-checkout@v7.3.3
uses: funkincrew/ci-checkout@v6
with:
submodules: 'recursive'
token: ${{ steps.app_token.outputs.token }}
persist-credentials: false
submodule-aliases: |
https://github.com/FunkinCrew/Funkin.assets > https://github.com/FunkinCrew/Funkin-assets-secret
https://github.com/FunkinCrew/Funkin.art > https://github.com/FunkinCrew/Funkin-art-secret
- name: Config haxelib
run: |
haxelib --never newrepo

View file

@ -1,6 +1,3 @@
# When a pull request is merged, cancel all queued workflows for that branch
# This helps to ensure workers focus on the master branch.
name: Cancel queued workflows on PR merge
on:
@ -26,7 +23,7 @@ jobs:
let branch_workflows = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: "build-game.yml",
workflow_id: "build-shit.yml",
status: "queued",
branch: "${{ github.event.pull_request.head.ref }}"
});

View file

@ -1,6 +1,5 @@
# Perform actions when labels are applied to issues, discussions, or pull requests
# See .github/label-actions.yml
name: 'Label Actions'
on:

View file

@ -1,13 +1,9 @@
# Applies the following labels to pull requests when created:
# - status: pending triage
name: "Pull Request Labeler 2 (Runs on PR creation)"
on:
pull_request_target:
types:
- opened
jobs:
# Apply `status: pending triage` to newly created pull requests
apply-pending-triage:

View file

@ -1,19 +1,7 @@
# Applies the following labels to pull requests whenver they are created or modified:
# - pr: documentation
# - pr: haxe
# - pr: github
# - size: tiny
# - size: small
# - size: medium
# - size: large
# - size: huge
# see .github/labeler.yml and .github/changed-lines-count-labeler.yml
name: "Pull Request Labeler"
on:
- pull_request_target
jobs:
# Apply labels to pull requests based on which files were edited
labeler:

9
.gitignore vendored
View file

@ -3,21 +3,14 @@
.vs/
APIStuff.hx
dump/
temp/
docs/temp/
export/
astc-textures/
project/
RECOVER_*.fla
shitAudio/
.build_time
.swp
NewgroundsCredentials.hx
# Mobile signing stuff
.apple/
.env
key.keystore
# Exclude JS stuff
node_modules/
package.json

View file

@ -1,66 +0,0 @@
{
"type": "object",
"properties": {
"dependencies": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"title": "Name",
"type": "string",
"description": "The name of the Haxe library"
},
"type": {
"title": "Type",
"type": "string",
"description": "one of haxelib, git, hg, or dev",
"enum": ["haxelib", "git", "hg", "dev"]
},
"ref": {
"type": "string",
"description": "the git/hg ref (branch, commit, tag, etc.)"
},
"url": {
"title": "URL",
"type": "string",
"description": "the git/hg URL"
},
"version": {
"title": "Version",
"type": "string",
"description": "the haxelib library version. Must be in SemVer format (MAJOR.MINOR.PATCH)",
"pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"
}
},
"required": ["name", "type"],
"dependentSchemas": {
"type": {
"if": {
"properties": {
"type": {
"const": "git"
}
}
},
"then": {
"required": ["url"]
},
"else": {
"if": {
"properties": {
"type": {
"const": "haxelib"
}
}
},
"then": {
"required": ["version"]
}
}
}
}
}
}
}
}

15
.vscode/settings.json vendored
View file

@ -90,8 +90,6 @@
"haxecheckstyle.configurationFile": "checkstyle.json",
"haxecheckstyle.codeSimilarityBufferSize": 100,
"lime.projectFile": "project.hxp",
"lime.targetConfigurations": [
{
"label": "Windows / Debug (Discord)",
@ -170,7 +168,7 @@
{
"label": "Windows / Debug (Debug hxvlc)",
"target": "windows",
"args": ["-debug", "-DHXVLC_LOGGING", "-DFEATURE_DEBUG_FUNCTIONS"]
"args": ["-debug", "-DHXC_LIBVLC_LOGGING", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Straight to Animation Editor)",
@ -213,10 +211,6 @@
"label": "Debug",
"args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "Debug (Unlock Everything)",
"args": ["-debug", "-DUNLOCK_EVERYTHING"]
},
{
"label": "Debug (Tracy)",
"args": ["-debug", "-DFEATURE_DEBUG_TRACY", "-DFEATURE_DEBUG_FUNCTIONS"]
@ -241,12 +235,5 @@
"coverage.xml",
"jacoco.xml",
"coverage.cobertura.xml"
],
"vscord.app.privacyMode.enable": true,
"json.schemas": [
{
"fileMatch": ["/hmm.json"],
"url": "./.vscode/schema/hmm.json"
}
]
}

View file

@ -4,303 +4,6 @@ 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.7.3] - 2025-07-21
### Fixed
- Fixed stuttering throughout the game caused by the Polymod upgrade. (Thanks NotHyper-474!)
- [MOBILE] Fixed buttons in the Main Menu not working.
- [iOS] The Upgrade button no longer appears if you have already purchased it (actually this time).
- Fixed the countdown overlapping itself when restarting the song. (Thanks NotHyper-474!)
- Optimized the Week 6 Erect stage.
- Fixed an oversight when clearing the cache. (Thanks cherrythecool!)
- The Input Offset Test menu text now displays in the correct position.
- Fixed script errors appearing in the Week 3 Erect stage.
- Fixed adding variations in the Chart Editor erasing difficulties. (Thanks NotHyper-474!)
## New Contributors for 0.7.3
* @cherrythecool made their first contribution in [#5458](https://github.com/FunkinCrew/Funkin/pull/5458)
## [0.7.2] - 2025-07-18
### Added
- [ANDROID] Added a button in the Options menu to access the mods folder.
- [MOBILE] Added a preference to adjust the intensity of haptic feedback, ranging from 0.1 to 5.
- [MOBILE] Added an easter egg when tapping the player's healthbar icon.
### Changed
- Changed default OpenAL configuration settings to improve audio quality. (Thanks Smokey555, cyn0x8, and CCobaltDev!)
- The difference may be more or less noticeable on different devices and hardware.
- Applies to Desktop and Android, but not iOS yet.
- Made several improvements to Polymod and HScript. These changes might break some mods, so please update them accordingly!
- Fixed an issue where scripted classes can define two or more fields with the same name.
- Fixed an issue causing some syntax errors (such as missing commas) to be ignored by the parser.
- Scripted classes can now create static fields and functions.
- Scripted classes can now create variables with the `final` keyword.
- Scripted classes can now access variables from another scripted class with `class.someVariable`, instead of `class.scriptGet("someVariable")`
- This applies to functions too: `class.someFunction()`
- Scripted classes that don't extend another class can now be created!
- This only works if you access the class in a static context. Creating an instance of said class doesn't work just yet!
- Added support for properties (`get_` and `set_` functions) (Thanks KoloInDaCrib!)
- Added support for abstracts in a static context. (Thanks lemz1!)
- You can now use classes like `FlxColor` properly!
- Added support for creating and using enums. (Thanks lemz1!)
- You can import them in another script as usual.
- Added support for renaming imported classes using the `as` keyword. (Thanks KoloInDaCrib!)
- Fixed `try`/`catch` blocks not working properly. (Thanks NotHyper-474!)
- Fixed null-safe field access not working properly for functions (ex. `class?.someFunction()). (Thanks KoloInDaCrib!)
### Fixed
- [MOBILE] Weekend 1 Story Mode no longer crashes before loading into Blazin'.
- [MOBILE] Beating 2hot from Freeplay no longer crashes in the Results screen.
- [MOBILE] Retrying and pressing the Back button at the same time no longer crashes the game.
- [MOBILE] Pressing the Options and Back buttons at the same time no longer softlocks the game.
- [HTML5] Pausing while the train passes by on the Week 3 Erect stage no longer crashes.
- [DESKTOP] Getting a Bad/Shit rating on Blazin' no longer breaks animations.
- The scroll sound no longer plays once after entering Freeplay.
- The Freeplay song preview and album cover now update properly when switching variations.
- The Freeplay clear percent counter now consistently displays the correct value on unranked songs.
- The Freeplay difficulty star flames no longer become offset from the stars.
- The Freeplay difficulty star flames no longer appear during a new rank animation.
- The Freeplay menu now correctly assigns the `currentCharacterId`. (Thanks TechnikTil!)
- Boyfriend's Perfect (Gold) Results animation now loops properly.
- [DESKTOP] The Input Offsets menu no longer activates the debug cursor.
- The Input Offsets Test menu no longer generates stacked notes.
- The Input Offsets Test menu drums no longer desync from the rest of the track.
- The Input Offsets Test menu no longer breaks when a keyboard or controller is connected.
- [MOBILE] Sustain trails now display properly with upscroll enabled.
- [MOBILE] Added a Back button to the keyboard/gamepad Controls menu.
- [iOS] Fixed app name spacing on the Home Screen.
- [iOS] Adjusted the preloader to accommodate for different screen sizes.
- [iOS] The Upgrade button no longer appears if you have already purchased it.
- [ANDROID] Fixed some issues with scrolling.
- [ANDROID] Toasts with blank messages no longer appear.
- Fixed a critical security vulnerability that could be exploited in mods.
- A few more bugfixes and optimizations here and there.
## New Contributors for 0.7.2
* @Smokey555 made their first contribution in [#3318](https://github.com/FunkinCrew/Funkin/pull/3318)
* @CCobaltDev made their first contribution in [#3318](https://github.com/FunkinCrew/Funkin/pull/3318)
## [0.7.1] - 2025-07-15
### Fixed
- Properly implemented ad playback on iOS devices.
## [0.7.0] - 2025-07-15
### Added
- Friday Night Funkin' now has OFFICIAL mobile versions for Android and iOS, available on the Google Play Store and Apple App Store!
- This version contains 100% of the songs from the desktop version of the game.
- [MOBILE] New touch input compatibility for all menus.
- [MOBILE] Added banner and interstitial advertisements to the game. You can upgrade to the full version through an in-app purchase to permanently disable advertisements.
- [MOBILE] Graphics are compressed using the ASTC algorithm, decreasing memory usage in exchange for a slightly larger file size.
- [MOBILE] Added haptic feedback to several areas of the game.
- Added a visual indicator that shows available difficulties for the currently selected song in Freeplay.
- Overhauled the input offsets system, including:
- One unified "offset" value.
- An "Offset Calibration" screen where the game determines your ideal offset.
- A "Test" screen where you can play a short note pattern to try out your offset.
- A brand new offset testing theme: Syncobation by Kawai Sprite!
- The ability to change your offsets in the Pause Menu, mid-song!
- The Input Offsets menu isn't yet available on HTML5, but offsets are still configurable through the Pause Menu.
- Added null-safety to a bunch of classes in the source code.
- Added the Changelog back to the game files, written by Hundrec and AbnormalPoof!
- Added a few sandboxed classes to give mods limited access to the Discord and Newgrounds APIs. ([50d9584](https://github.com/FunkinCrew/Funkin/commit/50d9584a388bd891aa2f8b68a5cde894a6e1ede6)) - by @KoloInDaCrib in [#5040](https://github.com/FunkinCrew/Funkin/pull/5040)
- Added script support for Freeplay Backing Cards. ([0001017](https://github.com/FunkinCrew/Funkin/commit/0001017c003be653236c6cc56487c7d0ee33633e)) - by @KoloInDaCrib in [#5233](https://github.com/FunkinCrew/Funkin/pull/5233)
- Sparrow results screen animations can now be scriptable. ([7bb2336](https://github.com/FunkinCrew/Funkin/commit/7bb23369727ca4955aa1fbe25e5798809e8169bd)) - by @KoloInDaCrib in [#5168](https://github.com/FunkinCrew/Funkin/pull/5168)
- Added a blank `Object` class for scripts to extend, and made `FlxObject` and `FlxBasic` scriptable. ([eb6becc](https://github.com/FunkinCrew/Funkin/commit/eb6becc03fff76117ee3fcbeb32fe254236ca232)) - by @cyn0x8 in [#3119](https://github.com/FunkinCrew/Funkin/pull/3119)
- Added default gamepad controls for two recently added Freeplay controls. ([a0d3f8e](https://github.com/FunkinCrew/Funkin/commit/a0d3f8ec553e06b625b463c7989658edbebbbdf5)) - by @MrMadera in [#4559](https://github.com/FunkinCrew/Funkin/pull/4559)
- Added the ability to press the Chart Editor keybind in Freeplay with a song capsule selected. ([2221594](https://github.com/FunkinCrew/Funkin/commit/2221594883afa7cd0e518fca7ea975d05626692a)) - by @Lasercar in [#4114](https://github.com/FunkinCrew/Funkin/pull/4114)
- The Chart Editor now highlights and deletes stacked notes using a customizable threshold. ([8cae34e](https://github.com/FunkinCrew/Funkin/commit/8cae34eed711bff70e5348ffc6178a0fd69b5846)) - by @NotHyper-474 in [#3574](https://github.com/FunkinCrew/Funkin/pull/3574)
- Added a variation indicator next to the Chart Editor playbar difficulty. ([ccd0148](https://github.com/FunkinCrew/Funkin/commit/ccd0148e9b46d512a22b4958d3f289cfc7854965)) - by @KoloInDaCrib in [#5236](https://github.com/FunkinCrew/Funkin/pull/5236)
- Added more tween types to certain Chart Editor events. ([5177e12](https://github.com/FunkinCrew/Funkin/commit/5177e1275eb2fb2b016224c139e84debb421b895)) - by @Lasercar in [#4249](https://github.com/FunkinCrew/Funkin/pull/4249)
- Pressing Ctrl + N now creates a new stage in the Stage Editor. ([576f8e5](https://github.com/FunkinCrew/Funkin/commit/576f8e54ff8ca8e205241fafa33d0256b62d11d5)) - by @Lasercar in [#5175](https://github.com/FunkinCrew/Funkin/pull/5175)
- Added "Flip character horizontally" to the list of shortcuts in the Animation Editor. ([c464cae](https://github.com/FunkinCrew/funkin.assets/commit/c464caec921dcefef7b0b74b2abf95e76ce64491)) - by @AbnormalPoof in [funkin.assets#60](https://github.com/FunkinCrew/funkin.assets/pull/60)
- Added Perfect (Gold) to the list of available ranks in Results Debug menu. ([c5308cc](https://github.com/FunkinCrew/Funkin/commit/c5308ccbb9d2b98c62fa4974b8ad7ac1e1ec7d19)) - by @AbnormalPoof in [#4642](https://github.com/FunkinCrew/Funkin/pull/4642)
- [MOBILE] Implemented Kevin and Michael.
### Changed
- The mod API version now supports v0.7.0, along with v0.6.3. Be sure to check that your mods still work!
- Updated the app icon for Desktop platforms.
- [MOBILE] Modified several parts of the game to look better on phone screens with wider aspect ratios, up to 20:9.
- [DESKTOP] The game now tries to match the window's aspect ratio when changing states, extending as wide as 20:9.
- [DESKTOP] Included Mobile stage expansions on Desktop as well. Now you'll have more room for camera events!
- Playable Pico and Weekend 1 songs are now always unlocked in Freeplay, even on new saves.
- The Freeplay difficulty graphic now scrolls smoothly when changing difficulties.
- The "Pause on Unfocus" preference now opens the Pause Menu when unfocusing during a song.
- Scripts can now make hold note trails semi-transparent.
- Completely reformatted every script file within the game's assets for better readability.
- Completely reformatted and optimized every single chart file in the game.
- Recharted pico-speaker's chart in Stress
- Tweaked charts for the following songs:
- Bopeebo [all difficulties] - Removed an extra hey animation event
- Bopeebo (Pico Mix) [Hard] - Added a missing note in Section 24
- Fresh Erect [Nightmare] - Added a missing grace note for BF in Section 24
- South Erect [Nightmare] - Added missing grace notes for BF in Sections 13, 17, and 53
- Philly Nice [Hard] - Added missing grace notes for Pico in Sections 30 and 62
- Philly Nice [all difficulties] - Added hey animations throughout the song
- Philly Nice Erect [Erect] - Added a grace note for BF in Section 33, removed a stacked note for opponent in Section 12
- Philly Nice (Pico Mix) [Normal] - Adjusted a left note by 1/96 in Section 60
- Blammed (Pico Mix) [Hard] - Added a missing jack in Section 46
- Satin Panties [Hard] - Added grace notes in Sections 7-10
- Satin Panties [Normal/Hard] - Made Mom sing a sustain rather than two notes in Section 30
- High Erect [Erect/Nightmare] - Added a missing note in Section 16
- Cocoa [Easy] - Added some notes to reduce sparseness, fixed Mom singing Dad's notes
- Cocoa Erect [Erect/Nightmare] - Reimplemented BF's censored notes for Nightmare, adjusted one note by 1/48 in Section 63
- Eggnog Erect [Erect/Nightmare] Added two grace notes in Sections 10 and 14 and a missing note for Dad in Section 44
- Eggnog (Pico Mix) [Hard] - Added a missing grace note for Pico that was present on Normal in Section 20
- Roses [Normal/Hard] - Made Senpai sing a sustain rather than two notes (sneaky)
- Roses Erect [Erect/Nightmare] - Mirrored the changes from normal Roses
- Guns [all difficulties] - Added a missing note in Sections 28 and 32 and adjusted a hold note's length in Section 73
- Stress [Hard] - Split whole notes in halves in Sections 57-60
- Darnell [Hard] - Added one missing note for Pico in Section 35
- Darnell [all difficulties] - Adjusted camera event timings for consistency
- Darnell (BF Mix) [all difficulties] - Removed 3 extra notes and fixed Darnell's pattern being offset
- Lit Up [all difficulties] - Added 4 sustains for Darnell throughout the song
- Lit Up (BF Mix) [all difficulties] - Added 4 sustains for Darnell throughout the song
- 2hot [Easy/Hard] - Fixed remaining offset rhythms (for real this time)
- Notes now scroll more smoothly by rendering based on delta timing. ([6ad9ffc](https://github.com/FunkinCrew/Funkin/commit/6ad9ffc7f9d66bbaf6ba343663de4c7268f4be3b)) - by @KutikiPlayz in [#3544](https://github.com/FunkinCrew/Funkin/pull/3544)
- The Freeplay character select hint now always displays if you have more than one character unlocked. ([7ccf75c](https://github.com/FunkinCrew/Funkin/commit/7ccf75cd869ba4b6f18a5adc01e65e52ae7bb809)) - by @Hundrec in [#5023](https://github.com/FunkinCrew/Funkin/pull/5023)
- Favorite songs in Freeplay are now sorted by Week order instead of alphabetically. ([da0964a](https://github.com/FunkinCrew/Funkin/commit/da0964a7b7bbe4ece1bdbd19233eb6dba0de3ac5)) - by @Hundrec in [#3609](https://github.com/FunkinCrew/Funkin/pull/3609)
- Shifted Mommy Mearest's pixel icon to the left in Freeplay. ([d861eba](https://github.com/FunkinCrew/funkin.assets/commit/d861ebac027dd07d0254c79d7c89b59ee04b38f1)) - by @KoloInDaCrib in [funkin.assets#197](https://github.com/FunkinCrew/funkin.assets/pull/197)
- The Character Select screen now opens on the currently selected character. ([4819a74](https://github.com/FunkinCrew/Funkin/commit/4819a74c2959cc9b32dfe2cb76c3a4c00e7c7f9a)) - by @Lasercar in [#4072](https://github.com/FunkinCrew/Funkin/pull/4072)
- Visualizers now zero out when the game audio is muted. ([6dcec59](https://github.com/FunkinCrew/Funkin/commit/6dcec592f467a0daeb8ff1e0ce122916e36ca869)) - by @Lasercar in [#5266](https://github.com/FunkinCrew/Funkin/pull/5266)
- The Options Menu can now scroll to display more menu items. ([70f0a54](https://github.com/FunkinCrew/Funkin/commit/70f0a54191597bd72a6d30d4d12ef5ece6ba078c)) - by @AbnormalPoof in [#4706](https://github.com/FunkinCrew/Funkin/pull/4706)
- Raised the FPS cap preference from 300 to 500. ([be73134](https://github.com/FunkinCrew/Funkin/commit/be7313453f70983fff55e69a8b52d741f0cc53b4)) - by @Hundrec in [#5044](https://github.com/FunkinCrew/Funkin/pull/5044)
- The Credits menu now uses less memory, especially with many entries. ([1b68c3a](https://github.com/FunkinCrew/Funkin/commit/1b68c3a8d6f66905a9a508a1cb692fe3beb7b4a2)) - by @lemz1 in [#2655](https://github.com/FunkinCrew/Funkin/pull/2655)
- Added a timer sequence class to queue up multiple timers in scripts with ease. ([9e182f7](https://github.com/FunkinCrew/Funkin/commit/9e182f70d2bcc92eb68d730d74af143c45f7dcf8)) - by @cyn0x8 in [#2391](https://github.com/FunkinCrew/Funkin/pull/2391)
- Replaced smoothLerp and coolLerp with smoothLerpPrecision to fix a few lerp-related bugs. ([94eae11](https://github.com/FunkinCrew/Funkin/commit/94eae116c7a5e6039683d6391208b169378b5ff1)) - by @cyn0x8 in [#3617](https://github.com/FunkinCrew/Funkin/pull/3617)
- Fixed empty text strings softlocking the dialogue box. ([88d0e8c](https://github.com/FunkinCrew/Funkin/commit/88d0e8c3b0529654fb7eee8aebb099f7fb346f66)) - by @xenkap in [#4671](https://github.com/FunkinCrew/Funkin/pull/4671)
- Adjusted the size of the Beat/Step display in the Chart Editor. ([905181c](https://github.com/FunkinCrew/Funkin/commit/905181c9af29bb11280bda33ef9343069678a762)) - by @NotHyper-474 in [#4994](https://github.com/FunkinCrew/Funkin/pull/4994)
- The Chart Editor will now only fall back to the first available difficulty if the selected difficulty cannot be found. ([1c25713](https://github.com/FunkinCrew/Funkin/commit/1c257134648ebd89acf6c9d07f5a0c088fb915c6)) - by @Lasercar in [#4949](https://github.com/FunkinCrew/Funkin/pull/4949)
- The undo/redo history is now cleared when loading another song in the Chart Editor. ([426a9c0](https://github.com/FunkinCrew/Funkin/commit/426a9c0c108ac65a042295194679d46444ec1ea5)) - by @Lasercar in [#4308](https://github.com/FunkinCrew/Funkin/pull/4308)
- Blacklisted more classes for security reasons. ([cadfa3b](https://github.com/FunkinCrew/Funkin/commit/cadfa3b7ceae2ecabe2d544ddc4c9f453b0dfd56)) - by @NotHyper-474 in [#5185](https://github.com/FunkinCrew/Funkin/pull/5185)
### Fixed
- Fixed a ton of performance issues to help the game run better on mobile devices.
- Exiting the Freeplay Menu no longer freezes the game for a really long time (thanks NotHyper-474!)
- Notestyle graphics are now preloaded before the song starts, fixing the stutter at the beginning of the song.
- Hitting many hold note trails in one song no longer leads to a lag spike.
- The first lightning strike in Week 2 Erect no longer creates a lag spike.
- [DESKTOP] The conductor and music no longer gradually drift out of sync to eventually trigger a resync.
- Pixel notestyle strumlines are now properly positioned when Downscroll is enabled.
- Added the missing graffiti to the wall in the Weekend 1 Blazin' stage.
- Accept keybinds now properly scroll faster through the Credits.
- Typing in most text fields in debug editors no longer triggers keyboard shortcuts.
- The Chart Editor playback speed feature now works properly.
- The Chart Editor metronome and hitsounds now play at exactly the right time.
- The Chart Editor notification box no longer covers playbar info.
- Selecting a Recent File too quickly in the Stage Editor no longer crashes the game.
- Blacklisted a few classes for security.
- Opening the logs or backups folder before it's created no longer crashes the game. ([d3490f8](https://github.com/FunkinCrew/Funkin/commit/d3490f8c9929eefb9879ad65ce43038c193642d6)) - by @NotHyper-474 in [#4940](https://github.com/FunkinCrew/Funkin/pull/4940)
- Fixed a crash when mashing D or I during startup. ([b52c73f](https://github.com/FunkinCrew/Funkin/commit/b52c73f2b0fa32fbc349804621cf947dff2d364e)) - by @CrusherNotDrip in [#5160](https://github.com/FunkinCrew/Funkin/pull/5160)
- Hot-reloading with F5 during gameplay no longer crashes the game. ([d2acb5d](https://github.com/FunkinCrew/Funkin/commit/d2acb5d167afd42299d7200ab8c67972044a09c6)) - by @AbnormalPoof in [#5065](https://github.com/FunkinCrew/Funkin/pull/5065)
- Hot-reloading with F5 in the Input Offsets menu no longer crashes the game. ([58257f6](https://github.com/FunkinCrew/Funkin/commit/58257f6ac187925f3b23d3f1eef2c812ae569a6b)) - by @NotHyper-474 in [#5085](https://github.com/FunkinCrew/Funkin/pull/5085)
- Songs no longer skip forward at the beginning with high offsets. ([1f75a64](https://github.com/FunkinCrew/Funkin/commit/1f75a641e0c80d15f1af10bae7ee71a6ffecf219)) - by @xenkap in [#3732](https://github.com/FunkinCrew/Funkin/pull/3732)
- The song countdown no longer stacks when restarting or continues behind the Pause Menu.
([63eca96](https://github.com/FunkinCrew/Funkin/commit/63eca96c98a87e7155df8b2a1735f269ea83e1b5)) - by @KoloInDaCrib and @NotHyper-474 in [#4875](https://github.com/FunkinCrew/Funkin/pull/4875)
- Fixed incorrect highlighting and squashed text on Freeplay song capsules. ([0c62428](https://github.com/FunkinCrew/Funkin/commit/0c62428fc883c0fa6d09cd403efb287ff3af8c53)) - by @VioletSnowLeopard in [#5036](https://github.com/FunkinCrew/Funkin/pull/5036)
- The Freeplay song preview no longer plays twice after returning from Character
Select. ([3d3e2bd](https://github.com/FunkinCrew/Funkin/commit/3d3e2bd3786b858143d214caf55be2ee3e9483fc)) - by @Lasercar in [#5248](https://github.com/FunkinCrew/Funkin/pull/5248)
- Freeplay song ranks no longer disappear after changing variations. ([7cc9464](https://github.com/FunkinCrew/Funkin/commit/7cc9464573d07996e4bd0d557f82847809d3a786)) - by @VioletSnowLeopard in [#4583](https://github.com/FunkinCrew/Funkin/pull/4583)
- Freeplay song capsules now cycle through long names consistently. ([e193f73](https://github.com/FunkinCrew/Funkin/commit/e193f7392a83a04e4aac85fba4f441a78f7b6668)) - by @VioletSnowLeopard in [#4677](https://github.com/FunkinCrew/Funkin/pull/4677)
- Fixed a few visual issues with Freeplay's rank slam animation. ([ab817bb](https://github.com/FunkinCrew/Funkin/commit/ab817bb1eab74ae71c1b1fd74d7512a11e6d4339)) - by @Lasercar in [#4986](https://github.com/FunkinCrew/Funkin/pull/4986)
- Fixed visual errors in Freeplay after exiting Character Select. ([56a18e1](https://github.com/FunkinCrew/Funkin/commit/56a18e1cf6a15971feebcc828be8818037948cef)) - by @KoloInDaCrib in [#5245](https://github.com/FunkinCrew/Funkin/pull/5245)
- Freeplay styles are now reloaded when hot-reloading with F5. ([f54e140](https://github.com/FunkinCrew/Funkin/commit/f54e140b65e36fcf810c52ce464799dbc0c73c6d)) - by @Keoiki in [#5286](https://github.com/FunkinCrew/Funkin/pull/5286)
- Adjusted offsets for Freeplay DJ Pico's fistPump animation. ([382e286](https://github.com/FunkinCrew/funkin.assets/commit/382e286a2478939d3d6aca1c7c90719f33815014)) - by @AbnormalPoof in [funkin.assets#91](https://github.com/FunkinCrew/funkin.assets/pull/91)
- The "New Highscore" text no longer appears more than once in the Results screen. ([4e31003](https://github.com/FunkinCrew/Funkin/commit/4e31003a0f60cd394edc2bd4586f9f385b3d07bc)) - by @Lasercar in [#4319](https://github.com/FunkinCrew/Funkin/pull/4319)
- The Results Debug menu now shows the correct rank after recent scoring changes. ([11d9998](https://github.com/FunkinCrew/Funkin/commit/11d9998e5c73a3839ea39e06b7f217cbaab69c6d)) - by @NotHyper-474 in [#4905](https://github.com/FunkinCrew/Funkin/pull/4905)
- The Input Offsets menu now exits to the Options menu instead of the Main menu. ([5361df2](https://github.com/FunkinCrew/Funkin/commit/5361df254470e68d7571b4534cf456f08d5ffd60)) - by @JackXson-Real in [#5076](https://github.com/FunkinCrew/Funkin/pull/5076)
- The Debug Menu can no longer be opened after selecting an item in the Main Menu.
([5695bc2](https://github.com/FunkinCrew/Funkin/commit/5695bc20e721f0afd5b97a36167f13e550c12b16)) - by @Lasercar in [#4211](https://github.com/FunkinCrew/Funkin/pull/4211)
- The debug cursor is now always hidden when the game starts. ([6222c38](https://github.com/FunkinCrew/Funkin/commit/6222c389e301fa2bb4939697376c4e51e29a9977)) - by @Hundrec in [#4520](https://github.com/FunkinCrew/Funkin/pull/4520)
- The Story Mode Weekend 1 level title no longer clips into other level titles below it. ([19d1a8c](https://github.com/FunkinCrew/Funkin/commit/19d1a8c59380009f0d0814c94fd0b4eccb0c80cd)) - by @KoloInDaCrib in [#4348](https://github.com/FunkinCrew/Funkin/pull/4348)
- Hold note covers now display properly if a hold note was previously dropped. ([96d1324](https://github.com/FunkinCrew/Funkin/commit/96d1324af140858cb93edd07dc36a969c8ae84c0)) - by @T5mpler in [#5275](https://github.com/FunkinCrew/Funkin/pull/5275)
- Girlfriend's and Nene's combo drop animations now play consistently. ([34d5ed1](https://github.com/FunkinCrew/Funkin/commit/34d5ed11695cef7348c13505f13fb1da38b7988c)) - by @VioletSnowLeopard in [#4968](https://github.com/FunkinCrew/Funkin/pull/4968)
- Girlfriend (Tankman Stickup) now plays her combo drop animation. ([e329601](https://github.com/FunkinCrew/funkin.assets/commit/e329601834f270910ce80ea5539e4487b9895a8a)) - by @qt2k4 in [funkin.assets#149](https://github.com/FunkinCrew/funkin.assets/pull/149)
- Darnell's idle animation now loops consistently. ([df64586](https://github.com/FunkinCrew/funkin.assets/commit/df64586771ea41c544913e049fd0f32bdc655417)) - by @qt2k4 in [funkin.assets#159](https://github.com/FunkinCrew/funkin.assets/pull/159)
- Darnell's kneeCan animation now plays properly in 2hot. ([a2e9931](https://github.com/FunkinCrew/funkin.assets/commit/a2e993167aaa2ff6bff8806940f912e444608645)) - by @biomseed in [funkin.assets#78](https://github.com/FunkinCrew/funkin.assets/pull/78)
- Otis and Pico (Speaker) no longer spaz out when playtesting Stress. ([3f6d75f](https://github.com/FunkinCrew/funkin.assets/commit/3f6d75f3b6f6c8deb660323e3fb9bf1974c06520)) - by @Lasercar in [funkin.assets#124](https://github.com/FunkinCrew/funkin.assets/pull/124)
- The gasp sound now only plays once in the Week 3 Pico Mix doppelganger cutscene. ([ab4598b](https://github.com/FunkinCrew/funkin.assets/commit/ab4598baf3c6790d30cfb727d04c8b57fa18dd0d)) - by @KoloInDaCrib in [funkin.assets#126](https://github.com/FunkinCrew/funkin.assets/pull/126)
- Fixed a few issues with the train in Week 3. ([8db2426](https://github.com/FunkinCrew/funkin.assets/commit/8db2426991caebde44d19c81b198e8e2ad86f700)) - by @ShadzXD in [funkin.assets#180](https://github.com/FunkinCrew/funkin.assets/pull/180)
- The cars in Week 4 and Weekend 1 no longer get stuck when the song is restarted. ([9c511e3](https://github.com/FunkinCrew/funkin.assets/commit/9c511e371fd08a43ddf834766a6d35667bdef4f7)) - by @MetaBreeze in [funkin.assets#186](https://github.com/FunkinCrew/funkin.assets/pull/186)
- A-Bot's visualizer no longer jumps to a random volume when the song ends. ([51cc118](https://github.com/FunkinCrew/funkin.assets/commit/51cc1186bc77a2ee45a47fdbde2d75d9ec69de3a)) - by @VioletSnowLeopard in [funkin.assets#183](https://github.com/FunkinCrew/funkin.assets/pull/183)
- Pico's burpShit animation now re-enables volume for player vocals. ([cefda0e](https://github.com/FunkinCrew/funkin.assets/commit/cefda0e52fabbbe04afe60b7aed560267e2cb01e)) - by @Hundrec in [funkin.assets#71](https://github.com/FunkinCrew/funkin.assets/pull/71)
- Removed vocals from Monster's instrumental on web builds. ([1c9473f](https://github.com/FunkinCrew/funkin.assets/commit/1c9473f3dfdfb97d97f6c8457001055322abf5ab)) - by @JVNpixels in [funkin.assets#182](https://github.com/FunkinCrew/funkin.assets/pull/182)
- Fixed the retry sound not playing after a Tankman death quote finishes. ([e7c4b1b](https://github.com/FunkinCrew/Funkin/commit/e7c4b1ba38ba0739cfe347f6c4763f9811fb95b0)) - by @VioletSnowLeopard in [#4726](https://github.com/FunkinCrew/Funkin/pull/4726)
- Darnell (BF Mix)'s alternate instrumental is now properly accessible. ([5abdabf](https://github.com/FunkinCrew/funkin.assets/commit/5abdabf69b39ca4eebd36ae6bbd77fab736d0b86)) - by @Hundrec in [funkin.assets#168](https://github.com/FunkinCrew/funkin.assets/pull/168)
- Fixed Newgrounds score submissions for Lit Up and Lit Up (BF Mix). ([183cec6](https://github.com/FunkinCrew/Funkin/commit/183cec62dc1fd3c7f3f634dd3e2400e6ee77b476)) - by @Raltyro in [#4577](https://github.com/FunkinCrew/Funkin/pull/4577)
- Inputs are now disabled before Senpai's dialogue appears. ([c43d906](https://github.com/FunkinCrew/funkin.assets/commit/c43d906d19d91d71b9096e65d5e0d3543af8cd31)) - by @anysad in [funkin.assets#165](https://github.com/FunkinCrew/funkin.assets/pull/165)
- An easter egg now restarts the song using the correct instrumental. ([e657bc9](https://github.com/FunkinCrew/Funkin/commit/e657bc900bc62cc220276dc171dd47f0a176ac66)) - by @KoloInDaCrib in [#4956](https://github.com/FunkinCrew/Funkin/pull/4956)
- Encountering an easter egg during a Chart Editor playtest no longer crashes the game. ([b53b5bd](https://github.com/FunkinCrew/funkin.assets/commit/b53b5bdaecf975555538725a4cdfe71d38565b08)) - by @NotHyper-474 in [funkin.assets#133](https://github.com/FunkinCrew/funkin.assets/pull/133)
- Nonexistent characters no longer crash the Chart Editor. ([3bbb4b0](https://github.com/FunkinCrew/Funkin/commit/3bbb4b06c8c1a1885a18d4354fcfa4363a0c6c75)) - by @Lasercar in [#5008](https://github.com/FunkinCrew/Funkin/pull/5008)
- Holding Ctrl and clicking on a hold note trail no longer crashes the Chart Editor. ([dc56cca](https://github.com/FunkinCrew/Funkin/commit/dc56ccada50e671996caf0557de1977b7ff8d236)) - by @Lasercar in [#4203](https://github.com/FunkinCrew/Funkin/pull/4203)
- Tweens and timers are now canceled when returning to the Chart Editor. ([7e76cf6](https://github.com/FunkinCrew/Funkin/commit/7e76cf66340c00ef6ec84358e6304d62815173b6)) - by @KoloInDaCrib in [#5278](https://github.com/FunkinCrew/Funkin/pull/5278)
- Reduced the severity of a memory leak in the Chart Editor. ([cce8c18](https://github.com/FunkinCrew/Funkin/commit/cce8c18822e083910200597f5db4d87b6e3b521f)) - by @NotHyper-474 in [#5247](https://github.com/FunkinCrew/Funkin/pull/5247)
- Pressing the Chart Editor keybind during a song now opens to the variation and difficulty you were playing. ([e3fca16](https://github.com/FunkinCrew/Funkin/commit/e3fca167938642bea85398fe57347c10439c1892)) - by @Lasercar in [#4116](https://github.com/FunkinCrew/Funkin/pull/4116)
- The Chart Editor now properly saves audio levels when exiting. ([f78ab4d](https://github.com/FunkinCrew/Funkin/commit/f78ab4da1db4f9527a2e1715d5cfb37670e11a74)) - by @Lasercar in [#4149](https://github.com/FunkinCrew/Funkin/pull/4149)
- The Chart Editor "Load Metadata File" and "Load Chart File" buttons now function properly. ([9df5395](https://github.com/FunkinCrew/Funkin/commit/9df5395ff888cb6740e21204c8e19116e0472db4)) - by @Lasercar in [#4278](https://github.com/FunkinCrew/Funkin/pull/4278)
- FNF Legacy files can now be opened in the Chart Editor on MacOS. ([d98628c](https://github.com/FunkinCrew/Funkin/commit/d98628ca0f9f60357715bd7f95fc686a83201209)) - by @AbnormalPoof in [#4580](https://github.com/FunkinCrew/Funkin/pull/4580)
- The Chart Editor now consistently displays the correct waveform for vocal tracks. ([c0e0523](https://github.com/FunkinCrew/Funkin/commit/c0e0523651e8aaaae2a0eed6d5fef6c5ef1b7315)) - by @NotHyper-474 in [#5231](https://github.com/FunkinCrew/Funkin/pull/5231)
- Fixed selection boxes duplicating in the Chart Editor. ([65ed583](https://github.com/FunkinCrew/Funkin/commit/65ed58350b798bca0044603510540cfe81b48611)) - by @NotHyper-474 in [#5073](https://github.com/FunkinCrew/Funkin/pull/5073)
- Fixed the Chart Editor timer occasionally displaying incorrect millisecond values. ([26dc895](https://github.com/FunkinCrew/Funkin/commit/26dc895a27e0d7e49469251cd68b83be66384e15)) - by @Hundrec in [#4257](https://github.com/FunkinCrew/Funkin/pull/4257)
- The Chart Editor playhead can no longer be scrolled to before the beginning of the song. ([7c7dc11](https://github.com/FunkinCrew/Funkin/commit/7c7dc11f18644882444df97ac927e11adaa4ce50)) - by @Hundrec in [#5024](https://github.com/FunkinCrew/Funkin/pull/5024)
- The Chart Editor playbar no longer extends past the right of the grid. ([c7abb19](https://github.com/FunkinCrew/Funkin/commit/c7abb196989476cfa4db6354cc6fac5bacb2e56a)) - by @anysad in [#5090](https://github.com/FunkinCrew/Funkin/pull/5090)
- Dragging a hold note in the Chart Editor now drags its trail along with its head. ([d3d8aaa](https://github.com/FunkinCrew/Funkin/commit/d3d8aaae7bfbfa8975a9573807b9a3ca68a1ff55)) - by @KoloInDaCrib in [#4127](https://github.com/FunkinCrew/Funkin/pull/4127)
- Hold note trails will no longer disappear when dragged too far in the Chart Editor. ([37dc66b](https://github.com/FunkinCrew/Funkin/commit/37dc66bc189bc1941cf60587e1c068770aeec872)) - by @NotHyper-474 in
[#5261](https://github.com/FunkinCrew/Funkin/pull/5261)
- Undoing and redoing hold note length changes now visually updates the trail in the Chart Editor. ([06a440f](https://github.com/FunkinCrew/Funkin/commit/06a440f21c285666ce2d2bdf17a91ee82ab01061)) - by @NotHyper-474 in [#5265](https://github.com/FunkinCrew/Funkin/pull/5265)
- The Chart Editor hold note context menu now displays the correct options. ([4801316](https://github.com/FunkinCrew/Funkin/commit/48013168ef09ddc09549268a2e5309520d3fcc18)) - by @Lasercar in [#4231](https://github.com/FunkinCrew/Funkin/pull/4231)
- The buttons in the Chart Editor context menu for selections now do the right thing. ([62d24fc](https://github.com/FunkinCrew/Funkin/commit/62d24fcf4cbe7be328a995bad04f3eeb265262c5)) - by @Lasercar in [#4233](https://github.com/FunkinCrew/Funkin/pull/4233)
- The charter field in the song metadata now properly displays the charter in the Chart Editor. ([894d8cb](https://github.com/FunkinCrew/Funkin/commit/894d8cb4637fd0a64359a5edc805a23403f3045c)) - by @Lasercar in [#4879](https://github.com/FunkinCrew/Funkin/pull/4879)
- Chart Editor difficulties are now sorted in a consistent order. ([7aa77a1](https://github.com/FunkinCrew/Funkin/commit/7aa77a11cf7508fc05758045a13b2220aca96dd5)) - by @Lasercar in [#4528](https://github.com/FunkinCrew/Funkin/pull/4528)
- Chart Editor and Stage Editor windows now consistently show a close button. ([b23b7b8](https://github.com/FunkinCrew/funkin.assets/commit/b23b7b81d843c8fa2334fbdbe1ef979633956bb9)) - by @Lasercar in [funkin.assets#121](https://github.com/FunkinCrew/funkin.assets/pull/121)
- The Chart Editor playbar's font size no longer becomes too small. ([f9c1f7a](https://github.com/FunkinCrew/Funkin/commit/f9c1f7a5f7d3906155c2004ad2ec1d02556b9730)) - by @KoloInDaCrib in [#5253](https://github.com/FunkinCrew/Funkin/pull/5253)
- The Chart Editor copy notification no longer chases the mouse cursor. ([0ea42e1](https://github.com/FunkinCrew/Funkin/commit/0ea42e18e93b0b57f1ae5499d70c4681f191dacb)) - by @KoloInDaCrib in [#4029](https://github.com/FunkinCrew/Funkin/pull/4029)
- Changed "Tankman Battlefield (Erect)" to "Tankman Battlefield [Erect]" in the Chart Editor for consistency. ([52852a0](https://github.com/FunkinCrew/funkin.assets/commit/52852a02f60df81c281bfd0e49fd8fa09e118409)) - by @JVNpixels in [funkin.assets#155](https://github.com/FunkinCrew/funkin.assets/pull/155)
- Fixed Pico (Pixel) having the incorrect name in the Chart Editor. ([3ff0e9c](https://github.com/FunkinCrew/funkin.assets/commit/3ff0e9ca4f8cdc5734c335f2d7d72fa310686d46)) - by @ExtraCode75 in [funkin.assets#158](https://github.com/FunkinCrew/funkin.assets/pull/158)
- Fixed various issues and added missing functionalities to the Stage Editor. ([a776ce1](https://github.com/FunkinCrew/Funkin/commit/a776ce1a81a539f75f8bd2220a9768c03d347058)) - by @KoloInDaCrib in [#3974](https://github.com/FunkinCrew/Funkin/pull/3974)
- Stage Editor windows are now able to be closed. ([65461d8](https://github.com/FunkinCrew/Funkin/commit/65461d839b3764659aab33f1a35edf64ee514952)) - by @Lasercar in [#5238](https://github.com/FunkinCrew/Funkin/pull/5238)
- Fixed duplicate exit prompts appearing in the Stage Editor. ([136a5df](https://github.com/FunkinCrew/Funkin/commit/136a5dfad430b461a909be3a7e89f46c8be1d3b3)) - by @Lasercar in [#5239](https://github.com/FunkinCrew/Funkin/pull/5239)
- The help guide in the Stage Editor can no longer be opened multiple times. ([564d679](https://github.com/FunkinCrew/Funkin/commit/564d679f969c500834426ce3cf50507091d69697)) - by @Lasercar in [#4128](https://github.com/FunkinCrew/Funkin/pull/4128)
- Removed a spammy console trace from Spooky Kids (Dark). ([5935a61](https://github.com/FunkinCrew/funkin.assets/commit/5935a61dd71f51e6264dcd039f1164cd1ff295ef)) - by @NotHyper-474 in [funkin.assets#187](https://github.com/FunkinCrew/funkin.assets/pull/187)
- Removed spammy console traces from `DiscordClient`. ([e89f9f5](https://github.com/FunkinCrew/Funkin/commit/e89f9f50dc6085e3550736459ce9e4fd02c1fc5b)) - by @AbnormalPoof in [#4207](https://github.com/FunkinCrew/Funkin/pull/4207)
- Removed a spammy console trace from some Chart Editor events. ([b883ad3](https://github.com/FunkinCrew/Funkin/commit/b883ad3d50b990e605d7b79d042292c7371db5f0)) - by @anysad in [#5097](https://github.com/FunkinCrew/Funkin/pull/5097)
- ANSI colors now display in the console on more computers. ([3747b94](https://github.com/FunkinCrew/Funkin/commit/3747b942461c3644fe31f25d2ce847fd74d1b1e0)) - by @AbnormalPoof in [#4676](https://github.com/FunkinCrew/Funkin/pull/4676)
- Properly blacklist a certain class from scripts. ([3dc7699](https://github.com/FunkinCrew/Funkin/commit/3dc7699aac737998b637ff9a9f16986a434424be)) - by @charlesisfeline in [#4773](https://github.com/FunkinCrew/Funkin/pull/4773)
### Removed
- Removed the VSync preference from web builds, where it's non-functional. ([0b7a94b](https://github.com/FunkinCrew/Funkin/commit/0b7a94b1cc5c5d0dbac5a4c3d595349c4e6eb6e4)) - by @NotHyper-474 in [#5062](https://github.com/FunkinCrew/Funkin/pull/5062)
- Removed a few non-functional screenshot preferences. ([93e4f79](https://github.com/FunkinCrew/Funkin/commit/93e4f799f4f0ea435b3d77ca8ef2ab6beeb0a955)) - by @Lasercar in [#4895](https://github.com/FunkinCrew/Funkin/pull/4895)
## New Contributors for 0.7.0
* @KutikiPlayz made their first contribution in [#3544](https://github.com/FunkinCrew/Funkin/pull/3544)
* @xenkap made their first contribution in [#3732](https://github.com/FunkinCrew/Funkin/pull/3732)
* @Raltyro made their first contribution in [#4577](https://github.com/FunkinCrew/Funkin/pull/4577)
* @charlesisfeline made their first contribution in [#4773](https://github.com/FunkinCrew/Funkin/pull/4773)
* @T5mpler made their first contribution in [#5275](https://github.com/FunkinCrew/Funkin/pull/5275)
* @biomseed made their first contribution in [funkin.assets#78](https://github.com/FunkinCrew/funkin.assets/pull/78)
* @qt2k4 made their first contribution in [funkin.assets#149](https://github.com/FunkinCrew/funkin.assets/pull/149)
* @ExtraCode75 made their first contribution in [funkin.assets#158](https://github.com/FunkinCrew/funkin.assets/pull/158)
* @MetaBreeze made their first contribution in [funkin.assets#186](https://github.com/FunkinCrew/funkin.assets/pull/186)
## [0.6.4] - 2025-05-02
@ -1042,7 +745,7 @@ The Weekend 1 update!
- Improvements to video cutscenes and dialogue, allowing them to be easily skipped or restarted.
- Updated Polymod by several major versions, allowing for fully dynamic asset replacement and support for scripted classes.
- Completely refactored almost every part of the game's code for performance, stability, and extensibility.
- This is not the Ludem Dare game held together with sticks and glue you played three years ago.
- This is not the Ludum Dare game held together with sticks and glue you played three years ago.
- Characters, stages, songs, story levels, and dialogue are now built from JSON data registries rather than being hardcoded.
- All of these also support attaching scripts for custom behavior, more documentation on this soon.
- You can forcibly reload the game's JSON data and scripts by pressing F5.

View file

@ -1,28 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11134" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11106"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<simulatedScreenMetrics key="simulatedMetrics" type="freeform" size="375x667"/>
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
<viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0" green="0" blue="0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>

103
README.md
View file

@ -1,55 +1,48 @@
# Friday Night Funkin'
Friday Night Funkin' is a rhythm game. Built using HaxeFlixel for Ludum Dare 47.
This game was made with love to Newgrounds and its community. Extra love to Tom Fulp.
- [Playable web demo on Newgrounds!](https://www.newgrounds.com/portal/view/770371)
- [Demo download builds for Windows, Mac, and Linux from Itch.io!](https://ninja-muffin24.itch.io/funkin)
- [Download Android builds from Google Play!](https://play.google.com/store/apps/details?id=me.funkin.fnf)
- [Download iOS builds from the App Store!](https://apps.apple.com/app/id6740428530)
# Getting Started
**PLEASE USE THE LINKS ABOVE IF YOU JUST WANT TO PLAY THE GAME**
To learn how to install the necessary dependencies and compile the game from source, please follow our [Compiling Guide](/docs/COMPILING.md).
# Contributing
Check out our [Contributing Guide](/docs/CONTRIBUTING.md) to learn how you can actively contribute to the development of Friday Night Funkin'!
# Modding
Feel free to start learning to mod the game by reading our [documentation](https://funkincrew.github.io/funkin-modding-docs/) and guide to modding.
# Credits and Special Thanks
Full credits can be found in-game, or in the `credits.json` file which is located [here](https://github.com/FunkinCrew/funkin.assets/blob/main/exclude/data/credits.json).
## Programming
- [ninjamuffin99](https://twitter.com/ninja_muffin99) - Lead Programmer
- [EliteMasterEric](https://twitter.com/EliteMasterEric) - Programmer
- [MtH](https://twitter.com/emmnyaa) - Charting and Additional Programming
- [GeoKureli](https://twitter.com/Geokureli/) - Additional Programming
- [ZackDroid](https://x.com/ZackDroidCoder) - Lead Mobile Programmer
- [MAJigsaw77](https://github.com/MAJigsaw77) - Mobile Programmer
- [Karim-Akra](https://x.com/KarimAkra_0) - Mobile Programmer
- [Sector_5](https://github.com/sector-a) - Mobile Programmer
- [Luckydog7](https://github.com/luckydog7) - Mobile Programmer
- Our contributors on GitHub
## Art / Animation / UI
- [PhantomArcade3K](https://twitter.com/phantomarcade3k) - Artist and Animator
- [Evilsk8r](https://twitter.com/evilsk8r) - Art
- [Moawling](https://twitter.com/moawko) - Week 6 Pixel Art
- [IvanAlmighty](https://twitter.com/IvanA1mighty) - Misc UI Design
## Music
- [Kawaisprite](https://twitter.com/kawaisprite) - Musician
- [BassetFilms](https://twitter.com/Bassetfilms) - Music for "Monster", Additional Character Design
## Special Thanks
- [Tom Fulp](https://twitter.com/tomfulp) - For being a great guy and for Newgrounds
- [JohnnyUtah](https://twitter.com/JohnnyUtahNG/) - Voice of Tankman
- [L0Litsmonica](https://twitter.com/L0Litsmonica) - Voice of Mommy Mearest
# Friday Night Funkin'
Friday Night Funkin' is a rhythm game. Built using HaxeFlixel for Ludum Dare 47.
This game was made with love to Newgrounds and its community. Extra love to Tom Fulp.
- [Playable web demo on Newgrounds!](https://www.newgrounds.com/portal/view/770371)
- [Demo download builds for Windows, Mac, and Linux from Itch.io!](https://ninja-muffin24.itch.io/funkin)
# Getting Started
**PLEASE USE THE LINKS ABOVE IF YOU JUST WANT TO PLAY THE GAME**
To learn how to install the necessary dependencies and compile the game from source, please follow our [Compiling Guide](/docs/COMPILING.md).
# Contributing
Check out our [Contributing Guide](/docs/CONTRIBUTING.md) to learn how you can actively contribute to the development of Friday Night Funkin'!
# Modding
Feel free to start learning to mod the game by reading our [documentation](https://funkincrew.github.io/funkin-modding-docs/) and guide to modding.
# Credits and Special Thanks
Full credits can be found in-game, or in the `credits.json` file which is located [here](https://github.com/FunkinCrew/funkin.assets/blob/main/exclude/data/credits.json).
## Programming
- [ninjamuffin99](https://twitter.com/ninja_muffin99) - Lead Programmer
- [EliteMasterEric](https://twitter.com/EliteMasterEric) - Programmer
- [MtH](https://twitter.com/emmnyaa) - Charting and Additional Programming
- [GeoKureli](https://twitter.com/Geokureli/) - Additional Programming
- Our contributors on GitHub
## Art / Animation / UI
- [PhantomArcade3K](https://twitter.com/phantomarcade3k) - Artist and Animator
- [Evilsk8r](https://twitter.com/evilsk8r) - Art
- [Moawling](https://twitter.com/moawko) - Week 6 Pixel Art
- [IvanAlmighty](https://twitter.com/IvanA1mighty) - Misc UI Design
## Music
- [Kawaisprite](https://twitter.com/kawaisprite) - Musician
- [BassetFilms](https://twitter.com/Bassetfilms) - Music for "Monster", Additional Character Design
## Special Thanks
- [Tom Fulp](https://twitter.com/tomfulp) - For being a great guy and for Newgrounds
- [JohnnyUtah](https://twitter.com/JohnnyUtahNG/) - Voice of Tankman
- [L0Litsmonica](https://twitter.com/L0Litsmonica) - Voice of Mommy Mearest

14
alsoft.txt Normal file
View file

@ -0,0 +1,14 @@
[general]
sample-type=float32
period_size=441
stereo-mode=speakers
stereo-encoding=panpot
hrtf=false
cf_level=0
resampler=fast_bsinc24
front-stablizer=false
output-limiter=false
[decoder]
hq-mode=false
distance-comp=false
nfc=false

2
art

@ -1 +1 @@
Subproject commit 490e97f4c6e673a52ee4f9af98325b1aa2d0c3fe
Subproject commit 78dc310219370144719b4eeef9b3b511c5a44532

2
assets

@ -1 +1 @@
Subproject commit a8d15febf5e37a4fc7ffeb0d3eff9c4d36457a37
Subproject commit 88778a44853255b082ada57e9cb77d8fb4956494

View file

@ -70,10 +70,9 @@
},
{
"props": {
"severity": "IGNORE",
"policy": "aligned",
"allowSingleline": true
"allowSingleline": true,
"severity": "INFO"
},
"type": "ConditionalCompilation"
},

View file

@ -87,3 +87,4 @@ filter_commits = false
topo_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "newest"

View file

@ -1,21 +0,0 @@
assets/preload/images/cursor/*
assets/preload/images/freeplay/*
assets/preload/images/icons/*
assets/preload/images/titleEnter.png
assets/preload/images/titleEnter_mobile.png
assets/preload/images/soundtray/*
assets/preload/images/stageBuild/*
assets/preload/images/ui/popup/pixel/
assets/shared/images/characters/abotPixel/
assets/shared/images/characters/bfPixel.png
assets/shared/images/characters/bfPixelsDEAD.png
assets/shared/images/characters/gfPixel.png
assets/shared/images/characters/nenePixel/
assets/shared/images/characters/picoPixel/
assets/shared/images/characters/senpai.png
assets/shared/images/characters/spirit.png
assets/shared/images/resultScreen/*
assets/shared/images/ui/chart-editor/*
assets/shared/images/ui/countdown/pixel/
assets/week1/
assets/week6/*

View file

@ -40,8 +40,8 @@ There are several useful build flags you can add to a build to affect how it wor
- This feature causes the game to load exported assets from the project's assets folder rather than the exported one. Great for fast iteration, but the game will break if you try to zip it up and send it to someone, so it's disabled for release builds.
- `-DFEATURE_DISCORD_RPC` or `-DNO_FEATURE_DISCORD_RPC` to forcibly enable or disable support for Discord Rich Presence.
- `-DFEATURE_VIDEO_PLAYBACK` or `-DNO_FEATURE_VIDEO_PLAYBACK` to forcibly enable or disable video cutscene support.
- `-DFEATURE_SCREENSHOTS` or `-DNO_FEATURE_SCREENSHOTS` to forcibly enable or disable the screenshots feature.
- `-DFEATURE_CHART_EDITOR` or `-DNO_FEATURE_CHART_EDITOR` to forcibly enable or disable the chart editor in the Debug menu.
- `-DFEATURE_SCREENSHOTS` or `-DNO_FEATURE_SCREENSHOTS` to forcibly enable or disable the screenshots feature.
- `-DFEATURE_STAGE_EDITOR` to forcibly enable the experimental stage editor.
- `-DFEATURE_GHOST_TAPPING` to forcibly enable an experimental gameplay change to the anti-mash system.

View file

@ -1,98 +0,0 @@
# Compiling Friday Night Funkin' for Mobile Devices
Before starting, **make sure your game builds on desktop.**
Check [COMPILING.md](./COMPILING.md) if you havent done that yet.
## Android
0. **Create a new folder** this will store Android tools (remember where you put it!).
1. **Open a terminal as Administrator.**
2. Run this in the terminal (replace the path with your actual folder):
```bash
setx ANDROID_HOME "C:\path\to\your\folder" /M
```
3. Download [Android Studio Command-line Tools](https://developer.android.com/studio#command-line-tools-only).
4. Extract the ZIP into your folder from step 1.
5. (Optional) Close and reopen the terminal if needed.
6. Run:
```bash
sdkmanager --install "build-tools;35.0.0" "ndk;29.0.13113456" "platforms;android-29" "platforms;android-35"
```
- The latest NDK is not compatible with Lime you have to use the old one.
7. Download and install [JDK 17 (MSI)](https://adoptium.net/temurin/releases/?version=17&os=windows).
8. Run:
```bash
lime setup android
```
Use these when asked:
- **Android SDK:** `C:\path\to\your\folder`
- **Android NDK:** `C:\path\to\your\folder\ndk\29.0.13113456`
- **JDK:** `C:\Program Files\Java\jdk-17`
9. Now build your game:
```bash
lime test android
```
### macOS
0. **Create a new folder** this will store Android tools (remember where you put it!).
1. Open **Terminal** (Command ⌘ + Space → type “terminal” → Enter).
2. In Terminal:
```bash
cd /path/to/your/folder
export ANDROID_HOME=/path/to/your/folder
export PATH=$PATH:$ANDROID_HOME/cmdline-tools:$ANDROID_HOME/cmdline-tools/bin:$ANDROID_HOME/platform-tools
```
3. Download [Android Studio Command-line Tools](https://developer.android.com/studio#command-line-tools-only).
4. Extract the ZIP into your folder from step 1.
5. (Optional) Restart Terminal if needed.
6. Run:
```bash
sdkmanager --install "build-tools;35.0.0" "ndk;29.0.13113456" "platforms;android-29" "platforms;android-35"
```
7. Download and install [JDK 17 for macOS](https://adoptium.net/temurin/releases/?os=mac&version=17).
8. Run:
```bash
lime setup android
```
Use these when asked:
- **Android SDK:** `/path/to/your/folder`
- **Android NDK:** `/path/to/your/folder/ndk/28.0.13004108`
- **JDK:** `/Library/Java/JavaVirtualMachines/temurin-17.jdk/Contents/Home`
_(If not asked for JDK, dont worry — just skip it.)_
9. Build your game:
```bash
lime test android
```
## iOS
Note that you can only build the game for iOS on a computer running MacOS.
0. Build the game for desktop to make sure everything works. Check [COMPILING.md](./COMPILING.md).
1. Get Xcode from the app store on your MacOS Machine.
2. Download the iPhone SDK (First thing that pops up in Xcode)
3. Open up a terminal tab and run `lime test ios -xcode`
4. You will need to sign your own copy in order to run the game with a real iOS device! That requires an Apple Developer account, sorry!
- To run with an iOS simulator instead of `-xcode` use `-simulator`
### iOS Troubleshooting
- **A required plugin failed to load. Please ensure system content is up-to-date — try running 'xcodebuild -runFirstLaunch'.**
Make sure you have the iOS SDK isntalled, see Step 2.
- **error: No Accounts: Add a new account in Accounts settings. (in target 'Funkin' from project 'Funkin')**
Open XCode, press CMD+, to open Settings, select Accounts, add an Apple ID.
- error: No Account for Team "Z7G7AVNGSH". Add a new account in Accounts settings or verify that your accounts have valid credentials.
Open `project.hxp` and change `IOS_TEAM_ID` to your personal team's ID.
- error: Failed Registering Bundle Identifier: The app identifier "me.funkin.fnf" cannot be registered to your development team because it is not available.
The Funkin' Crew are the only ones that can build an iOS app with the identifier `me.funkin.fnf`. Open `project.hxp` and change `PACKAGE_NAME` to a unique value.
- error: No profiles for 'me.funkin.fnf' were found: Xcode couldn't find any iOS App Development provisioning profiles matching 'me.funkin.fnf'

View file

@ -1,32 +0,0 @@
# HTML5/Web
1. Nope
# Windows
1. Start the game at least once. This will create a `mods` folder if it doesn't already exist, alongside the executable.
2. Extract the mod you downloaded from its ZIP file, and place the mod folder into the game's `mods` folder.
3. Restart the game. The game should detect the mod and start with it.
# Linux
1. Start the game at least once. This will create a `mods` folder if it doesn't already exist, alongside the executable.
2. Extract the mod you downloaded from its ZIP file, and place the mod folder into the game's `mods` folder.
3. Restart the game. The game should detect the mod and start with it.
# MacOS
1. Start the game at least once. This will create a `mods` folder if it doesn't already exist in the game's system files.
2. Right click `Funkin.app` and select `Show Package Contents`.
3. Navigate to `Contents/Resources/mods`.
4. Extract the mod you downloaded from its ZIP file, and place the mod folder into the game's `mods` folder.
5. Restart the game. The game should detect the mod and start with it.
# Android
1. Start the game at least once. This will create a `mods` folder deep in your system files.
2. Get an Android file browser that lets you view the app data files, or use Android Studio and open up the Device Explorer.
3. Locate the `/sdcard/Android/obb/me.funkin.fnf/mods` folder.
4. Extract the mod you downloaded from its ZIP file, and place the mod folder into the game's `mods` folder.
5. Restart the game (you may have to [force close](https://support.google.com/android/answer/9079646?hl=en) the app first). The game should detect the mod and start with it.
# iOS
1. Start the game at least once. This will create a `mods` folder in your system files.
2. Open the Files app, and navigate to `On My iPhone` -> `Friday Night Funkin` -> `mods`.
3. Extract the mod you downloaded from its ZIP file, and place the mod folder into the game's `mods` folder.
4. Restart the game (you may have to [force close](https://support.apple.com/en-us/109359) the app first). The game should detect the mod and start with it.

View file

@ -1,66 +0,0 @@
@echo off
set ZIP_FILE="./temp/_temp_jdk.zip"
set OUTPUT_DIR="./temp/"
set SIX_LINK="https://drive.usercontent.google.com/download?id=1GqFpIk_bkxFb0tNN3x9LxnN-Zh_oDUX5&export=download&authuser=0&confirm=t&uuid=43108c0a-bd53-4465-86f3-80aaceaa7a38&at=APZUnTVNS_BV9cNyC_iicDInosmz%3A1718921284514"
set EIGHT_LINK="https://drive.usercontent.google.com/download?id=1X8jjtYYos8aDfZKwehGS9B3zFQa-sCb-&export=download&authuser=0&confirm=t&uuid=07b24a6c-5352-4ba5-9fb8-cff151a6d91e&at=APZUnTUfw26NBAl0nCMn6HBKgHwK%3A1718922303598"
echo MAKING TEMP
mkdir %OUTPUT_DIR%
echo MADE TEMP
echo INSTALLING ANDROID BUILD TOOLS
call .\asclt\bin\sdkmanager "build-tools;32.0.0" --sdk_root="%LOCALAPPDATA%/Android/Sdk/"
call .\asclt\bin\sdkmanager "build-tools;32.1.0-rc1" --sdk_root="%LOCALAPPDATA%/Android/Sdk/"
echo INSTALLED ANDROID BUILD TOOLS
echo INSTALLING ANDROID SDK
REM First install the sdks
call .\asclt\bin\sdkmanager "platforms;android-29" --sdk_root="%LOCALAPPDATA%/Android/Sdk/"
echo ANDROID SDK INSTALLED
echo INSTALLING ANDROID NDK
REM then the ndks
call ./asclt/bin/sdkmanager "ndk;21.4.7075529" --sdk_root="%LOCALAPPDATA%/Android/Sdk/"
echo ANDROID NDK INSTALLED
echo DOWNLOADING JDK
call curl -o %ZIP_FILE% %SIX_LINK%
echo DOWNLOADED JDK
echo UNZIPPING JDK
call powershell -Command "Expand-Archive -Path '%ZIP_FILE%' -DestinationPath '%OUTPUT_DIR%' -Force"
echo UNZIPPED JDK
echo MAKING JDK PATH
mkdir "%LOCALAPPDATA%/Android/jdk"
echo MADE JDK PATH
echo MOVING JDK TO PROPER PATH
call move "%OUTPUT_DIR%/jdk-17.0.11+9" "%LOCALAPPDATA%/Android/jdk/"
echo MOVED JDK
echo LIME SETTING UP
haxelib run lime config ANDROID_SDK %LOCALAPPDATA%\Android\Sdk
haxelib run lime config ANDROID_NDK_ROOT %LOCALAPPDATA%\Android\Sdk\ndk\21.4.7075529
haxelib run lime config JAVA_HOME %LOCALAPPDATA%\Android\Sdk\jdk\jdk-17.0.11+9
haxelib run lime config ANDROID_SETUP true
echo DONE
pause

View file

@ -1,66 +0,0 @@
@echo off
set ZIP_FILE="./temp/_temp_jdk.zip"
set OUTPUT_DIR="./temp/"
set SIX_LINK="https://drive.usercontent.google.com/download?id=1GqFpIk_bkxFb0tNN3x9LxnN-Zh_oDUX5&export=download&authuser=0&confirm=t&uuid=43108c0a-bd53-4465-86f3-80aaceaa7a38&at=APZUnTVNS_BV9cNyC_iicDInosmz%3A1718921284514"
set EIGHT_LINK="https://drive.usercontent.google.com/download?id=1X8jjtYYos8aDfZKwehGS9B3zFQa-sCb-&export=download&authuser=0&confirm=t&uuid=07b24a6c-5352-4ba5-9fb8-cff151a6d91e&at=APZUnTUfw26NBAl0nCMn6HBKgHwK%3A1718922303598"
echo MAKING TEMP
mkdir %OUTPUT_DIR%
echo MADE TEMP
echo INSTALLING ANDROID BUILD TOOLS
call .\asclt\bin\sdkmanager "build-tools;32.0.0" --sdk_root="%LOCALAPPDATA%/Android/Sdk/"
call .\asclt\bin\sdkmanager "build-tools;32.1.0-rc1" --sdk_root="%LOCALAPPDATA%/Android/Sdk/"
echo INSTALLED ANDROID BUILD TOOLS
echo INSTALLING ANDROID SDK
REM First install the sdks
call .\asclt\bin\sdkmanager "platforms;android-29" --sdk_root="%LOCALAPPDATA%/Android/Sdk/"
echo ANDROID SDK INSTALLED
echo INSTALLING ANDROID NDK
REM then the ndks
call ./asclt/bin/sdkmanager "ndk;21.4.7075529" --sdk_root="%LOCALAPPDATA%/Android/Sdk/"
echo ANDROID NDK INSTALLED
echo DOWNLOADING JDK
call curl -o %ZIP_FILE% %EIGHT_LINK%
echo DOWNLOADED JDK
echo UNZIPPING JDK
call powershell -Command "Expand-Archive -Path '%ZIP_FILE%' -DestinationPath '%OUTPUT_DIR%' -Force"
echo UNZIPPED JDK
echo MAKING JDK PATH
mkdir "%LOCALAPPDATA%/Android/jdk"
echo MADE JDK PATH
echo MOVING JDK TO PROPER PATH
call move "%OUTPUT_DIR%/jdk-17.0.11+9" "%LOCALAPPDATA%/Android/jdk/"
echo MOVED JDK
echo LIME SETTING UP
haxelib run lime config ANDROID_SDK %LOCALAPPDATA%\Android\Sdk
haxelib run lime config ANDROID_NDK_ROOT %LOCALAPPDATA%\Android\Sdk\ndk\21.4.7075529
haxelib run lime config JAVA_HOME %LOCALAPPDATA%\Android\Sdk\jdk\jdk-17.0.11+9
haxelib run lime config ANDROID_SETUP true
echo DONE
pause

View file

@ -6,7 +6,7 @@
"name": "EliteMasterEric"
}
],
"api_version": "0.7.0",
"api_version": "0.5.0",
"mod_version": "1.0.0",
"license": "Apache-2.0"
}

View file

@ -6,7 +6,7 @@
"name": "EliteMasterEric"
}
],
"api_version": "0.7.0",
"api_version": "0.5.0",
"mod_version": "1.0.0",
"license": "Apache-2.0"
}

View file

@ -4,48 +4,14 @@
"name": "FlxPartialSound",
"type": "git",
"dir": null,
"ref": "3c9f63e3501c20c0b60442089dc05306f5a87968",
"url": "https://github.com/FunkinDroidTeam/FlxPartialSound.git"
},
{
"name": "astc-compressor",
"type": "git",
"dir": null,
"ref": "8c9f56927c523df7b849352c6951f04112fe15cc",
"url": "https://github.com/KarimAkra/astc-compressor"
},
{
"name": "extension-admob",
"type": "git",
"dir": null,
"ref": "02334589ff9603a5f483077a44395009644f6274",
"url": "https://github.com/FunkinCrew/extension-admob"
},
{
"name": "extension-androidtools",
"type": "haxelib",
"version": "2.2.2"
},
{
"name": "extension-haptics",
"type": "haxelib",
"version": "1.0.4"
},
{
"name": "extension-iapcore",
"type": "haxelib",
"version": "1.0.4"
},
{
"name": "extension-iarcore",
"type": "haxelib",
"version": "1.0.3"
"ref": "41f35ddb1eb9d10bc742e6f8b5bcc62f9ef8ad84",
"url": "https://github.com/FunkinCrew/FlxPartialSound.git"
},
{
"name": "flixel",
"type": "git",
"dir": null,
"ref": "08fc955ca87f192a971719a675f1d3b21709725d",
"ref": "fffb1a74cf08f63dacc2ab09976340563f5b6e6d",
"url": "https://github.com/FunkinCrew/flixel"
},
{
@ -55,11 +21,18 @@
"ref": "b9118f47f43a66bc0e5fbfcfd9903f0425e918ee",
"url": "https://github.com/FunkinCrew/flixel-addons"
},
{
"name": "flixel-text-input",
"type": "git",
"dir": null,
"ref": "951a0103a17bfa55eed86703ce50b4fb0d7590bc",
"url": "https://github.com/FunkinCrew/flixel-text-input"
},
{
"name": "flxanimate",
"type": "git",
"dir": null,
"ref": "b1faf19885dad06c899cb71ffe07b4e40b8c6d0c",
"ref": "f8842cea9883d6112a2c854cf93fa72856171428",
"url": "https://github.com/FunkinCrew/flxanimate"
},
{
@ -78,8 +51,8 @@
"name": "grig.audio",
"type": "git",
"dir": "src",
"ref": "8567c4dad34cfeaf2ff23fe12c3796f5db80685e",
"url": "https://github.com/FunkinCrew/grig.audio"
"ref": "57f5d47f2533fd0c3dcd025a86cb86c0dfa0b6d2",
"url": "https://gitlab.com/haxe-grig/grig.audio.git"
},
{
"name": "hamcrest",
@ -90,14 +63,14 @@
"name": "haxeui-core",
"type": "git",
"dir": null,
"ref": "47ee9f5fa02d422e186e844f87e68220cabfcc5b",
"ref": "07fc7aa8098deaea633b0726d01f83eb4ef8a832",
"url": "https://github.com/haxeui/haxeui-core"
},
{
"name": "haxeui-flixel",
"type": "git",
"dir": null,
"ref": "100f2c96beab619cfe72c567a058c41c71e3e998",
"ref": "b899a4c7d7318c5ff2b1bb645fbc73728fad1ac9",
"url": "https://github.com/haxeui/haxeui-flixel"
},
{
@ -111,8 +84,8 @@
"name": "hxcpp",
"type": "git",
"dir": null,
"ref": "5a0dc3f644dc676a4a092b7e6c8edc8be941f024",
"url": "https://github.com/FunkinCrew/hxcpp"
"ref": "v4.3.75",
"url": "https://github.com/HaxeFoundation/hxcpp"
},
{
"name": "hxcpp-debug-server",
@ -143,7 +116,7 @@
{
"name": "hxvlc",
"type": "haxelib",
"version": "2.2.2"
"version": "2.1.2"
},
{
"name": "json2object",
@ -170,7 +143,7 @@
"name": "lime",
"type": "git",
"dir": null,
"ref": "e5f8c27124598505917a001588b560244731adfb",
"ref": "be81ad7e4e1a92c3482bcc009648a4ac892cfa35",
"url": "https://github.com/FunkinCrew/lime"
},
{
@ -212,14 +185,14 @@
"name": "openfl",
"type": "git",
"dir": null,
"ref": "a0df7c3afe360c9af59a76e45007dbf4e53b5131",
"ref": "d061c936b462f040304ec2bd42d9f59d2e59e285",
"url": "https://github.com/FunkinCrew/openfl"
},
{
"name": "polymod",
"type": "git",
"dir": null,
"ref": "d4142dd15a3b57ed4eb149f9f6a2c3ad9935bf7b",
"ref": "3e030c81de99ca84acde681431f806d8103bcf6e",
"url": "https://github.com/larsiusprime/polymod"
},
{

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,17 +1,12 @@
package;
import flixel.FlxG;
import flixel.FlxGame;
import flixel.FlxState;
import funkin.ui.FullScreenScaleMode;
import funkin.Preferences;
import funkin.util.logging.CrashHandler;
import funkin.ui.debug.MemoryCounter;
import funkin.save.Save;
import haxe.ui.Toolkit;
#if hxvlc
import hxvlc.util.Handle;
#end
import openfl.display.FPS;
import openfl.display.Sprite;
import openfl.events.Event;
@ -36,15 +31,6 @@ class Main extends Sprite
public static function main():Void
{
// Set the current working directory for Android and iOS devices
#if android
// On Android use External Files Dir.
Sys.setCwd(haxe.io.Path.addTrailingSlash(extension.androidtools.content.Context.getExternalFilesDir()));
#elseif ios
// On iOS use Documents Dir.
Sys.setCwd(haxe.io.Path.addTrailingSlash(lime.system.System.documentsDirectory));
#end
// We need to make the crash handler LITERALLY FIRST so nothing EVER gets past it.
CrashHandler.initialize();
CrashHandler.queryStatus();
@ -111,19 +97,9 @@ class Main extends Sprite
memoryCounter = new MemoryCounter(10, 13, 0xFFFFFF);
#end
#if mobile
// Add this signal so we can reposition and resize the memory and fps counter.
FlxG.signals.preUpdate.add(repositionCounters.bind(true));
#end
// George recommends binding the save before FlxGame is created.
Save.load();
#if hxvlc
// Initialize hxvlc's Handle here so the videos are loading faster.
Handle.init();
#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
@ -133,6 +109,7 @@ class Main extends Sprite
WindowUtil.setVSyncMode(funkin.Preferences.vsyncMode);
var game:FlxGame = new FlxGame(gameWidth, gameHeight, initialState, Preferences.framerate, Preferences.framerate, skipSplash, startFullscreen);
// FlxG.game._customSoundTray wants just the class, it calls new from
@ -147,15 +124,6 @@ class Main extends Sprite
game.debugger.interaction.addTool(new funkin.util.TrackerToolButtonUtil());
#end
#if !html5
FlxG.scaleMode = new FullScreenScaleMode();
#end
#if mobile
// Reposition and resize the memory and fps counter without lerping.
repositionCounters(false);
#end
#if hxcpp_debug_server
trace('hxcpp_debug_server is enabled! You can now connect to the game with a debugger.');
#else
@ -177,56 +145,4 @@ class Main extends Sprite
funkin.input.Cursor.registerHaxeUICursors();
haxe.ui.tooltips.ToolTipManager.defaultDelay = 200;
}
#if mobile
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);
if (fpsCounter != null)
{
fpsCounter.scaleX = fpsCounter.scaleY = scale;
if (FlxG.game != null)
{
if (lerp)
{
fpsCounter.x = flixel.math.FlxMath.lerp(fpsCounter.x, FlxG.game.x + thypos, FlxG.elapsed * 3);
}
else
{
fpsCounter.x = FlxG.game.x + FullScreenScaleMode.notchSize.x + 10;
}
fpsCounter.y = FlxG.game.y + (3 * scale);
}
}
if (memoryCounter != null)
{
memoryCounter.scaleX = memoryCounter.scaleY = scale;
if (FlxG.game != null)
{
if (lerp)
{
memoryCounter.x = flixel.math.FlxMath.lerp(memoryCounter.x, FlxG.game.x + thypos, FlxG.elapsed * 3);
}
else
{
memoryCounter.x = FlxG.game.x + FullScreenScaleMode.notchSize.x + 10;
}
memoryCounter.y = FlxG.game.y + (13 * scale);
}
}
}
#end
}

View file

@ -29,7 +29,7 @@ class Postbuild
var buildTime:Float = roundToTwoDecimals(end - start);
Sys.println('[INFO] Build took: ${buildTime} seconds');
trace('Build took: ${buildTime} seconds');
}
}

View file

@ -9,24 +9,48 @@ class Prebuild
{
static inline final BUILD_TIME_FILE:String = '.build_time';
static final NG_CREDS_PATH:String = './source/funkin/api/newgrounds/NewgroundsCredentials.hx';
static final NG_CREDS_TEMPLATE:String = "package funkin.api.newgrounds;
class NewgroundsCredentials
{
public static final APP_ID:String = #if API_NG_APP_ID haxe.macro.Compiler.getDefine(\"API_NG_APP_ID\") #else 'INSERT APP ID HERE' #end;
public static final ENCRYPTION_KEY:String = #if API_NG_ENC_KEY haxe.macro.Compiler.getDefine(\"API_NG_ENC_KEY\") #else 'INSERT ENCRYPTION KEY HERE' #end;
}";
static function main():Void
{
var start:Float = Sys.time();
Sys.println('[INFO] Performing pre-build tasks...');
trace('Building...');
saveBuildTime();
var end:Float = Sys.time();
var duration:Float = end - start;
Sys.println('[INFO] Finished pre-build tasks in $duration seconds.');
buildCredsFile();
}
static function saveBuildTime():Void
{
// PostBuild.hx reads this file and computes the total build duration.
var fo:sys.io.FileOutput = File.write(BUILD_TIME_FILE);
var now:Float = Sys.time();
fo.writeDouble(now);
fo.close();
}
static function buildCredsFile():Void
{
#if sys
if (sys.FileSystem.exists(NG_CREDS_PATH))
{
trace('NewgroundsCredentials.hx already exists, skipping.');
}
else
{
trace('Creating NewgroundsCredentials.hx...');
var fileContents:String = NG_CREDS_TEMPLATE;
sys.io.File.saveContent(NG_CREDS_PATH, fileContents);
}
#end
}
}

View file

@ -24,27 +24,6 @@ extern class Native_TracyProfiler
@:native('::__hxcpp_tracy_framemark')
public static function frameMark():Void;
/**
Mark a named frame. Allows creating multiple frame sets for different timing categories.
Each unique name creates a separate frame set in the Tracy timeline.
**/
@:native('::__hxcpp_tracy_framemark_named')
public static function frameMarkNamed(_name:String):Void;
/**
Mark the start of a discontinuous frame. Use for periodic work with gaps.
Must be paired with frameMarkEnd() using the same name.
**/
@:native('::__hxcpp_tracy_framemark_start')
public static function frameMarkStart(_name:String):Void;
/**
Mark the end of a discontinuous frame. Use for periodic work with gaps.
Must be paired with frameMarkStart() using the same name.
**/
@:native('::__hxcpp_tracy_framemark_end')
public static function frameMarkEnd(_name:String):Void;
/**
Print a message into Tracy's log.
**/
@ -92,18 +71,6 @@ class Cppia_TracyProfiler
public static function frameMark()
Native_TracyProfiler.frameMark();
@:inheritDoc(cpp.vm.tracy.Native_TracyProfiler.frameMarkNamed)
public static function frameMarkNamed(_name:String)
Native_TracyProfiler.frameMarkNamed(_name);
@:inheritDoc(cpp.vm.tracy.Native_TracyProfiler.frameMarkStart)
public static function frameMarkStart(_name:String)
Native_TracyProfiler.frameMarkStart(_name);
@:inheritDoc(cpp.vm.tracy.Native_TracyProfiler.frameMarkEnd)
public static function frameMarkEnd(_name:String)
Native_TracyProfiler.frameMarkEnd(_name);
@:inheritDoc(cpp.vm.tracy.Native_TracyProfiler.message)
public static function message(_msg:String, ?_color:Int = 0x000000)
Native_TracyProfiler.message(_msg, _color);

View file

@ -9,11 +9,6 @@ import openfl.utils.Future;
@:nullSafety
class Assets
{
/**
* The assets cache.
*/
public static var cache:openfl.utils.IAssetCache = openfl.utils.Assets.cache;
/**
* Get the file system path for an asset
* @param path The asset path to load from, relative to the assets folder

View file

@ -249,18 +249,25 @@ class Conductor
* No matter if you're using a local conductor or not, this always loads
* to/from the save file
*/
public var globalOffset(get, never):Int;
public var inputOffset(get, set):Int;
/**
* An offset set by the user to compensate for audio/visual lag
* No matter if you're using a local conductor or not, this always loads
* to/from the save file
*/
public var audioVisualOffset(get, never):Int;
public var audioVisualOffset(get, set):Int;
function get_globalOffset():Int
function get_inputOffset():Int
{
return Preferences.globalOffset;
return Save?.instance?.options?.inputOffset ?? 0;
}
function set_inputOffset(value:Int):Int
{
Save.instance.options.inputOffset = value;
Save.instance.flush();
return Save.instance.options.inputOffset;
}
function get_audioVisualOffset():Int
@ -268,11 +275,18 @@ class Conductor
return Save?.instance?.options?.audioVisualOffset ?? 0;
}
function set_audioVisualOffset(value:Int):Int
{
Save.instance.options.audioVisualOffset = value;
Save.instance.flush();
return Save.instance.options.audioVisualOffset;
}
public var combinedOffset(get, never):Float;
function get_combinedOffset():Float
{
return instrumentalOffset + formatOffset + globalOffset;
return instrumentalOffset + audioVisualOffset + inputOffset;
}
/**
@ -394,10 +408,8 @@ class Conductor
* @param songPosition The current position in the song in milliseconds.
* Leave blank to use the FlxG.sound.music position.
* @param applyOffsets If it should apply the instrumentalOffset + formatOffset + audioVisualOffset
* @param forceDispatch If it should force the dispatch of onStepHit, onBeatHit, and onMeasureHit
* even if the current step, beat, or measure hasn't changed.
*/
public function update(?songPos:Float, applyOffsets:Bool = true, forceDispatch:Bool = false):Void
public function update(?songPos:Float, applyOffsets:Bool = true, forceDispatch:Bool = false)
{
var currentTime:Float = (FlxG.sound.music != null) ? FlxG.sound.music.time : 0.0;
var currentLength:Float = (FlxG.sound.music != null) ? FlxG.sound.music.length : 0.0;

View file

@ -1,428 +0,0 @@
package funkin;
import flixel.graphics.FlxGraphic;
import flixel.FlxG;
import funkin.play.notes.notestyle.NoteStyle;
import openfl.utils.AssetType;
import openfl.Assets;
import openfl.system.System;
import openfl.media.Sound;
import lime.app.Future;
import lime.app.Promise;
/**
* Handles caching of textures and sounds for the game.
* TODO: Remove this once Eric finishes the memory system.
*/
@:nullSafety
class FunkinMemory
{
static var permanentCachedTextures:Map<String, FlxGraphic> = [];
static var currentCachedTextures:Map<String, FlxGraphic> = [];
static var previousCachedTextures:Map<String, FlxGraphic> = [];
// waow
static var permanentCachedSounds:Map<String, Sound> = [];
static var currentCachedSounds:Map<String, Sound> = [];
static var previousCachedSounds:Map<String, Sound> = [];
static var purgeFilter:Array<String> = ["/week", "/characters", "/charSelect", "/results"];
/**
* Caches textures that are always required.
*/
public static inline function initialCache():Void
{
var allImages:Array<String> = Assets.list();
for (file in allImages)
{
if (!(file.endsWith(".png") #if FEATURE_COMPRESSED_TEXTURES || file.endsWith(".astc") #end)
|| file.contains("chart-editor")
|| !file.contains("ui/"))
{
continue;
}
file = file.replace(" ", ""); // Handle stray spaces.
if (file.contains("shared") || Assets.exists('shared:$file', AssetType.IMAGE))
{
file = 'shared:$file';
}
permanentCacheTexture(file);
}
permanentCacheTexture(Paths.image("healthBar"));
permanentCacheTexture(Paths.image("menuDesat"));
permanentCacheTexture(Paths.image("notes", "shared"));
permanentCacheTexture(Paths.image("noteSplashes", "shared"));
permanentCacheTexture(Paths.image("noteStrumline", "shared"));
permanentCacheTexture(Paths.image("NOTE_hold_assets"));
// dude
permanentCacheTexture(Paths.image("fonts/bold", null));
permanentCacheTexture(Paths.image("fonts/default", null));
permanentCacheTexture(Paths.image("fonts/freeplay-clear", null));
var allSounds:Array<String> = Assets.list(AssetType.SOUND);
for (file in allSounds)
{
if (!file.endsWith(".ogg") || !file.contains("countdown/")) continue;
file = file.replace(" ", "");
if (file.contains("shared") || Assets.exists('shared:$file', AssetType.SOUND))
{
file = 'shared:$file';
}
permanentCacheSound(file);
}
permanentCacheSound(Paths.sound("cancelMenu"));
permanentCacheSound(Paths.sound("confirmMenu"));
permanentCacheSound(Paths.sound("screenshot"));
permanentCacheSound(Paths.sound("scrollMenu"));
permanentCacheSound(Paths.sound("soundtray/Voldown"));
permanentCacheSound(Paths.sound("soundtray/VolMAX"));
permanentCacheSound(Paths.sound("soundtray/Volup"));
permanentCacheSound(Paths.music("freakyMenu/freakyMenu"));
permanentCacheSound(Paths.music("offsetsLoop/offsetsLoop"));
permanentCacheSound(Paths.music("offsetsLoop/drumsLoop"));
permanentCacheSound(Paths.sound("missnote1", "shared"));
permanentCacheSound(Paths.sound("missnote2", "shared"));
permanentCacheSound(Paths.sound("missnote3", "shared"));
}
/**
* Clears the current texture and sound caches.
*/
public static inline function purgeCache(callGarbageCollector:Bool = false):Void
{
preparePurgeTextureCache();
purgeTextureCache();
preparePurgeSoundCache();
purgeSoundCache();
#if (cpp || neko || hl)
if (callGarbageCollector) funkin.util.MemoryUtil.collect(true);
#end
}
///// TEXTURES /////
/**
* Ensures a texture with the given key is cached.
* @param key The key of the texture to cache.
*/
public static function cacheTexture(key:String):Void
{
if (currentCachedTextures.exists(key))
{
return; // Already cached.
}
if (previousCachedTextures.exists(key))
{
// Move the texture from the previous cache to the current cache.
var graphic:Null<FlxGraphic> = previousCachedTextures.get(key);
previousCachedTextures.remove(key);
if (graphic != null) currentCachedTextures.set(key, graphic);
return;
}
var graphic:Null<FlxGraphic> = FlxGraphic.fromAssetKey(key, false, null, true);
if (graphic == null)
{
FlxG.log.warn('Failed to cache graphic: $key');
}
else
{
trace('Successfully cached graphic: $key');
graphic.persist = true;
currentCachedTextures.set(key, graphic);
}
}
/**
* Permanently caches a texture with the given key.
* @param key The key of the texture to cache.
*/
static function permanentCacheTexture(key:String):Void
{
if (permanentCachedTextures.exists(key))
{
return; // Already cached.
}
var graphic:Null<FlxGraphic> = FlxGraphic.fromAssetKey(key, false, null, true);
if (graphic == null)
{
FlxG.log.warn('Failed to cache graphic: $key');
}
else
{
trace('Successfully cached graphic: $key');
graphic.persist = true;
permanentCachedTextures.set(key, graphic);
}
currentCachedTextures = permanentCachedTextures;
}
/**
* Prepares the cache for purging unused textures.
*/
public inline static function preparePurgeTextureCache():Void
{
previousCachedTextures = currentCachedTextures;
for (graphicKey in previousCachedTextures.keys())
{
if (permanentCachedTextures.exists(graphicKey))
{
previousCachedTextures.remove(graphicKey);
}
}
currentCachedTextures = permanentCachedTextures;
}
/**
* Purges unused textures from the cache.
*/
public static function purgeTextureCache():Void
{
for (graphicKey in previousCachedTextures.keys())
{
if (permanentCachedTextures.exists(graphicKey))
{
previousCachedTextures.remove(graphicKey);
continue;
}
if (graphicKey.contains("fonts")) continue;
var graphic:Null<FlxGraphic> = previousCachedTextures.get(graphicKey);
if (graphic != null)
{
FlxG.bitmap.remove(graphic);
graphic.destroy();
previousCachedTextures.remove(graphicKey);
Assets.cache.clear(graphicKey);
}
}
@:privateAccess
if (FlxG.bitmap._cache == null)
{
@:privateAccess
FlxG.bitmap._cache = new Map();
}
@:privateAccess
for (key in FlxG.bitmap._cache.keys())
{
var obj:Null<FlxGraphic> = FlxG.bitmap.get(key);
if (obj == null || obj.persist || permanentCachedTextures.exists(key) || key.contains("fonts"))
{
continue;
}
if (obj.useCount > 0)
{
for (purgeEntry in purgeFilter)
{
if (key.contains(purgeEntry))
{
FlxG.bitmap.removeKey(key);
obj.destroy();
}
}
}
}
}
///// NOTE STYLE //////
public static function cacheNoteStyle(style:NoteStyle):Void
{
// TODO: Texture paths should fall back to the default values.
cacheTexture(Paths.image(style.getNoteAssetPath() ?? "note"));
cacheTexture(style.getHoldNoteAssetPath() ?? "noteHold");
cacheTexture(Paths.image(style.getStrumlineAssetPath() ?? "strumline"));
cacheTexture(Paths.image(style.getSplashAssetPath() ?? "noteSplash"));
cacheTexture(Paths.image(style.getHoldCoverDirectionAssetPath(LEFT) ?? "LEFT"));
cacheTexture(Paths.image(style.getHoldCoverDirectionAssetPath(RIGHT) ?? "RIGHT"));
cacheTexture(Paths.image(style.getHoldCoverDirectionAssetPath(UP) ?? "UP"));
cacheTexture(Paths.image(style.getHoldCoverDirectionAssetPath(DOWN) ?? "DOWN"));
// cacheTexture(Paths.image(style.buildCountdownSpritePath(THREE) ?? "THREE"));
cacheTexture(Paths.image(style.buildCountdownSpritePath(TWO) ?? "TWO"));
cacheTexture(Paths.image(style.buildCountdownSpritePath(ONE) ?? "ONE"));
cacheTexture(Paths.image(style.buildCountdownSpritePath(GO) ?? "GO"));
cacheSound(style.getCountdownSoundPath(THREE) ?? "THREE");
cacheSound(style.getCountdownSoundPath(TWO) ?? "TWO");
cacheSound(style.getCountdownSoundPath(ONE) ?? "ONE");
cacheSound(style.getCountdownSoundPath(GO) ?? "GO");
cacheTexture(Paths.image(style.buildJudgementSpritePath("sick") ?? 'sick'));
cacheTexture(Paths.image(style.buildJudgementSpritePath("good") ?? 'good'));
cacheTexture(Paths.image(style.buildJudgementSpritePath("bad") ?? 'bad'));
cacheTexture(Paths.image(style.buildJudgementSpritePath("shit") ?? 'shit'));
cacheTexture(Paths.image(style.buildComboNumSpritePath(0) ?? '0'));
cacheTexture(Paths.image(style.buildComboNumSpritePath(1) ?? '1'));
cacheTexture(Paths.image(style.buildComboNumSpritePath(2) ?? '2'));
cacheTexture(Paths.image(style.buildComboNumSpritePath(3) ?? '3'));
cacheTexture(Paths.image(style.buildComboNumSpritePath(4) ?? '4'));
cacheTexture(Paths.image(style.buildComboNumSpritePath(5) ?? '5'));
cacheTexture(Paths.image(style.buildComboNumSpritePath(6) ?? '6'));
cacheTexture(Paths.image(style.buildComboNumSpritePath(7) ?? '7'));
cacheTexture(Paths.image(style.buildComboNumSpritePath(8) ?? '8'));
cacheTexture(Paths.image(style.buildComboNumSpritePath(9) ?? '9'));
}
///// SOUND //////
public static function cacheSound(key:String):Void
{
if (currentCachedSounds.exists(key)) return;
if (previousCachedSounds.exists(key))
{
// Move the texture from the previous cache to the current cache.
var sound:Null<Sound> = previousCachedSounds.get(key);
previousCachedSounds.remove(key);
if (sound != null) currentCachedSounds.set(key, sound);
return;
}
var sound:Null<Sound> = Assets.getSound(key, true);
if (sound == null) return;
else
currentCachedSounds.set(key, sound);
}
public static function permanentCacheSound(key:String):Void
{
if (permanentCachedSounds.exists(key)) return;
var sound:Null<Sound> = Assets.getSound(key, true);
if (sound == null) return;
else
permanentCachedSounds.set(key, sound);
if (sound != null) currentCachedSounds.set(key, sound);
}
public static function preparePurgeSoundCache():Void
{
previousCachedSounds = currentCachedSounds;
for (key in previousCachedSounds.keys())
{
if (permanentCachedSounds.exists(key))
{
previousCachedSounds.remove(key);
}
}
currentCachedSounds = permanentCachedSounds;
}
/**
* Purges unused sounds from the cache.
*/
public static inline function purgeSoundCache():Void
{
for (key in previousCachedSounds.keys())
{
if (permanentCachedSounds.exists(key))
{
previousCachedSounds.remove(key);
continue;
}
var sound:Null<Sound> = previousCachedSounds.get(key);
if (sound != null)
{
Assets.cache.removeSound(key);
previousCachedSounds.remove(key);
}
}
Assets.cache.clear("songs");
Assets.cache.clear("music");
// Felt lazy.
var key = Paths.music("freakyMenu/freakyMenu");
var sound:Null<Sound> = Assets.getSound(key, true);
if (sound != null)
{
permanentCachedSounds.set(key, sound);
currentCachedSounds.set(key, sound);
}
}
///// MISC /////
public static inline function clearFreeplay():Void
{
var keysToRemove:Array<String> = [];
@:privateAccess
for (key in FlxG.bitmap._cache.keys())
{
if (!key.contains("freeplay")) continue;
if (permanentCachedTextures.exists(key) || key.contains("fonts")) continue;
keysToRemove.push(key);
}
@:privateAccess
for (key in keysToRemove)
{
trace('Cleaning up $key');
var obj:Null<FlxGraphic> = FlxG.bitmap.get(key);
if (obj != null)
{
obj.destroy();
}
FlxG.bitmap.removeKey(key);
if (currentCachedTextures.exists(key)) currentCachedTextures.remove(key);
Assets.cache.clear(key);
}
preparePurgeSoundCache();
purgeSoundCache();
}
public static inline function clearStickers():Void
{
var keysToRemove:Array<String> = [];
@:privateAccess
for (key in FlxG.bitmap._cache.keys())
{
if (!key.contains("stickers")) continue;
if (permanentCachedTextures.exists(key) || key.contains("fonts")) continue;
keysToRemove.push(key);
}
@:privateAccess
for (key in keysToRemove)
{
trace('Cleaning up $key');
var obj:Null<FlxGraphic> = FlxG.bitmap.get(key);
if (obj != null)
{
obj.destroy();
}
FlxG.bitmap.removeKey(key);
if (currentCachedTextures.exists(key)) currentCachedTextures.remove(key);
Assets.cache.clear(key);
}
}
}

View file

@ -10,7 +10,6 @@ import flixel.math.FlxPoint;
import flixel.math.FlxRect;
import flixel.system.debug.log.LogStyle;
import flixel.util.FlxColor;
import funkin.graphics.FunkinSprite;
import funkin.data.dialogue.conversation.ConversationRegistry;
import funkin.data.dialogue.dialoguebox.DialogueBoxRegistry;
import funkin.data.dialogue.speaker.SpeakerRegistry;
@ -34,6 +33,7 @@ import funkin.ui.transition.LoadingState;
import funkin.util.CLIUtil;
import funkin.util.CLIUtil.CLIParams;
import funkin.util.macro.MacroUtil;
import funkin.util.TimerUtil;
import funkin.util.TrackerUtil;
import funkin.util.WindowUtil;
import openfl.display.BitmapData;
@ -92,31 +92,6 @@ class InitState extends FlxState
funkin.util.WindowUtil.initTracy();
#end
#if FEATURE_HAPTICS
// Setup Haptic feedback
extension.haptics.Haptic.initialize();
#end
#if FEATURE_MOBILE_ADVERTISEMENTS
// Setup Admob
funkin.mobile.util.AdMobUtil.init();
#end
#if FEATURE_MOBILE_IAP
// Setup In-App purchases
funkin.mobile.util.InAppPurchasesUtil.init();
#end
#if FEATURE_MOBILE_IAR
// Setup In-App purchases
funkin.mobile.util.InAppReviewUtil.init();
#end
#if ios
// Setup Audio session
funkin.external.ios.AudioSession.initialize();
#end
// This ain't a pixel art game! (most of the time)
FlxSprite.defaultAntialiasing = true;
@ -129,15 +104,13 @@ class InitState extends FlxState
// but that makes our soundtray not show up on init if we have the game muted.
// We set it to active so it at least calls it's update function once (see FlxGame.onEnterFrame(), it's called there)
// and also see FunkinSoundTray.update() to see what we do and how we check if we are muted or not
#if !mobile
FlxG.game.soundTray.active = true;
#end
// Set the game to a lower frame rate while it is in the background.
FlxG.game.focusLostFramerate = 30;
// Makes Flixel use frame times instead of locked movements per frame for things like tweens
FlxG.fixedTimestep = false;
FlxG.fixedTimestep = false;
setupFlixelDebug();
@ -160,25 +133,6 @@ class InitState extends FlxState
// Don't play transition in when entering the title state.
FlxTransitionableState.skipNextTransIn = true;
FlxG.signals.gameResized.add(function(width:Int, height:Int) {
FlxTransitionableState.defaultTransIn = new TransitionData(FADE, FlxColor.BLACK, 1, new FlxPoint(0, -1), tileData,
new FlxRect(-200, -200, FlxG.width * 1.4, FlxG.height * 1.4));
FlxTransitionableState.defaultTransOut = new TransitionData(FADE, FlxColor.BLACK, 0.7, new FlxPoint(0, 1), tileData,
new FlxRect(-200, -200, FlxG.width * 1.4, FlxG.height * 1.4));
});
// SDL for some reason enables VSync on focus lost/gained in Android
// Since we don't really need VSync on Android we're gonna forcefully disable it on these signals for now
// This is fixed on SDL3 from what I've heared but that doodoo isn't working poperly for Android
#if android
FlxG.signals.focusLost.add(function() {
WindowUtil.setVSyncMode(lime.ui.WindowVSyncMode.OFF);
});
FlxG.signals.focusGained.add(function() {
WindowUtil.setVSyncMode(lime.ui.WindowVSyncMode.OFF);
});
#end
//
// NEWGROUNDS API SETUP
//
@ -202,7 +156,6 @@ class InitState extends FlxState
//
#if android
FlxG.android.preventDefaultKeys = [flixel.input.android.FlxAndroidKey.BACK];
funkin.external.android.CallbackUtil.init();
#end
//
@ -217,20 +170,12 @@ class InitState extends FlxState
#if FEATURE_SCREENSHOTS
funkin.util.plugins.ScreenshotPlugin.initialize();
#end
#if FEATURE_NEWGROUNDS
funkin.util.plugins.NewgroundsMedalPlugin.initialize();
#end
funkin.util.plugins.EvacuateDebugPlugin.initialize();
funkin.util.plugins.ForceCrashPlugin.initialize();
funkin.util.plugins.ReloadAssetsDebugPlugin.initialize();
#if !mobile
funkin.util.plugins.VolumePlugin.initialize();
#end
funkin.util.plugins.WatchPlugin.initialize();
#if mobile
funkin.util.plugins.TouchPointerPlugin.initialize();
funkin.mobile.input.ControlsHandler.initInputTrackers();
#end
//
// GAME DATA PARSING
@ -239,6 +184,7 @@ class InitState extends FlxState
// NOTE: Registries must be imported and not referenced with fully qualified names,
// to ensure build macros work properly.
trace('Parsing game data...');
var perfStart:Float = TimerUtil.start();
SongEventRegistry.loadEventCache(); // SongEventRegistry is structured differently so it's not a BaseRegistry.
SongRegistry.instance.loadEntries();
LevelRegistry.instance.loadEntries();
@ -264,10 +210,7 @@ class InitState extends FlxState
funkin.input.Cursor.hide();
#if !html5
// This fucking breaks on HTML5 builds because the "shared" library isn't loaded yet.
funkin.FunkinMemory.initialCache();
#end
trace('Parsing game data took: ${TimerUtil.ms(perfStart)}');
}
/**
@ -374,7 +317,7 @@ class InitState extends FlxState
}
else
{
// FlxG.sound.cache(Paths.music('freakyMenu/freakyMenu'));
FlxG.sound.cache(Paths.music('freakyMenu/freakyMenu'));
FlxG.switchState(() -> new TitleState());
}
}

View file

@ -1,12 +1,7 @@
package funkin;
#if mobile
import funkin.mobile.ui.FunkinHitbox;
import funkin.mobile.util.InAppPurchasesUtil;
#end
import funkin.save.Save;
import funkin.util.WindowUtil;
import funkin.util.HapticUtil.HapticsMode;
/**
* A core class which provides a store of user-configurable, globally relevant values.
@ -16,7 +11,6 @@ class Preferences
{
/**
* FPS
* Always the refresh rate of the display on mobile, or 60 on web.
* @default `60`
*/
public static var framerate(get, set):Int;
@ -25,12 +19,6 @@ class Preferences
{
#if web
return 60;
#elseif mobile
var refreshRate:Int = FlxG.stage.window.displayMode.refreshRate;
if (refreshRate < 60) refreshRate = 60;
return refreshRate;
#else
return Save?.instance?.options?.framerate ?? 60;
#end
@ -58,19 +46,11 @@ class Preferences
static function get_naughtyness():Bool
{
#if NO_FEATURE_NAUGHTYNESS
return false;
#else
return Save?.instance?.options?.naughtyness ?? true;
#end
}
static function set_naughtyness(value:Bool):Bool
{
#if NO_FEATURE_NAUGHTYNESS
value = false;
#end
var save:Save = Save.instance;
save.options.naughtyness = value;
save.flush();
@ -85,7 +65,7 @@ class Preferences
static function get_downscroll():Bool
{
return Save?.instance?.options?.downscroll #if mobile ?? true #else ?? false #end;
return Save?.instance?.options?.downscroll ?? false;
}
static function set_downscroll(value:Bool):Bool
@ -136,16 +116,12 @@ class Preferences
/**
* If enabled, an FPS and memory counter will be displayed even if this is not a debug build.
* Always disabled on mobile.
* @default `false`
*/
public static var debugDisplay(get, set):Bool;
static function get_debugDisplay():Bool
{
#if mobile
return false;
#end
return Save?.instance?.options?.debugDisplay ?? false;
}
@ -162,78 +138,14 @@ class Preferences
return value;
}
/**
* If enabled, haptic feedback will be enabled.
* @default `All`
*/
public static var hapticsMode(get, set):HapticsMode;
static function get_hapticsMode():HapticsMode
{
var value = Save?.instance?.options?.hapticsMode ?? "All";
return switch (value)
{
case "None":
HapticsMode.NONE;
case "Notes Only":
HapticsMode.NOTES_ONLY;
default:
HapticsMode.ALL;
};
}
static function set_hapticsMode(value:HapticsMode):HapticsMode
{
var string;
switch (value)
{
case HapticsMode.NONE:
string = "None";
case HapticsMode.NOTES_ONLY:
string = "Notes Only";
default:
string = "All";
};
var save:Save = Save.instance;
save.options.hapticsMode = string;
save.flush();
return value;
}
/**
* Multiplier of intensity for all the haptic feedback effects.
* @default `2.5`
*/
public static var hapticsIntensityMultiplier(get, set):Float;
static function get_hapticsIntensityMultiplier():Float
{
return Save?.instance?.options?.hapticsIntensityMultiplier ?? 1;
}
static function set_hapticsIntensityMultiplier(value:Float):Float
{
var save:Save = Save.instance;
save.options.hapticsIntensityMultiplier = value;
save.flush();
return value;
}
/**
* If enabled, the game will automatically pause when tabbing out.
* Always enabled on mobile.
* @default `true`
*/
public static var autoPause(get, set):Bool;
static function get_autoPause():Bool
{
#if mobile
return true;
#end
return Save?.instance?.options?.autoPause ?? true;
}
@ -266,26 +178,6 @@ class Preferences
return value;
}
/**
* A global audio offset in milliseconds.
* This is used to sync the audio.
* @default `0`
*/
public static var globalOffset(get, set):Int;
static function get_globalOffset():Int
{
return Save?.instance?.options?.globalOffset ?? 0;
}
static function set_globalOffset(value:Int):Int
{
var save:Save = Save.instance;
save.options.globalOffset = value;
save.flush();
return value;
}
/**
* If enabled, the game will utilize VSync (or adaptive VSync) on startup.
* @default `OFF`
@ -465,16 +357,8 @@ class Preferences
#if web
toggleFramerateCap(Preferences.unlockedFramerate);
#end
#if desktop
// Apply the autoFullscreen setting (launches the game in fullscreen automatically)
FlxG.fullscreen = Preferences.autoFullscreen;
#end
#if mobile
// Apply the allowScreenTimeout setting.
lime.system.System.allowScreenTimeout = Preferences.screenTimeout;
#end
}
static function toggleFramerateCap(unlocked:Bool):Void
@ -490,88 +374,18 @@ class Preferences
if (show)
{
// Enable the debug display.
FlxG.game.parent.addChild(Main.fpsCounter);
FlxG.stage.addChild(Main.fpsCounter);
#if !html5
FlxG.game.parent.addChild(Main.memoryCounter);
FlxG.stage.addChild(Main.memoryCounter);
#end
}
else
{
// Disable the debug display.
FlxG.game.parent.removeChild(Main.fpsCounter);
FlxG.stage.removeChild(Main.fpsCounter);
#if !html5
FlxG.game.parent.removeChild(Main.memoryCounter);
FlxG.stage.removeChild(Main.memoryCounter);
#end
}
}
#if mobile
/**
* If enabled, device will be able to sleep on its own.
* @default `false`
*/
public static var screenTimeout(get, set):Bool;
static function get_screenTimeout():Bool
{
return Save?.instance?.mobileOptions?.screenTimeout ?? false;
}
static function set_screenTimeout(value:Bool):Bool
{
if (value != Save.instance.mobileOptions.screenTimeout) lime.system.System.allowScreenTimeout = value;
var save:Save = Save.instance;
save.mobileOptions.screenTimeout = value;
save.flush();
return value;
}
/**
* Controls Scheme for the hitbox.
* @default `4 Lanes`
*/
public static var controlsScheme(get, set):String;
static function get_controlsScheme():String
{
return Save?.instance?.mobileOptions?.controlsScheme ?? FunkinHitboxControlSchemes.Arrows;
}
static function set_controlsScheme(value:String):String
{
var save:Save = Save.instance;
save.mobileOptions.controlsScheme = value;
save.flush();
return value;
}
#if FEATURE_MOBILE_IAP
/**
* If bought, the game will not show any ads.
* @default `false`
*/
@:unreflective
public static var noAds(get, set):Bool;
@:unreflective
static function get_noAds():Bool
{
if (InAppPurchasesUtil.hasInitialized) noAds = InAppPurchasesUtil.isPurchased(InAppPurchasesUtil.UPGRADE_PRODUCT_ID);
var returnedValue = Save?.instance?.mobileOptions?.noAds ?? false;
return returnedValue;
}
@:unreflective
static function set_noAds(value:Bool):Bool
{
var save:Save = Save.instance;
save.mobileOptions.noAds = value;
save.flush();
return value;
}
#end
#end
}

View file

@ -1,6 +1,5 @@
package funkin.api.discord;
import funkin.util.macro.EnvironmentConfigMacro;
#if FEATURE_DISCORD_RPC
import hxdiscord_rpc.Discord;
import hxdiscord_rpc.Types.DiscordButton;
@ -12,7 +11,7 @@ import sys.thread.Thread;
@:nullSafety
class DiscordClient
{
static final CLIENT_ID:Null<String> = EnvironmentConfigMacro.environmentConfig?.get("DESKTOP_DISCORD_CLIENT_ID");
static final CLIENT_ID:String = "816168432860790794";
public static var instance(get, never):DiscordClient;
static var _instance:Null<DiscordClient> = null;
@ -41,28 +40,12 @@ class DiscordClient
{
trace('[DISCORD] Initializing connection...');
if (!hasValidCredentials())
{
FlxG.log.warn("Tried to initialize Discord connection, but credentials are invalid!");
return;
}
@:nullSafety(Off)
{
Discord.Initialize(CLIENT_ID, cpp.RawPointer.addressOf(handlers), 1, "");
}
// Discord.initialize(CLIENT_ID, handlers, true, null);
Discord.Initialize(CLIENT_ID, cpp.RawPointer.addressOf(handlers), 1, "");
createDaemon();
}
/**
* @returns `false` if the client ID is invalid.
*/
static function hasValidCredentials():Bool
{
return !(CLIENT_ID == null || CLIENT_ID == "" || (CLIENT_ID != null && CLIENT_ID.contains(" ")));
}
var daemon:Null<Thread> = null;
function createDaemon():Void
@ -219,27 +202,14 @@ typedef DiscordClientPresenceParams =
class DiscordClientSandboxed
{
public static function setPresence(params:DiscordClientPresenceParams):Void
public static function setPresence(params:DiscordClientPresenceParams)
{
DiscordClient.instance.setPresence(params);
return DiscordClient.instance.setPresence(params);
}
public static function shutdown():Void
public static function shutdown()
{
DiscordClient.instance.shutdown();
}
}
#else
class DiscordClientSandboxed
{
public static function setPresence(params:Dynamic):Void
{
// Do nothing.
}
public static function shutdown():Void
{
// Do nothing.
}
}
#end

View file

@ -1,11 +1,9 @@
package funkin.api.newgrounds;
#if FEATURE_NEWGROUNDS_EVENTS
import io.newgrounds.Call.CallOutcome;
import io.newgrounds.NG;
import io.newgrounds.objects.events.Outcome;
import io.newgrounds.objects.events.Result;
#end
/**
* Use Newgrounds to perform basic telemetry. Ignore if not logged in to Newgrounds.
@ -33,7 +31,6 @@ class Events
#end
}
#if FEATURE_NEWGROUNDS_EVENTS
static function onEventLogged(eventName:String, outcome:CallOutcome<LogEventData>)
{
switch (outcome)
@ -58,7 +55,6 @@ class Events
}
}
}
#end
public static inline function logStartGame():Void
{

View file

@ -1,156 +0,0 @@
package funkin.api.newgrounds;
#if FEATURE_NEWGROUNDS
import io.newgrounds.utils.SaveSlotList;
import io.newgrounds.objects.SaveSlot;
import io.newgrounds.Call.CallError;
import io.newgrounds.objects.events.Outcome;
import funkin.save.Save;
@:nullSafety
@:access(funkin.save.Save)
class NGSaveSlot
{
public static var instance(get, never):NGSaveSlot;
static var _instance:Null<NGSaveSlot> = null;
static function get_instance():NGSaveSlot
{
if (_instance == null)
{
return loadInstance();
}
return _instance;
}
public static function loadInstance():NGSaveSlot
{
var loadedSave:NGSaveSlot = loadSlot(Save.BASE_SAVE_SLOT);
if (_instance == null) _instance = loadedSave;
return loadedSave;
}
static function loadSlot(slot:Int):NGSaveSlot
{
trace('[NEWGROUNDS] Getting save slot from ID $slot');
var saveSlot:Null<SaveSlot> = NewgroundsClient.instance.saveSlots?.getById(slot);
var saveSlotObj:NGSaveSlot = new NGSaveSlot(saveSlot);
return saveSlotObj;
}
public var ngSaveSlot:Null<SaveSlot> = null;
public function new(?ngSaveSlot:Null<SaveSlot>)
{
this.ngSaveSlot = ngSaveSlot;
#if FLX_DEBUG
FlxG.console.registerClass(NGSaveSlot);
FlxG.console.registerClass(Save);
#end
}
/**
* Saves `data` to the newgrounds save slot.
* @param data The raw save data.
*/
public function save(data:RawSaveData):Void
{
var encodedData:String = haxe.Serializer.run(data);
try
{
ngSaveSlot?.save(encodedData, function(outcome:Outcome<CallError>) {
switch (outcome)
{
case SUCCESS:
trace('[NEWGROUNDS] Successfully saved save data to save slot!');
case FAIL(error):
trace('[NEWGROUNDS] Failed to save data to save slot!');
trace(error);
}
});
}
catch (error:String)
{
trace('[NEWGROUNDS] Failed to save data to save slot!');
trace(error);
}
}
public function load(?onComplete:Null<Dynamic->Void>, ?onError:Null<CallError->Void>):Void
{
try
{
ngSaveSlot?.load(function(outcome:SaveSlotOutcome):Void {
switch (outcome)
{
case SUCCESS(value):
trace('[NEWGROUNDS] Loaded save slot with the ID of ${ngSaveSlot?.id}!');
#if FEATURE_DEBUG_FUNCTIONS
trace('Save Slot Data:');
trace(value);
#end
if (onComplete != null && value != null)
{
var decodedData:Dynamic = haxe.Unserializer.run(value);
onComplete(decodedData);
}
case FAIL(error):
trace('[NEWGROUNDS] Failed to load save slot with the ID of ${ngSaveSlot?.id}!');
trace(error);
if (onError != null)
{
onError(error);
}
}
});
}
catch (error:String)
{
trace('[NEWGROUNDS] Failed to load save slot with the ID of ${ngSaveSlot?.id}!');
trace(error);
if (onError != null)
{
onError(RESPONSE({message: error, code: 500}));
}
}
}
public function clear():Void
{
try
{
ngSaveSlot?.clear(function(outcome:Outcome<CallError>) {
switch (outcome)
{
case SUCCESS:
trace('[NEWGROUNDS] Successfully cleared save slot!');
case FAIL(error):
trace('[NEWGROUNDS] Failed to clear save slot!');
trace(error);
}
});
}
catch (error:String)
{
trace('[NEWGROUNDS] Failed to clear save slot!');
trace(error);
}
}
public function checkSlot():Void
{
trace('[NEWGROUNDS] Checking save slot with the ID of ${ngSaveSlot?.id}...');
trace(' Is null? ${ngSaveSlot == null}');
trace(' Is empty? ${ngSaveSlot?.isEmpty() ?? false}');
}
}
#end

View file

@ -1,6 +1,5 @@
package funkin.api.newgrounds;
import funkin.util.macro.EnvironmentConfigMacro;
import funkin.save.Save;
import funkin.api.newgrounds.Medals.Medal;
#if FEATURE_NEWGROUNDS
@ -11,18 +10,13 @@ import io.newgrounds.NGLite.LoginOutcome;
import io.newgrounds.NGLite.LoginFail;
import io.newgrounds.objects.events.Outcome;
import io.newgrounds.utils.MedalList;
import io.newgrounds.utils.SaveSlotList;
import io.newgrounds.utils.ScoreBoardList;
import io.newgrounds.objects.User;
@:nullSafety
class NewgroundsClient
{
static final APP_ID:Null<String> = EnvironmentConfigMacro.environmentConfig?.get("API_NG_APP_ID");
static final ENCRYPTION_KEY:Null<String> = EnvironmentConfigMacro.environmentConfig?.get("API_NG_ENC_KEY");
public static var instance(get, never):NewgroundsClient;
static var _instance:Null<NewgroundsClient> = null;
static function get_instance():NewgroundsClient
@ -35,15 +29,14 @@ class NewgroundsClient
public var user(get, never):Null<User>;
public var medals(get, never):Null<MedalList>;
public var leaderboards(get, never):Null<ScoreBoardList>;
public var saveSlots(get, never):Null<SaveSlotList>;
private function new()
{
trace('[NEWGROUNDS] Initializing client...');
#if FEATURE_NEWGROUNDS_DEBUG
trace('[NEWGROUNDS] App ID: ${APP_ID}');
trace('[NEWGROUNDS] Encryption Key: ${ENCRYPTION_KEY}');
trace('[NEWGROUNDS] App ID: ${NewgroundsCredentials.APP_ID}');
trace('[NEWGROUNDS] Encryption Key: ${NewgroundsCredentials.ENCRYPTION_KEY}');
#end
if (!hasValidCredentials())
@ -52,12 +45,9 @@ class NewgroundsClient
return;
}
@:nullSafety(Off)
{
NG.create(APP_ID, getSessionId(), #if FEATURE_NEWGROUNDS_DEBUG true #else false #end, onLoginResolved);
NG.core.setupEncryption(ENCRYPTION_KEY);
}
var debug = #if FEATURE_NEWGROUNDS_DEBUG true #else false #end;
NG.create(NewgroundsCredentials.APP_ID, getSessionId(), debug, onLoginResolved);
NG.core.setupEncryption(NewgroundsCredentials.ENCRYPTION_KEY);
}
public function init()
@ -176,12 +166,12 @@ class NewgroundsClient
*/
static function hasValidCredentials():Bool
{
return !(APP_ID == null
|| APP_ID == ""
|| (APP_ID != null && APP_ID.contains(" "))
|| ENCRYPTION_KEY == null
|| ENCRYPTION_KEY == ""
|| (ENCRYPTION_KEY != null && ENCRYPTION_KEY.contains(" ")));
return !(NewgroundsCredentials.APP_ID == null
|| NewgroundsCredentials.APP_ID == ""
|| NewgroundsCredentials.APP_ID.contains(" ")
|| NewgroundsCredentials.ENCRYPTION_KEY == null
|| NewgroundsCredentials.ENCRYPTION_KEY == ""
|| NewgroundsCredentials.ENCRYPTION_KEY.contains(" "));
}
function onLoginResolved(outcome:LoginOutcome):Void
@ -246,8 +236,6 @@ class NewgroundsClient
trace('[NEWGROUNDS] Submitting leaderboard request...');
NG.core.scoreBoards.loadList(onFetchedLeaderboards);
trace('[NEWGROUNDS] Submitting save slot request...');
NG.core.saveSlots.loadList(onFetchedSaveSlots);
}
function onLoginFailed(result:LoginFail):Void
@ -313,13 +301,6 @@ class NewgroundsClient
// trace(funkin.api.newgrounds.Leaderboards.listLeaderboardData());
}
function onFetchedSaveSlots(outcome:Outcome<CallError>):Void
{
trace('[NEWGROUNDS] Fetched save slots!');
NGSaveSlot.instance.checkSlot();
}
function get_user():Null<User>
{
if (NG.core == null || !this.isLoggedIn()) return null;
@ -338,12 +319,6 @@ class NewgroundsClient
return NG.core.scoreBoards;
}
function get_saveSlots():Null<SaveSlotList>
{
if (NG.core == null || !this.isLoggedIn()) return null;
return NG.core.saveSlots;
}
static function getSessionId():Null<String>
{
#if js

View file

@ -0,0 +1,32 @@
package funkin.audio;
#if desktop
import haxe.io.Path;
import sys.FileSystem;
/*
* A class that simply points the audio backend OpenALSoft to use a custom
* configuration when the game starts up.
*
* The config file overrides a few global OpenALSoft settings to improve audio
* quality on desktop targets.
*/
@:nullSafety
class ALSoftConfig
{
private static function __init__():Void
{
var configPath:String = Path.directory(Path.withoutExtension(#if hl Sys.getCwd() #else Sys.programPath() #end));
#if windows
configPath += "/plugins/alsoft.ini";
#elseif mac
configPath = '${Path.directory(configPath)}/Resources/plugins/alsoft.conf';
#else
configPath += "/plugins/alsoft.conf";
#end
Sys.putEnv("ALSOFT_CONF", FileSystem.fullPath(configPath));
}
}
#end

View file

@ -20,6 +20,9 @@ import openfl.media.Sound;
import openfl.media.SoundChannel;
import openfl.media.SoundMixer;
#if (openfl >= "8.0.0")
#end
/**
* A FlxSound which adds additional functionality:
* - Delayed playback via negative song position.
@ -42,9 +45,9 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
if (_onVolumeChanged == null)
{
_onVolumeChanged = new FlxTypedSignal<Float->Void>();
FlxG.sound.onVolumeChange.add(function(volume:Float) {
FlxG.sound.volumeHandler = function(volume:Float) {
_onVolumeChanged.dispatch(volume);
});
}
}
return _onVolumeChanged;
}
@ -475,7 +478,7 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
if (autoPlay) sound.play();
sound.volume = volume;
FlxG.sound.defaultSoundGroup.add(sound);
sound.group = FlxG.sound.defaultSoundGroup;
sound.persist = persist;
sound.important = important;

View file

@ -72,8 +72,8 @@ class ABotVis extends FlxTypedSpriteGroup<FlxSprite>
// we use a very low minFreq since some songs use low low subbass like a boss
analyzer.minFreq = 10;
#if sys
// On native it uses FFT stuff that isn't as optimized as the direct browser stuff we use on HTML5
#if desktop
// On desktop it uses FFT stuff that isn't as optimized as the direct browser stuff we use on HTML5
// So we want to manually change it!
analyzer.fftN = 256;
#end

View file

@ -243,10 +243,9 @@ class WaveformDataChannel
}
/**
* @param i Index
* @return minimum point at an index.
* Retrieve a given minimum point at an index.
*/
public function minSample(i:Int):Int
public function minSample(i:Int)
{
var offset = (i * parent.channels + this.channelId) * 2;
return inline parent.get(offset);

View file

@ -1,5 +1,7 @@
package funkin.audio.waveform;
import funkin.util.TimerUtil;
@:nullSafety
class WaveformDataParser
{
@ -43,58 +45,73 @@ class WaveformDataParser
public static function interpretAudioBuffer(soundBuffer:lime.media.AudioBuffer):Null<WaveformData>
{
var sampleRate = soundBuffer.sampleRate;
var channels = soundBuffer.channels;
var bitsPerSample = soundBuffer.bitsPerSample;
var samplesPerPoint:Int = 256; // I don't think we need to configure this.
var pointsPerSecond:Float = sampleRate / samplesPerPoint; // 172 samples per second for most songs is plenty precise while still being performant..
// TODO: Make this work better on HTML5.
var soundData:lime.utils.Int16Array = cast soundBuffer.data;
var soundDataSampleCount:Int = Std.int(Math.ceil(soundData.length / channels / (bitsPerSample == 16 ? 2 : 1)));
var soundDataRawLength:Int = soundData.length;
var soundDataSampleCount:Int = Std.int(Math.ceil(soundDataRawLength / channels / (bitsPerSample == 16 ? 2 : 1)));
var outputPointCount:Int = Std.int(Math.ceil(soundDataSampleCount / samplesPerPoint));
// Pre-allocate Vector with exact final size for better performance and memory efficiency
var outputDataLength:Int = outputPointCount * channels * 2;
var outputData = new haxe.ds.Vector<Int>(outputDataLength);
// trace('Interpreting audio buffer:');
// trace(' sampleRate: ${sampleRate}');
// trace(' channels: ${channels}');
// trace(' bitsPerSample: ${bitsPerSample}');
// trace(' samplesPerPoint: ${samplesPerPoint}');
// trace(' pointsPerSecond: ${pointsPerSecond}');
// trace(' soundDataRawLength: ${soundDataRawLength}');
// trace(' soundDataSampleCount: ${soundDataSampleCount}');
// trace(' soundDataRawLength/4: ${soundDataRawLength / 4}');
// trace(' outputPointCount: ${outputPointCount}');
// Reusable min/max tracking arrays to avoid allocation in the loop
var minValues = new haxe.ds.Vector<Int>(channels);
var maxValues = new haxe.ds.Vector<Int>(channels);
var minSampleValue:Int = bitsPerSample == 16 ? INT16_MIN : INT8_MIN;
var maxSampleValue:Int = bitsPerSample == 16 ? INT16_MAX : INT8_MAX;
var outputData:Array<Int> = [];
var perfStart:Float = TimerUtil.start();
for (pointIndex in 0...outputPointCount)
{
var rangeStart:Int = pointIndex * samplesPerPoint;
var rangeEnd:Int = Std.int(Math.min(rangeStart + samplesPerPoint, soundDataSampleCount));
// minChannel1, maxChannel1, minChannel2, maxChannel2, ...
var values:Array<Int> = [];
// Reset min/max values for this range
for (i in 0...channels)
{
minValues[i] = bitsPerSample == 16 ? INT16_MAX : INT8_MAX;
maxValues[i] = bitsPerSample == 16 ? INT16_MIN : INT8_MIN;
values.push(bitsPerSample == 16 ? INT16_MAX : INT8_MAX);
values.push(bitsPerSample == 16 ? INT16_MIN : INT8_MIN);
}
// Process all samples in this range
var rangeStart = pointIndex * samplesPerPoint;
var rangeEnd = rangeStart + samplesPerPoint;
if (rangeEnd > soundDataSampleCount) rangeEnd = soundDataSampleCount;
for (sampleIndex in rangeStart...rangeEnd)
{
for (channelIndex in 0...channels)
{
var sampleValue:Int = soundData[sampleIndex * channels + channelIndex];
var sampleIndex:Int = sampleIndex * channels + channelIndex;
var sampleValue = soundData[sampleIndex];
if (sampleValue < minValues[channelIndex]) minValues[channelIndex] = sampleValue;
if (sampleValue > maxValues[channelIndex]) maxValues[channelIndex] = sampleValue;
if (sampleValue < values[channelIndex * 2]) values[(channelIndex * 2)] = sampleValue;
if (sampleValue > values[channelIndex * 2 + 1]) values[(channelIndex * 2) + 1] = sampleValue;
}
}
// Write directly to final positions in output Vector
var baseIndex:Int = pointIndex * channels * 2;
for (channelIndex in 0...channels)
{
outputData[baseIndex + channelIndex * 2] = minValues[channelIndex];
outputData[baseIndex + channelIndex * 2 + 1] = maxValues[channelIndex];
}
// We now have the min and max values for the range.
for (value in values)
outputData.push(value);
}
var result = new WaveformData(null, channels, soundBuffer.sampleRate, samplesPerPoint, bitsPerSample, outputPointCount, outputData.toArray());
var outputDataLength:Int = Std.int(outputData.length / channels / 2);
var result = new WaveformData(null, channels, sampleRate, samplesPerPoint, bitsPerSample, outputPointCount, outputData);
trace('[WAVEFORM] Interpreted audio buffer in ${TimerUtil.seconds(perfStart)}.');
return result;
}

View file

@ -57,11 +57,7 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData> impleme
var player = fetchEntry(charId);
if (player == null) continue;
#if UNLOCK_EVERYTHING
count++;
#else
if (player.isUnlocked()) count++;
#end
}
return count;
@ -69,7 +65,6 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData> impleme
public function hasNewCharacter():Bool
{
#if (!UNLOCK_EVERYTHING)
var charactersSeen = Save.instance.charactersSeen.clone();
for (charId in listEntryIds())
@ -83,7 +78,6 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData> impleme
// This character is unlocked but we haven't seen them in Freeplay yet.
return true;
}
#end
// Fallthrough case.
return false;
@ -91,10 +85,9 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData> impleme
public function listNewCharacters():Array<String>
{
var charactersSeen = Save.instance.charactersSeen.clone();
var result = [];
#if (!UNLOCK_EVERYTHING)
var charactersSeen = Save.instance.charactersSeen.clone();
for (charId in listEntryIds())
{
var player = fetchEntry(charId);
@ -106,7 +99,6 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData> impleme
// This character is unlocked but we haven't seen them in Freeplay yet.
result.push(charId);
}
#end
return result;
}
@ -125,7 +117,6 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData> impleme
/**
* Return true if the given stage character is associated with a specific playable character.
* If so, the level should only appear if that character is selected in Freeplay.
* NOTE: This is NOT THE SAME as `player.isUnlocked()`!
* @param characterId The stage character ID.
* @return Whether the character is owned by any one character.
*/
@ -133,17 +124,4 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData> impleme
{
return ownedCharacterIds.exists(characterId);
}
/**
* @param characterId The character ID to check.
* @return Whether the player saw the character unlock animation in Character Select.
*/
public function isCharacterSeen(characterId:String):Bool
{
#if UNLOCK_EVERYTHING
return true;
#else
return Save.instance.charactersSeen.contains(characterId);
#end
}
}

View file

@ -140,7 +140,7 @@ class SongMetadata implements ICloneable<SongMetadata>
*/
public function toString():String
{
return 'SongMetadata(${this.songName} by ${this.artist}, charted by ${this.charter}, variation ${this.variation})';
return 'SongMetadata(${this.songName} by ${this.artist} and ${this.charter}, variation ${this.variation})';
}
}
@ -1117,23 +1117,6 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
return 'SongNoteData(${this.time}ms, ' + (this.length > 0 ? '[${this.length}ms hold]' : '') + ' ${this.data}'
+ (this.kind != '' ? ' [kind: ${this.kind}])' : ')');
}
public function buildTooltip():String
{
if ((this.kind?.length ?? 0) == 0) return "";
var result:String = 'Kind: ${this.kind}';
if (this.params.length == 0) return result;
result += "\nParams:";
for (param in params)
{
result += '\n- ${param.name}: ${param.value}';
}
return result;
}
}
/**

View file

@ -37,7 +37,7 @@ class FNFLegacyImporter
{
trace('Migrating song metadata from FNF Legacy.');
var songMetadata:SongMetadata = new SongMetadata('Import', Constants.DEFAULT_ARTIST, Constants.DEFAULT_CHARTER, Constants.DEFAULT_VARIATION);
var songMetadata:SongMetadata = new SongMetadata('Import', Constants.DEFAULT_ARTIST, 'default');
// Set generatedBy string for debugging.
songMetadata.generatedBy = 'Chart Editor Import (FNF Legacy)';

View file

@ -24,7 +24,7 @@ class SongDataMigrator
public static function migrate_SongMetadata_v2_1_0(input:SongData_v2_1_0.SongMetadata_v2_1_0):SongMetadata
{
var result:SongMetadata = new SongMetadata(input.songName, input.artist, Constants.DEFAULT_CHARTER, input.variation);
var result:SongMetadata = new SongMetadata(input.songName, input.artist, input.variation);
result.version = SongRegistry.SONG_METADATA_VERSION;
result.timeFormat = input.timeFormat;
result.divisions = input.divisions;
@ -66,7 +66,7 @@ class SongDataMigrator
public static function migrate_SongMetadata_v2_0_0(input:SongData_v2_0_0.SongMetadata_v2_0_0):SongMetadata
{
var result:SongMetadata = new SongMetadata(input.songName, input.artist, Constants.DEFAULT_CHARTER, input.variation);
var result:SongMetadata = new SongMetadata(input.songName, input.artist, input.variation);
result.version = SongRegistry.SONG_METADATA_VERSION;
result.timeFormat = input.timeFormat;
result.divisions = input.divisions;

View file

@ -13,7 +13,7 @@ class StageRegistry extends BaseRegistry<Stage, StageData> implements ISingleton
* Handle breaking changes by incrementing this value
* and adding migration to the `migrateStageData()` function.
*/
public static final STAGE_DATA_VERSION:thx.semver.Version = "1.0.2";
public static final STAGE_DATA_VERSION:thx.semver.Version = "1.0.3";
public static final STAGE_DATA_VERSION_RULE:thx.semver.VersionRule = ">=1.0.0 <1.1.0";

View file

@ -1,70 +0,0 @@
package funkin.external.android;
#if android
import lime.system.JNI;
import flixel.util.FlxSignal;
/**
* A Utility class to handle Android API level callbacks and events.
*/
@:unreflective
class CallbackUtil
{
/**
* The result code for `DATA_FOLDER_CLOSED` activity.
*/
public static var DATA_FOLDER_CLOSED(get, never):Int;
@:noCompletion
static function get_DATA_FOLDER_CLOSED():Int
{
final field:Null<Dynamic> = JNIUtil.createStaticField('funkin/extensions/CallbackUtil', 'DATA_FOLDER_CLOSED', 'I');
return field != null ? field.get() : 0;
}
/**
* Signal triggered when an activity result is received.
*
* First argument is the request code, second is the result code.
*/
public static var onActivityResult:FlxTypedSignal<Int->Int->Void> = new FlxTypedSignal<Int->Int->Void>();
/**
* Initializes the callback utility.
*/
public static function init():Void
{
final initCallBackJNI:Null<Dynamic> = JNIUtil.createStaticMethod('funkin/extensions/CallbackUtil', 'initCallBack', '(Lorg/haxe/lime/HaxeObject;)V');
if (initCallBackJNI != null)
{
initCallBackJNI(new CallbackHandler());
}
}
}
/**
* Internal class to handle native callback events.
*/
class CallbackHandler #if (lime >= "8.0.0") implements JNISafety #end
{
@:allow(funkin.external.android.CallbackUtil)
function new():Void {}
/**
* Handles the activity result callback from native code.
*
* @param requestCode The request code of the acitivty.
* @param resultCode The result code of the acitivty.
*/
@:keep
#if (lime >= "8.0.0")
@:runOnMainThread
#end
public function onActivityResult(requestCode:Int, resultCode:Int):Void
{
if (CallbackUtil.onActivityResult != null) CallbackUtil.onActivityResult.dispatch(requestCode, resultCode);
}
}
#end

View file

@ -1,23 +0,0 @@
package funkin.external.android;
#if android
/**
* A Utility class to manage the Application's Data folder on Android.
*/
@:unreflective
class DataFolderUtil
{
/**
* Opens the data folder on an Android device using JNI.
*/
public static function openDataFolder():Void
{
final openDataFolderJNI:Null<Dynamic> = JNIUtil.createStaticMethod('funkin/util/DataFolderUtil', 'openDataFolder', '(I)V');
if (openDataFolderJNI != null)
{
openDataFolderJNI(CallbackUtil.DATA_FOLDER_CLOSED);
}
}
}
#end

View file

@ -1,111 +0,0 @@
package funkin.external.android;
#if android
import lime.system.JNI;
/**
* A utility class for caching JNI method and field references.
*/
class JNIUtil
{
@:noCompletion
private static var staticMethodCache:Map<String, Dynamic> = [];
@:noCompletion
private static var memberMethodCache:Map<String, Dynamic> = [];
@:noCompletion
private static var staticFieldCache:Map<String, JNIStaticField> = [];
@:noCompletion
private static var memberFieldCache:Map<String, JNIMemberField> = [];
/**
* Retrieves or creates a cached static method reference.
*
* @param className The name of the Java class containing the method.
* @param methodName The name of the method.
* @param signature The method signature in JNI format.
* @param cache Whether to cache the result (default true).
* @return A dynamic reference to the static method.
*/
public static function createStaticMethod(className:String, methodName:String, signature:String, cache:Bool = true):Null<Dynamic>
{
@:privateAccess
className = JNI.transformClassName(className);
final key:String = '$className::$methodName::$signature';
if (cache && !staticMethodCache.exists(key)) staticMethodCache.set(key, JNI.createStaticMethod(className, methodName, signature));
else if (!cache) return JNI.createStaticMethod(className, methodName, signature);
return staticMethodCache.get(key);
}
/**
* Retrieves or creates a cached member method reference.
*
* @param className The name of the Java class containing the method.
* @param methodName The name of the method.
* @param signature The method signature in JNI format.
* @param cache Whether to cache the result (default true).
* @return A dynamic reference to the member method.
*/
public static function createMemberMethod(className:String, methodName:String, signature:String, cache:Bool = true):Null<Dynamic>
{
@:privateAccess
className = JNI.transformClassName(className);
final key:String = '$className::$methodName::$signature';
if (cache && !memberMethodCache.exists(key)) memberMethodCache.set(key, JNI.createMemberMethod(className, methodName, signature));
else if (!cache) return JNI.createMemberMethod(className, methodName, signature);
return memberMethodCache.get(key);
}
/**
* Retrieves or creates a cached static field reference.
*
* @param className The name of the Java class containing the field.
* @param fieldName The name of the field.
* @param signature The field signature in JNI format.
* @param cache Whether to cache the result (default true).
* @return A reference to the static field.
*/
public static function createStaticField(className:String, fieldName:String, signature:String, cache:Bool = true):Null<JNIStaticField>
{
@:privateAccess
className = JNI.transformClassName(className);
final key:String = '$className::$fieldName::$signature';
if (cache && !staticFieldCache.exists(key)) staticFieldCache.set(key, JNI.createStaticField(className, fieldName, signature));
else if (!cache) return JNI.createStaticField(className, fieldName, signature);
return staticFieldCache.get(key);
}
/**
* Retrieves or creates a cached member field reference.
*
* @param className The name of the Java class containing the field.
* @param fieldName The name of the field.
* @param signature The field signature in JNI format.
* @param cache Whether to cache the result (default true).
* @return A reference to the member field.
*/
public static function createMemberField(className:String, fieldName:String, signature:String, cache:Bool = true):Null<JNIMemberField>
{
@:privateAccess
className = JNI.transformClassName(className);
final key:String = '$className::$fieldName::$signature';
if (cache && !memberFieldCache.exists(key)) memberFieldCache.set(key, JNI.createMemberField(className, fieldName, signature));
else if (!cache) return JNI.createMemberField(className, fieldName, signature);
return memberFieldCache.get(key);
}
}
#end

View file

@ -1,52 +0,0 @@
package funkin.external.android;
#if android
import lime.math.Rectangle;
import lime.system.JNI;
/**
* A Utility class to get Android screen related informations.
*/
@:unreflective
class ScreenUtil
{
/**
* Retrieves the dimensions of display cutouts (such as notches) on Android devices.
*
* @return An array of `Rectangle` objects, each representing a display cutout's position and size.
*/
public static function getCutoutDimensions():Array<Rectangle>
{
final getCutoutDimensionsJNI:Null<Dynamic> = JNIUtil.createStaticMethod('funkin/util/ScreenUtil', 'getCutoutDimensions', '()[Landroid/graphics/Rect;');
if (getCutoutDimensionsJNI != null)
{
final rectangles:Array<Rectangle> = [];
for (rectangle in cast(getCutoutDimensionsJNI(), Array<Dynamic>))
{
if (rectangle == null) continue;
final topJNI:Null<JNIMemberField> = JNIUtil.createMemberField('android/graphics/Rect', 'top', 'I');
final leftJNI:Null<JNIMemberField> = JNIUtil.createMemberField('android/graphics/Rect', 'left', 'I');
final rightJNI:Null<JNIMemberField> = JNIUtil.createMemberField('android/graphics/Rect', 'right', 'I');
final bottomJNI:Null<JNIMemberField> = JNIUtil.createMemberField('android/graphics/Rect', 'bottom', 'I');
if (topJNI != null && leftJNI != null && rightJNI != null && bottomJNI != null)
{
final top:Int = topJNI.get(rectangle);
final left:Int = leftJNI.get(rectangle);
final right:Int = rightJNI.get(rectangle);
final bottom:Int = bottomJNI.get(rectangle);
rectangles.push(new Rectangle(left, top, right - left, bottom - top));
}
}
return rectangles;
}
return [];
}
}
#end

View file

@ -1,36 +0,0 @@
package funkin.extensions;
import android.content.Intent;
import org.haxe.extension.Extension;
import org.haxe.lime.HaxeObject;
public class CallbackUtil extends Extension
{
/**
* Constant representing the event when the data folder is closed.
*/
public static int DATA_FOLDER_CLOSED = 0x01;
private static HaxeObject haxeObject;
/**
* Initializes the callback object for handling Haxe callbacks.
*
* @param haxeObject The HaxeObject instance to handle callbacks.
*/
public static void initCallBack(final HaxeObject haxeObject)
{
CallbackUtil.haxeObject = haxeObject;
}
@Override
public boolean onActivityResult(int requestCode, int resultCode, Intent data)
{
if (haxeObject != null)
haxeObject.call2("onActivityResult", requestCode, resultCode);
return true;
}
}

View file

@ -1,346 +0,0 @@
package funkin.provider;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.graphics.Point;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Root;
import android.provider.DocumentsProvider;
import android.webkit.MimeTypeMap;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedList;
/**
* @see https://github.com/termux/termux-app/blob/7bceab88e2272f961d1b94ef736f1a9e20173247/app/src/main/java/com/termux/filepicker/TermuxDocumentsProvider.java
*/
public class DataFolderProvider extends DocumentsProvider
{
private static File BASE_DIR;
private static String BASE_DIR_PATH;
private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
Root.COLUMN_ROOT_ID,
Root.COLUMN_MIME_TYPES,
Root.COLUMN_FLAGS,
Root.COLUMN_ICON,
Root.COLUMN_TITLE,
Root.COLUMN_SUMMARY,
Root.COLUMN_DOCUMENT_ID,
Root.COLUMN_AVAILABLE_BYTES
};
private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
Document.COLUMN_DOCUMENT_ID,
Document.COLUMN_MIME_TYPE,
Document.COLUMN_DISPLAY_NAME,
Document.COLUMN_LAST_MODIFIED,
Document.COLUMN_FLAGS,
Document.COLUMN_SIZE
};
@Override
public Cursor queryRoots(String[] projection)
{
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_ROOT_PROJECTION);
if (BASE_DIR == null)
return result;
final MatrixCursor.RowBuilder row = result.newRow();
row.add(Root.COLUMN_ROOT_ID, getDocIdForFile(BASE_DIR));
row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(BASE_DIR));
row.add(Root.COLUMN_SUMMARY, "Data Folder");
row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH | Root.FLAG_SUPPORTS_IS_CHILD);
row.add(Root.COLUMN_TITLE, "::APP_TITLE::");
row.add(Root.COLUMN_MIME_TYPES, "*/*");
row.add(Root.COLUMN_AVAILABLE_BYTES, BASE_DIR.getFreeSpace());
::if (APP_PACKAGE != "")::
row.add(Root.COLUMN_ICON, ::APP_PACKAGE::.R.mipmap.ic_launcher);
::end::
return result;
}
@Override
public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException
{
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
includeFile(result, documentId, null);
return result;
}
@Override
public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException
{
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
File parent = getFileForDocId(parentDocumentId);
if (parent != null)
{
File[] children = null;
try {
children = parent.listFiles();
} catch (SecurityException e) {
children = new File[0];
}
if (children != null)
{
for (File file : children)
includeFile(result, null, file);
}
}
return result;
}
@Override
public ParcelFileDescriptor openDocument(String documentId, String mode, CancellationSignal signal) throws FileNotFoundException
{
return ParcelFileDescriptor.open(getFileForDocId(documentId), ParcelFileDescriptor.parseMode(mode));
}
@Override
public AssetFileDescriptor openDocumentThumbnail(String documentId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException
{
final File file = getFileForDocId(documentId);
final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
return new AssetFileDescriptor(pfd, 0, file.length());
}
@Override
public boolean onCreate()
{
BASE_DIR = getContext().getExternalFilesDir(null);
if (BASE_DIR == null)
return false;
try
{
BASE_DIR_PATH = BASE_DIR.getCanonicalPath();
}
catch (IOException e)
{
BASE_DIR_PATH = BASE_DIR.getAbsolutePath();
}
return true;
}
@Override
public String createDocument(String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException
{
File parentFile = getFileForDocId(parentDocumentId);
File newFile = new File(parentFile, displayName);
int noConflictId = 2;
while (newFile.exists())
{
newFile = new File(parentFile, displayName + " (" + noConflictId++ + ")");
}
try
{
boolean succeeded;
if (Document.MIME_TYPE_DIR.equals(mimeType))
succeeded = newFile.mkdir();
else
succeeded = newFile.createNewFile();
if (!succeeded)
throw new FileNotFoundException("Failed to create document with id " + newFile.getPath());
}
catch (IOException e)
{
throw new FileNotFoundException("Failed to create document with id " + newFile.getPath());
}
return newFile.getPath();
}
@Override
public void deleteDocument(String documentId) throws FileNotFoundException
{
if (!deleteRecursive(getFileForDocId(documentId)))
throw new FileNotFoundException("Failed to delete document with id " + documentId);
}
@Override
public String getDocumentType(String documentId) throws FileNotFoundException
{
return getMimeType(getFileForDocId(documentId));
}
@Override
public Cursor querySearchDocuments(String rootId, String query, String[] projection) throws FileNotFoundException
{
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
final LinkedList<File> pending = new LinkedList<>();
pending.add(getFileForDocId(rootId));
final int MAX_SEARCH_RESULTS = 50;
while (!pending.isEmpty() && result.getCount() < MAX_SEARCH_RESULTS)
{
final File file = pending.removeFirst();
boolean isInsideHome;
try
{
isInsideHome = file.getCanonicalPath().startsWith(BASE_DIR_PATH);
}
catch (IOException e)
{
isInsideHome = true;
}
if (isInsideHome)
{
if (file.isDirectory())
{
try {
Collections.addAll(pending, file.listFiles());
} catch (SecurityException e) {
// ignore
}
}
else if (file.getName().toLowerCase().contains(query))
{
includeFile(result, null, file);
}
}
}
return result;
}
@Override
public boolean isChildDocument(String parentDocumentId, String documentId)
{
try
{
File parent = getFileForDocId(parentDocumentId).getCanonicalFile();
File child = getFileForDocId(documentId).getCanonicalFile();
return child.getPath().startsWith(parent.getPath() + "/");
}
catch (IOException e)
{
return false;
}
}
private boolean deleteRecursive(File file)
{
if (file.isDirectory())
{
File[] children = file.listFiles();
if (children != null)
{
for (File child : children)
{
if (!deleteRecursive(child))
return false;
}
}
}
return file.delete();
}
private static String getDocIdForFile(File file)
{
return file.getAbsolutePath();
}
private static File getFileForDocId(String docId) throws FileNotFoundException
{
if (BASE_DIR == null)
throw new FileNotFoundException("Base directory not available");
final File f = (docId == null || docId.isEmpty()) ? BASE_DIR : new File(docId);
if (!f.exists())
throw new FileNotFoundException(f.getAbsolutePath() + " not found");
return f;
}
private static String getMimeType(File file)
{
if (file == null || file.isDirectory())
return Document.MIME_TYPE_DIR;
String name = file.getName();
int lastDot = name.lastIndexOf('.');
if (lastDot >= 0)
{
String extension = name.substring(lastDot + 1).toLowerCase();
String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if (mime != null)
return mime;
}
return "application/octet-stream";
}
private void includeFile(MatrixCursor result, String docId, File file) throws FileNotFoundException
{
if (docId == null)
docId = getDocIdForFile(file);
else
file = getFileForDocId(docId);
int flags = 0;
if (file.isDirectory())
{
if (file.canWrite())
flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
}
else if (file.canWrite())
{
flags |= Document.FLAG_SUPPORTS_WRITE;
}
if (file.getParentFile() != null && file.getParentFile().canWrite())
flags |= Document.FLAG_SUPPORTS_DELETE;
final String mimeType = getMimeType(file);
if (mimeType.startsWith("image/"))
flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
final MatrixCursor.RowBuilder row = result.newRow();
row.add(Document.COLUMN_DOCUMENT_ID, docId);
row.add(Document.COLUMN_DISPLAY_NAME, file.getName());
row.add(Document.COLUMN_SIZE, file.length());
row.add(Document.COLUMN_MIME_TYPE, mimeType);
row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
row.add(Document.COLUMN_FLAGS, flags);
::if (APP_PACKAGE != "")::
row.add(Document.COLUMN_ICON, ::APP_PACKAGE::.R.mipmap.ic_launcher);
::end::
}
}

View file

@ -1,34 +0,0 @@
package funkin.util;
import android.content.Intent;
import android.content.ContentResolver;
import android.net.Uri;
import android.os.Build;
import android.provider.DocumentsContract;
import android.content.pm.PackageInfo;
import java.util.List;
import org.haxe.extension.Extension;
public class DataFolderUtil
{
/**
* A method that opens the Application's data folder for browsing through the Storage Access Framework.
* It's highly based on some code borrowed from Mterial Files
* https://github.com/zhanghai/MaterialFiles
*/
public static void openDataFolder(int requestCode)
{
::if (APP_PACKAGE != "")::
if (Extension.mainActivity != null)
{
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(DocumentsContract.buildRootUri("::APP_PACKAGE::.docprovider", ""), "vnd.android.document/directory");
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
Extension.mainActivity.startActivityForResult(intent, requestCode);
}
::end::
}
}

View file

@ -1,39 +0,0 @@
package funkin.util;
import android.graphics.Rect;
import android.os.Build;
import android.view.DisplayCutout;
import android.view.WindowInsets;
import java.util.List;
import org.haxe.extension.Extension;
public class ScreenUtil
{
public static Rect[] getCutoutDimensions()
{
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
{
if (Extension.mainActivity != null)
{
WindowInsets insets = Extension.mainActivity.getWindow().getDecorView().getRootWindowInsets();
if (insets != null)
{
DisplayCutout cutout = insets.getDisplayCutout();
if (cutout != null)
{
List<Rect> boundingRects = cutout.getBoundingRects();
if (boundingRects != null && !boundingRects.isEmpty())
return boundingRects.toArray(new Rect[0]);
}
}
}
}
return new Rect[0];
}
}

View file

@ -1,17 +0,0 @@
package funkin.external.ios;
#if ios
/**
* A Utility class to manage iOS audio.
*/
@:build(funkin.util.macro.LinkerMacro.xml('project/Build.xml'))
@:include('AudioSession.hpp')
@:unreflective
extern class AudioSession
{
@:native('initialize')
static function initialize():Void;
@:native('setActive')
static function setActive(active:Bool):Void;
}
#end

View file

@ -1,18 +0,0 @@
package funkin.external.ios;
#if ios
/**
* A Utility class to get iOS screen related informations.
*/
@:build(funkin.util.macro.LinkerMacro.xml('project/Build.xml'))
@:include('ScreenUtil.hpp')
@:unreflective
extern class ScreenUtil
{
@:native('getSafeAreaInsets')
static function getSafeAreaInsets(top:cpp.RawPointer<Float>, bottom:cpp.RawPointer<Float>, left:cpp.RawPointer<Float>, right:cpp.RawPointer<Float>):Void;
@:native('getScreenSize')
static function getScreenSize(width:cpp.RawPointer<Float>, height:cpp.RawPointer<Float>):Void;
}
#end

View file

@ -1,29 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<xml>
<pragma once="true" />
<files id="haxe">
<compilerflag value="-I${this_dir}/ios/include" if="iphoneos || iphonesim" />
</files>
<files id="__main__">
<compilerflag value="-I${this_dir}/ios/include" if="iphoneos || iphonesim" />
</files>
<files id="external-ios" dir="${this_dir}/ios" if="iphoneos || iphonesim">
<compilerflag value="-I${this_dir}/ios/include" />
<file name="src/ScreenUtil.mm" />
<file name="src/AudioSession.mm" />
</files>
<target id="haxe">
<section if="iphoneos || iphonesim">
<vflag name="-framework" value="UIKit" />
<vflag name="-framework" value="Foundation" />
<vflag name="-framework" value="AVFAudio" />
</section>
<files id="external-ios" if="iphoneos || iphonesim" />
</target>
</xml>

View file

@ -1,4 +0,0 @@
#pragma once
void initialize();
void setActive(bool active);

View file

@ -1,4 +0,0 @@
#pragma once
void getSafeAreaInsets(double* top, double* bottom, double* left, double* right);
void getScreenSize(double* width, double* height);

View file

@ -1,41 +0,0 @@
#import <Foundation/Foundation.h>
#import <AVFAudio/AVFAudio.h>
void initialize()
{
AVAudioSession *session = [AVAudioSession sharedInstance];
NSError *error;
[session setCategory:AVAudioSessionCategoryPlayback
mode:AVAudioSessionModeDefault
options:AVAudioSessionCategoryOptionAllowBluetoothA2DP|AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers
error:&error];
if (@available(iOS 17.0, *))
{
[session setPrefersInterruptionOnRouteDisconnect:false error:nil];
}
if (@available(iOS 14.5, *))
{
[session setPrefersNoInterruptionsFromSystemAlerts:true error:nil];
}
if (error)
{
NSLog(@"Unable to set category of audio session: %@", error);
}
}
void setActive(bool active)
{
AVAudioSession *session = [AVAudioSession sharedInstance];
NSError *error;
[session setActive:YES error:&error];
if (error)
{
NSLog(@"Unable to set active of audio session: %@", error);
}
}

View file

@ -1,36 +0,0 @@
#import "ScreenUtil.hpp"
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
void getSafeAreaInsets(double* top, double* bottom, double* left, double* right)
{
if (@available(iOS 11, *))
{
UIWindow* window = [UIApplication sharedApplication].windows[0];
UIEdgeInsets safeAreaInsets = window.safeAreaInsets;
float scale = [UIScreen mainScreen].scale;
(*top) = safeAreaInsets.top * scale;
(*bottom) = safeAreaInsets.bottom * scale;
(*left) = safeAreaInsets.left * scale;
(*right) = safeAreaInsets.right * scale;
return;
}
(*top) = 0.0;
(*bottom) = 0.0;
(*left) = 0.0;
(*right) = 0.0;
}
void getScreenSize(double* width, double* height)
{
CGRect screenRect = [[UIScreen mainScreen] bounds];
float scale = [UIScreen mainScreen].scale;
(*width) = (double)screenRect.size.width * scale;
(*height) = (double)screenRect.size.height * scale;
}

View file

@ -11,9 +11,6 @@ import flixel.math.FlxRect;
import flixel.math.FlxPoint;
import flixel.graphics.frames.FlxFrame.FlxFrameAngle;
import flixel.FlxCamera;
import openfl.system.System;
using StringTools;
/**
* An FlxSprite with additional functionality.
@ -35,8 +32,6 @@ class FunkinSprite extends FlxSprite
*/
static var previousCachedTextures:Map<String, FlxGraphic> = [];
static var permanentCachedTextures:Map<String, FlxGraphic> = [];
/**
* @param x Starting X position
* @param y Starting Y position
@ -248,27 +243,6 @@ class FunkinSprite extends FlxSprite
}
}
public static function permanentCacheTexture(key:String):Void
{
// We don't want to cache the same texture twice.
if (permanentCachedTextures.exists(key)) return;
// Else, texture is currently uncached.
var graphic:FlxGraphic = FlxGraphic.fromAssetKey(key, false, null, true);
if (graphic == null)
{
FlxG.log.warn('Failed to cache graphic: $key');
}
else
{
trace('Successfully cached graphic: $key');
graphic.persist = true;
permanentCachedTextures.set(key, graphic);
}
currentCachedTextures = permanentCachedTextures;
}
public static function cacheSparrow(key:String):Void
{
cacheTexture(Paths.image(key));
@ -285,14 +259,7 @@ class FunkinSprite extends FlxSprite
public static function preparePurgeCache():Void
{
previousCachedTextures = currentCachedTextures;
for (graphicKey in previousCachedTextures.keys())
{
if (!permanentCachedTextures.exists(graphicKey)) continue;
previousCachedTextures.remove(graphicKey);
}
currentCachedTextures = permanentCachedTextures;
currentCachedTextures = [];
}
public static function purgeCache():Void
@ -306,32 +273,6 @@ class FunkinSprite extends FlxSprite
graphic.destroy();
previousCachedTextures.remove(graphicKey);
}
@:privateAccess
if (FlxG.bitmap._cache == null)
{
@:privateAccess
FlxG.bitmap._cache = new Map();
System.gc();
return;
}
@:privateAccess
for (key in FlxG.bitmap._cache.keys())
{
var obj:Null<FlxGraphic> = FlxG.bitmap.get(key);
if (obj == null) continue;
if (obj.persist) continue;
if (permanentCachedTextures.exists(key)) continue;
if (!(obj.useCount <= 0 || key.contains("characters") || key.contains("charSelect") || key.contains("results"))) continue;
FlxG.bitmap.removeKey(key);
obj.destroy();
}
openfl.Assets.cache.clear("songs");
openfl.Assets.cache.clear("sounds");
openfl.Assets.cache.clear("music");
System.gc();
}
static function isGraphicCached(graphic:FlxGraphic):Bool

View file

@ -136,9 +136,6 @@ class DropShadowScreenspace extends DropShadowShader
// essentially just stole this from the AngleMask shader but repurposed it to smooth
// the threshold because without any sort of smoothing it produces horrible edges
float antialias(vec2 fragCoord, float curThreshold, bool useMask) {
if (AA_STAGES == 0.0) {
return intensityPass(fragCoord, curThreshold, useMask);
}
// In GLSL 100, we need to use constant loop bounds
// Well assume a reasonable maximum for AA_STAGES and use a fixed loop

View file

@ -198,7 +198,7 @@ class DropShadowShader extends FlxShader
/*
Loads an image for the mask.
While you *could* directly set the value of the mask, this function works for both HTML5 and native targets.
While you *could* directly set the value of the mask, this function works for both HTML5 and desktop targets.
*/
public function loadAltMask(path:String)
{
@ -362,9 +362,6 @@ class DropShadowShader extends FlxShader
// essentially just stole this from the AngleMask shader but repurposed it to smooth
// the threshold because without any sort of smoothing it produces horrible edges
float antialias(vec2 fragCoord, float curThreshold, bool useMask) {
if (AA_STAGES == 0.0) {
return intensityPass(fragCoord, curThreshold, useMask);
}
// In GLSL 100, we need to use constant loop bounds
// Well assume a reasonable maximum for AA_STAGES and use a fixed loop

View file

@ -11,7 +11,7 @@ import openfl.net.NetStream;
/**
* Plays a video via a NetStream. Only works on HTML5.
* This does NOT replace hxvlc, nor does hxvlc replace this.
* hxvlc only works on native and does not work on HTML5!
* hxvlc only works on desktop and does not work on HTML5!
*/
@:nullSafety
class FlxVideo extends FunkinSprite

View file

@ -2,7 +2,6 @@ package funkin.graphics.video;
#if hxvlc
import hxvlc.flixel.FlxVideoSprite;
import funkin.Preferences;
/**
* Not to be confused with FlxVideo, this is a hxvlc based video class
@ -14,13 +13,6 @@ class FunkinVideoSprite extends FlxVideoSprite
public function new(x:Float = 0, y:Float = 0)
{
super(x, y);
// null safety fucking SUCKS
if (bitmap != null)
{
bitmap.onOpening.add(function():Void {
if (bitmap != null) bitmap.audioDelay = Preferences.globalOffset * 1000; // Microseconds
});
}
}
}
#end

View file

@ -13,6 +13,7 @@ import flixel.input.gamepad.FlxGamepadInputID;
import flixel.input.keyboard.FlxKey;
import flixel.math.FlxAngle;
import flixel.math.FlxPoint;
import lime.ui.Haptic;
/**
* A core class which handles receiving player input and interpreting it into game actions.
@ -63,9 +64,7 @@ class Controls extends FlxActionSet
var _freeplay_jump_to_top = new FunkinAction(Action.FREEPLAY_JUMP_TO_TOP);
var _freeplay_jump_to_bottom = new FunkinAction(Action.FREEPLAY_JUMP_TO_BOTTOM);
var _cutscene_advance = new FunkinAction(Action.CUTSCENE_ADVANCE);
#if FEATURE_DEBUG_MENU
var _debug_menu = new FunkinAction(Action.DEBUG_MENU);
#end
#if FEATURE_CHART_EDITOR
var _debug_chart = new FunkinAction(Action.DEBUG_CHART);
#end
@ -288,12 +287,10 @@ class Controls extends FlxActionSet
inline function get_CUTSCENE_ADVANCE()
return _cutscene_advance.check();
#if FEATURE_DEBUG_MENU
public var DEBUG_MENU(get, never):Bool;
inline function get_DEBUG_MENU()
return _debug_menu.check();
#end
#if FEATURE_CHART_EDITOR
public var DEBUG_CHART(get, never):Bool;
@ -324,7 +321,7 @@ class Controls extends FlxActionSet
inline function get_VOLUME_MUTE()
return _volume_mute.check();
public function new(name, ?scheme:KeyboardScheme)
public function new(name, scheme:KeyboardScheme = null)
{
super(name);
@ -349,7 +346,7 @@ class Controls extends FlxActionSet
add(_freeplay_jump_to_top);
add(_freeplay_jump_to_bottom);
add(_cutscene_advance);
#if FEATURE_DEBUG_MENU add(_debug_menu); #end
add(_debug_menu);
#if FEATURE_CHART_EDITOR add(_debug_chart); #end
#if FEATURE_STAGE_EDITOR add(_debug_stage); #end
add(_volume_up);
@ -372,7 +369,7 @@ class Controls extends FlxActionSet
setKeyboardScheme(scheme, false);
}
override function update():Void
override function update()
{
super.update();
}
@ -477,7 +474,7 @@ class Controls extends FlxActionSet
case FREEPLAY_JUMP_TO_TOP: _freeplay_jump_to_top;
case FREEPLAY_JUMP_TO_BOTTOM: _freeplay_jump_to_bottom;
case CUTSCENE_ADVANCE: _cutscene_advance;
#if FEATURE_DEBUG_MENU case DEBUG_MENU: _debug_menu; #end
case DEBUG_MENU: _debug_menu;
#if FEATURE_CHART_EDITOR case DEBUG_CHART: _debug_chart; #end
#if FEATURE_STAGE_EDITOR case DEBUG_STAGE: _debug_stage; #end
case VOLUME_UP: _volume_up;
@ -488,7 +485,8 @@ class Controls extends FlxActionSet
static function init():Void
{
FlxG.inputs.addUniqueType(new FlxActionManager());
var actions = new FlxActionManager();
FlxG.inputs.add(actions);
}
/**
@ -561,10 +559,8 @@ class Controls extends FlxActionSet
func(_freeplay_jump_to_bottom, JUST_PRESSED);
case CUTSCENE_ADVANCE:
func(_cutscene_advance, JUST_PRESSED);
#if FEATURE_DEBUG_MENU
case DEBUG_MENU:
func(_debug_menu, JUST_PRESSED);
#end
#if FEATURE_CHART_EDITOR
case DEBUG_CHART:
func(_debug_chart, JUST_PRESSED);
@ -730,6 +726,11 @@ class Controls extends FlxActionSet
forEachBound(control, function(action, state) addKeys(action, keys, state));
}
public function bindSwipe(control:Control, swipeDir:FlxDirectionFlags = FlxDirectionFlags.UP, ?swpLength:Float = 90)
{
forEachBound(control, function(action, press) action.add(new FlxActionInputDigitalMobileSwipeGameplay(swipeDir, press, swpLength)));
}
/**
* Sets all actions that pertain to the binder to trigger when the supplied keys are used.
* If binder is a literal you can inline this
@ -787,9 +788,7 @@ class Controls extends FlxActionSet
bindKeys(Control.FREEPLAY_JUMP_TO_TOP, getDefaultKeybinds(scheme, Control.FREEPLAY_JUMP_TO_TOP));
bindKeys(Control.FREEPLAY_JUMP_TO_BOTTOM, getDefaultKeybinds(scheme, Control.FREEPLAY_JUMP_TO_BOTTOM));
bindKeys(Control.CUTSCENE_ADVANCE, getDefaultKeybinds(scheme, Control.CUTSCENE_ADVANCE));
#if FEATURE_DEBUG_MENU
bindKeys(Control.DEBUG_MENU, getDefaultKeybinds(scheme, Control.DEBUG_MENU));
#end
#if FEATURE_CHART_EDITOR
bindKeys(Control.DEBUG_CHART, getDefaultKeybinds(scheme, Control.DEBUG_CHART));
#end
@ -799,6 +798,8 @@ class Controls extends FlxActionSet
bindKeys(Control.VOLUME_UP, getDefaultKeybinds(scheme, Control.VOLUME_UP));
bindKeys(Control.VOLUME_DOWN, getDefaultKeybinds(scheme, Control.VOLUME_DOWN));
bindKeys(Control.VOLUME_MUTE, getDefaultKeybinds(scheme, Control.VOLUME_MUTE));
bindMobileLol();
}
function getDefaultKeybinds(scheme:KeyboardScheme, control:Control):Array<FlxKey>
@ -829,7 +830,7 @@ class Controls extends FlxActionSet
case Control.FREEPLAY_JUMP_TO_TOP: return [HOME];
case Control.FREEPLAY_JUMP_TO_BOTTOM: return [END];
case Control.CUTSCENE_ADVANCE: return [Z, ENTER];
#if FEATURE_DEBUG_MENU case Control.DEBUG_MENU: return [GRAVEACCENT]; #end
case Control.DEBUG_MENU: return [GRAVEACCENT];
#if FEATURE_CHART_EDITOR case Control.DEBUG_CHART: return []; #end
#if FEATURE_STAGE_EDITOR case Control.DEBUG_STAGE: return []; #end
case Control.VOLUME_UP: return [PLUS, NUMPADPLUS];
@ -860,7 +861,7 @@ class Controls extends FlxActionSet
case Control.FREEPLAY_JUMP_TO_TOP: return [HOME];
case Control.FREEPLAY_JUMP_TO_BOTTOM: return [END];
case Control.CUTSCENE_ADVANCE: return [G, Z];
#if FEATURE_DEBUG_MENU case Control.DEBUG_MENU: return [GRAVEACCENT]; #end
case Control.DEBUG_MENU: return [GRAVEACCENT];
#if FEATURE_CHART_EDITOR case Control.DEBUG_CHART: return []; #end
#if FEATURE_STAGE_EDITOR case Control.DEBUG_STAGE: return []; #end
case Control.VOLUME_UP: return [PLUS];
@ -891,7 +892,7 @@ class Controls extends FlxActionSet
case Control.FREEPLAY_JUMP_TO_TOP: return [];
case Control.FREEPLAY_JUMP_TO_BOTTOM: return [];
case Control.CUTSCENE_ADVANCE: return [ENTER];
#if FEATURE_DEBUG_MENU case Control.DEBUG_MENU: return []; #end
case Control.DEBUG_MENU: return [];
#if FEATURE_CHART_EDITOR case Control.DEBUG_CHART: return []; #end
#if FEATURE_STAGE_EDITOR case Control.DEBUG_STAGE: return []; #end
case Control.VOLUME_UP: return [NUMPADPLUS];
@ -905,6 +906,30 @@ class Controls extends FlxActionSet
return [];
}
function bindMobileLol()
{
#if FLX_TOUCH
// MAKE BETTER TOUCH BIND CODE
bindSwipe(Control.NOTE_UP, FlxDirectionFlags.UP, 40);
bindSwipe(Control.NOTE_DOWN, FlxDirectionFlags.DOWN, 40);
bindSwipe(Control.NOTE_LEFT, FlxDirectionFlags.LEFT, 40);
bindSwipe(Control.NOTE_RIGHT, FlxDirectionFlags.RIGHT, 40);
// feels more like drag when up/down are inversed
bindSwipe(Control.UI_UP, FlxDirectionFlags.DOWN);
bindSwipe(Control.UI_DOWN, FlxDirectionFlags.UP);
bindSwipe(Control.UI_LEFT, FlxDirectionFlags.LEFT);
bindSwipe(Control.UI_RIGHT, FlxDirectionFlags.RIGHT);
#end
#if android
forEachBound(Control.BACK, function(action, pres) {
action.add(new FlxActionInputDigitalAndroid(FlxAndroidKey.BACK, JUST_PRESSED));
});
#end
}
function removeKeyboard()
{
for (action in this.digitalActions)
@ -987,9 +1012,7 @@ class Controls extends FlxActionSet
Control.VOLUME_UP => getDefaultGamepadBinds(Control.VOLUME_UP),
Control.VOLUME_DOWN => getDefaultGamepadBinds(Control.VOLUME_DOWN),
Control.VOLUME_MUTE => getDefaultGamepadBinds(Control.VOLUME_MUTE),
#if FEATURE_DEBUG_MENU
Control.DEBUG_MENU => getDefaultGamepadBinds(Control.DEBUG_MENU),
#end
#if FEATURE_CHART_EDITOR
Control.DEBUG_CHART => getDefaultGamepadBinds(Control.DEBUG_CHART),
#end
@ -1053,10 +1076,8 @@ class Controls extends FlxActionSet
[];
case Control.VOLUME_MUTE:
[];
#if FEATURE_DEBUG_MENU
case Control.DEBUG_MENU:
[];
#end
#if FEATURE_CHART_EDITOR
case Control.DEBUG_CHART:
[];
@ -1080,6 +1101,13 @@ class Controls extends FlxActionSet
forEachBound(control, function(action, state) addButtons(action, buttons, state, id));
}
public function touchShit(control:Control, id)
{
forEachBound(control, function(action, state) {
// action
});
}
/**
* Sets all actions that pertain to the binder to trigger when the supplied keys are used.
* If binder is a literal you can inline this
@ -1441,6 +1469,163 @@ class FunkinAction extends FlxActionDigital
}
}
class FlxActionInputDigitalMobileSwipeGameplay extends FlxActionInputDigital
{
var touchMap:Map<Int, Swipes> = new Map();
var vibrationSteps:Int = 5;
var curStep:Int = 5;
var activateLength:Float = 90;
var hapticPressure:Int = 100;
public function new(swipeDir:FlxDirectionFlags = FlxDirectionFlags.ANY, Trigger:FlxInputState, ?swipeLength:Float = 90)
{
super(OTHER, swipeDir.toInt(), Trigger);
activateLength = swipeLength;
}
// fix right swipe
// make so cant double swipe during gameplay
// hold notes?
override function update():Void
{
super.update();
#if FLX_TOUCH
for (touch in FlxG.touches.list)
{
if (touch.justPressed)
{
var pos:FlxPoint = new FlxPoint(touch.screenX, touch.screenY);
var pos2:FlxPoint = new FlxPoint(touch.screenX, touch.screenY);
var swp:Swipes =
{
initTouchPos: pos,
curTouchPos: pos2,
touchAngle: 0,
touchLength: 0
};
touchMap[touch.touchPointID] = swp;
curStep = 1;
Haptic.vibrate(40, 70);
}
if (touch.pressed)
{
var daSwipe = touchMap[touch.touchPointID];
daSwipe.curTouchPos.set(touch.screenX, touch.screenY);
var dx = daSwipe.initTouchPos.x - touch.screenX;
var dy = daSwipe.initTouchPos.y - touch.screenY;
daSwipe.touchAngle = Math.atan2(dy, dx);
daSwipe.touchLength = Math.sqrt(dx * dx + dy * dy);
FlxG.watch.addQuick("LENGTH", daSwipe.touchLength);
FlxG.watch.addQuick("ANGLE", FlxAngle.asDegrees(daSwipe.touchAngle));
if (daSwipe.touchLength >= (activateLength / vibrationSteps) * curStep)
{
curStep += 1;
// Haptic.vibrate(Std.int(hapticPressure / (curStep * 1.5)), 50);
}
}
if (touch.justReleased)
{
touchMap.remove(touch.touchPointID);
}
/* switch (inputID)
{
case FlxDirectionFlags.UP:
return
case FlxDirectionFlags.DOWN:
}
*/
}
#end
}
override public function check(Action:FlxAction):Bool
{
for (swp in touchMap)
{
var degAngle = FlxAngle.asDegrees(swp.touchAngle);
switch (trigger)
{
case JUST_PRESSED:
if (swp.touchLength >= activateLength)
{
if (inputID == FlxDirectionFlags.UP.toInt())
{
if (degAngle >= 45 && degAngle <= 90 + 45) return properTouch(swp);
}
else if (inputID == FlxDirectionFlags.DOWN.toInt())
{
if (-degAngle >= 45 && -degAngle <= 90 + 45) return properTouch(swp);
}
else if (inputID == FlxDirectionFlags.LEFT.toInt())
{
if (degAngle <= 45 && -degAngle <= 45) return properTouch(swp);
}
else if (inputID == FlxDirectionFlags.RIGHT.toInt())
{
if (degAngle >= 90 + 45 && degAngle <= -90 + -45) return properTouch(swp);
}
}
default:
}
}
return false;
}
function properTouch(swipe:Swipes):Bool
{
curStep = 1;
Haptic.vibrate(100, 30);
swipe.initTouchPos.set(swipe.curTouchPos.x, swipe.curTouchPos.y);
return true;
}
}
// Maybe this can be committed to main HaxeFlixel repo?
#if android
class FlxActionInputDigitalAndroid extends FlxActionInputDigital
{
/**
* Android buttons action input
* @param androidKeyID Key identifier (FlxAndroidKey.BACK, FlxAndroidKey.MENU... those are the only 2 android specific ones)
* @param Trigger What state triggers this action (PRESSED, JUST_PRESSED, RELEASED, JUST_RELEASED)
*/
public function new(androidKeyID:FlxAndroidKey, Trigger:FlxInputState)
{
super(FlxInputDevice.OTHER, androidKeyID, Trigger);
}
override public function check(Action:FlxAction):Bool
{
return switch (trigger)
{
#if android
case PRESSED: FlxG.android.checkStatus(inputID, PRESSED) || FlxG.android.checkStatus(inputID, PRESSED);
case RELEASED: FlxG.android.checkStatus(inputID, RELEASED) || FlxG.android.checkStatus(inputID, JUST_RELEASED);
case JUST_PRESSED: FlxG.android.checkStatus(inputID, JUST_PRESSED);
case JUST_RELEASED: FlxG.android.checkStatus(inputID, JUST_RELEASED);
#end
default: false;
}
}
}
#end
/**
* Since, in many cases multiple actions should use similar keys, we don't want the
* rebinding UI to list every action. ActionBinders are what the user percieves as
@ -1479,7 +1664,7 @@ enum Control
VOLUME_DOWN;
VOLUME_MUTE;
// DEBUG
#if FEATURE_DEBUG_MENU DEBUG_MENU; #end
DEBUG_MENU;
#if FEATURE_CHART_EDITOR DEBUG_CHART; #end
#if FEATURE_STAGE_EDITOR DEBUG_STAGE; #end
}
@ -1535,9 +1720,7 @@ enum abstract Action(String) to String from String
var VOLUME_DOWN = "volume_down";
var VOLUME_MUTE = "volume_mute";
// DEBUG
#if FEATURE_DEBUG_MENU
var DEBUG_MENU = "debug_menu";
#end
#if FEATURE_CHART_EDITOR
var DEBUG_CHART = "debug_chart";
#end

View file

@ -294,7 +294,7 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
// 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;
timestamp -= Conductor.instance.inputOffset * Constants.NS_PER_MS;
// trace(timestamp);
updateKeyStates(key, true);

View file

@ -13,7 +13,6 @@ import funkin.input.Controls.Action;
*
* Example: Pressing Ctrl+Z will undo, while holding Ctrl+Z will start to undo repeatedly.
*/
@:nullSafety
class TurboActionHandler extends FlxBasic
{
/**

View file

@ -12,7 +12,6 @@ import flixel.FlxBasic;
*
* Example: Pressing Ctrl+Z will undo, while holding Ctrl+Z will start to undo repeatedly.
*/
@:nullSafety
class TurboButtonHandler extends FlxBasic
{
/**

View file

@ -11,7 +11,6 @@ import flixel.FlxBasic;
*
* Example: Pressing Ctrl+Z will undo, while holding Ctrl+Z will start to undo repeatedly.
*/
@:nullSafety
class TurboKeyHandler extends FlxBasic
{
/**

View file

@ -1,136 +0,0 @@
package funkin.mobile.input;
import funkin.input.Controls;
import flixel.input.FlxInput;
import flixel.input.actions.FlxAction;
import flixel.input.actions.FlxActionInput;
import flixel.input.actions.FlxActionInputDigital;
import funkin.mobile.ui.FunkinButton;
import funkin.mobile.ui.FunkinHitbox;
import funkin.play.notes.NoteDirection;
import openfl.events.KeyboardEvent;
import openfl.events.TouchEvent;
/**
* Handles setting up and managing input controls for the game.
*/
class ControlsHandler
{
/**
* Returns wether the last input was sent through touch.
*/
public static var lastInputTouch(default, null):Bool = true;
/**
* Returns wether there's a gamepad or keyboard devices connected and active.
*/
public static var hasExternalInputDevice(get, never):Bool;
/**
* Returns wether an external input device is currently used as the main input.
*/
public static var usingExternalInputDevice(get, never):Bool;
/**
* Initialize input trackers used to get the current status of the `lastInputTouch` field.
*/
public static function initInputTrackers():Void
{
FlxG.stage.addEventListener(KeyboardEvent.KEY_DOWN, (_) -> lastInputTouch = false);
FlxG.stage.addEventListener(TouchEvent.TOUCH_BEGIN, (_) -> lastInputTouch = true);
}
/**
* Adds a button input to a given FlxActionDigital and caches it.
*
* @param action The FlxActionDigital to add the button input to.
* @param button The FunkinButton associated with the action.
* @param state The input state to associate with the action.
* @param cachedInput The array of FlxActionInput objects to cache the input.
*/
public static function addButton(action:FlxActionDigital, button:FunkinButton, state:FlxInputState, cachedInput:Array<FlxActionInput>):Void
{
if (action == null || button == null || cachedInput == null) return;
final input:FlxActionInputDigitalIFlxInput = new FlxActionInputDigitalIFlxInput(button, state);
cachedInput.push(input);
action.add(input);
}
/**
* Sets up hitbox controls based on game controls and hitbox hints.
*
* @param controls The controls instance defining game controls.
* @param hitbox The hitbox to associate with the controls.
* @param cachedInput The array of action input objects to cache the input.
*/
@:access(funkin.input.Controls)
public static function setupHitbox(controls:Controls, hitbox:FunkinHitbox, cachedInput:Array<FlxActionInput>):Void
{
if (controls == null || hitbox == null) return;
for (hint in hitbox.members)
{
@:privateAccess
switch (hint.noteDirection)
{
case NoteDirection.LEFT:
controls.forEachBound(Control.NOTE_LEFT, function(action:FlxActionDigital, state:FlxInputState):Void {
addButton(action, hint, state, cachedInput);
});
case NoteDirection.DOWN:
controls.forEachBound(Control.NOTE_DOWN, function(action:FlxActionDigital, state:FlxInputState):Void {
addButton(action, hint, state, cachedInput);
});
case NoteDirection.UP:
controls.forEachBound(Control.NOTE_UP, function(action:FlxActionDigital, state:FlxInputState):Void {
addButton(action, hint, state, cachedInput);
});
case NoteDirection.RIGHT:
controls.forEachBound(Control.NOTE_RIGHT, function(action:FlxActionDigital, state:FlxInputState):Void {
addButton(action, hint, state, cachedInput);
});
}
}
}
/**
* Removes cached input associated with game controls.
*
* @param controls The Controls instance defining game controls.
* @param cachedInput The array of action input objects to clear cached input from.
*/
public static function removeCachedInput(controls:Controls, cachedInput:Array<FlxActionInput>):Void
{
for (action in controls.digitalActions)
{
var i:Int = action.inputs.length;
while (i-- > 0)
{
var j:Int = cachedInput.length;
while (j-- > 0)
{
if (cachedInput[j] == action.inputs[i])
{
action.remove(action.inputs[i]);
cachedInput.remove(cachedInput[j]);
}
}
}
}
}
@:noCompletion
private static function get_hasExternalInputDevice():Bool
{
return FlxG.gamepads.numActiveGamepads > 0;
}
@:noCompletion
private static function get_usingExternalInputDevice():Bool
{
return ControlsHandler.hasExternalInputDevice && !ControlsHandler.lastInputTouch;
}
}

View file

@ -1,57 +0,0 @@
package funkin.mobile.input;
import flixel.input.FlxInput;
import funkin.input.PreciseInputManager;
import funkin.mobile.ui.FunkinHitbox;
import funkin.play.notes.NoteDirection;
import haxe.Int64;
/**
* Handles setting up and managing precise input controls for the game.
*/
@:access(funkin.input.PreciseInputManager)
class PreciseInputHandler
{
/**
* Initializes the hitbox with the relevant hints and event handlers.
*
* @param hitbox The hitbox to initialize.
*/
public static function initializeHitbox(hitbox:FunkinHitbox):Void
{
hitbox.onHintDown.add(handleHintDown);
hitbox.onHintUp.add(handleHintUp);
}
/**
* Handles the event when a hint is pressed.
*
* @param hint The hint that was pressed.
*/
static function handleHintDown(hint:FunkinHint):Void
{
final timestamp:Int64 = PreciseInputManager.getCurrentTimestamp();
@:privateAccess
if (hint.input?.justPressed ?? false)
{
PreciseInputManager.instance.onInputPressed.dispatch({noteDirection: hint.noteDirection, timestamp: timestamp});
PreciseInputManager.instance._dirPressTimestamps.set(hint.noteDirection, timestamp);
}
}
/**
* Handles the event when a hint is released.
*
* @param hint The hint that was released.
*/
static function handleHintUp(hint:FunkinHint):Void
{
final timestamp:Int64 = PreciseInputManager.getCurrentTimestamp();
@:privateAccess
if (hint.input?.justReleased ?? false)
{
PreciseInputManager.instance.onInputReleased.dispatch({noteDirection: hint.noteDirection, timestamp: timestamp});
PreciseInputManager.instance._dirPressTimestamps.set(hint.noteDirection, timestamp);
}
}
}

View file

@ -1,156 +0,0 @@
package funkin.mobile.ui;
import flixel.FlxG;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
import flixel.util.FlxSignal;
import funkin.audio.FunkinSound;
import funkin.util.HapticUtil;
class FunkinBackButton extends FunkinButton
{
public var onConfirmStart(default, null):FlxSignal = new FlxSignal();
public var onConfirmEnd(default, null):FlxSignal = new FlxSignal();
public var enabled:Bool = true;
var confirming:Bool = false;
public var restingOpacity:Float;
var instant:Bool = false;
var held:Bool = false;
/**
* Creates a new FunkinBackButton instance.
*
* @param x The x position of the object.
* @param y The y position of the object.
* @param color Button's optional color.
* @param confirmCallback An optional callback function that will be triggered when the object is clicked.
* @param restingOpacity An optional float that is the alpha the button will be when not selected/hovered over.
* @param instant An optional flag that makes the button not play the full animation before calling the callback.
*/
public function new(?x:Float = 0, ?y:Float = 0, ?color:FlxColor = FlxColor.WHITE, ?confirmCallback:Void->Void, ?restingOpacity:Float = 0.3,
instant:Bool = false):Void
{
super(x, y);
frames = Paths.getSparrowAtlas("backButton");
animation.addByIndices('idle', 'back', [0], "", 24, false);
animation.addByIndices('hold', 'back', [5], "", 24, false);
animation.addByIndices('confirm', 'back', [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22], "", 24, false);
animation.play("idle");
scale.set(0.7, 0.7);
updateHitbox();
this.color = color;
this.restingOpacity = restingOpacity;
this.instant = instant;
this.alpha = restingOpacity;
this.ignoreDownHandler = true;
onUp.add(playConfirmAnim);
onDown.add(playHoldAnim);
onOut.add(playOutAnim);
onConfirmEnd.add(confirmCallback);
}
function playHoldAnim():Void
{
if (confirming || held || !enabled) return;
held = true;
FlxTween.cancelTweensOf(this);
HapticUtil.vibrate(0, 0.01, 0.5);
animation.play('hold');
alpha = 1;
}
function playConfirmAnim():Void
{
if (!enabled) return;
if (instant)
{
onConfirmEnd.dispatch();
return;
}
else if (confirming)
{
return;
}
confirming = true;
FlxTween.cancelTweensOf(this);
HapticUtil.vibrate(0, 0.05, 0.5);
animation.play('confirm');
FunkinSound.playOnce(Paths.sound('cancelMenu'));
onConfirmStart.dispatch();
animation.onFinish.addOnce(function(name:String) {
if (name != 'confirm') return;
confirming = false;
held = false;
onConfirmEnd.dispatch();
});
}
function playOutAnim():Void
{
if (confirming || !enabled) return;
FlxTween.cancelTweensOf(this);
HapticUtil.vibrate(0, 0.01, 0.2);
animation.play('idle');
FlxTween.tween(this, {alpha: restingOpacity}, 0.5,
{
ease: FlxEase.expoOut,
onComplete: function(tween:FlxTween):Void {
held = false;
}
});
}
public function resetCallbacks():Void
{
onUp.removeAll();
onDown.removeAll();
onOut.removeAll();
confirming = false;
held = false;
onUp.add(playConfirmAnim);
onDown.add(playHoldAnim);
onOut.add(playOutAnim);
}
override public function update(elapsed:Float):Void
{
#if android
if (FlxG.android.justReleased.BACK) onConfirmEnd.dispatch();
#end
super.update(elapsed);
}
override function destroy():Void
{
super.destroy();
onConfirmStart.removeAll();
onConfirmEnd.removeAll();
if (animation != null && animation.onFinish != null) animation.onFinish.removeAll();
}
}

View file

@ -1,464 +0,0 @@
package funkin.mobile.ui;
import flixel.FlxCamera;
import flixel.FlxG;
import flixel.util.FlxColor;
import funkin.graphics.FunkinSprite;
import flixel.input.FlxInput;
import flixel.input.IFlxInput;
import flixel.input.touch.FlxTouch;
import flixel.math.FlxPoint;
import flixel.util.FlxDestroyUtil;
import flixel.util.FlxSignal;
import openfl.display.Graphics;
import haxe.ds.Map;
/**
* Enum representing the status of the button.
*/
enum abstract FunkinButtonStatus(Int) from Int to Int
{
var NORMAL = 0;
var PRESSED = 1;
}
/**
* A simple button class that calls a function when touched.
*/
#if !display
@:generic
#end
@:allow(funkin.mobile.ui.FunkinHitbox)
@:allow(funkin.mobile.ui.FunkinButton)
class FunkinButton extends FunkinSprite implements IFlxInput
{
/**
* A map that's storing every active touch's ID that's pressing a button.
*/
public static var buttonsTouchID:Map<Int, FunkinButton> = new Map();
/**
* The current state of the button, either `FunkinButtonStatus.NORMAL` or `FunkinButtonStatus.PRESSED`.
*/
public var status:FunkinButtonStatus;
/**
* The callback function to call when the button is released.
*/
public var onUp(default, null):FlxSignal = new FlxSignal();
/**
* The callback function to call when the button is pressed down.
*/
public var onDown(default, null):FlxSignal = new FlxSignal();
/**
* The callback function to call when the button is no longer hovered over.
*/
public var onOut(default, null):FlxSignal = new FlxSignal();
/**
* Whether the button was just released.
*/
public var justReleased(get, never):Bool;
/**
* Whether the button is currently released.
*/
public var released(get, never):Bool;
/**
* Whether the button is currently pressed.
*/
public var pressed(get, never):Bool;
/**
* Whether the button was just pressed.
*/
public var justPressed(get, never):Bool;
/**
* The touch instance that pressed this button.
*/
public var currentTouch(get, never):Null<FlxTouch>;
/**
* An array of objects that blocks your input.
*/
public var deadZones:Array<FunkinSprite> = [];
/**
* Whether the button should be released if you swiped over somwhere else.
*/
public var limitToBounds:Bool = true;
/**
* A radius for circular buttons.
* If this radius is larger than 0 then the overlap check will look if the touch point is inside this raius.
*/
public var radius:Float = 0;
/**
* The vertices of the polygon defining the button's hitbox.
* The array should contain points in the format: [x1, y1, x2, y2, ...].
* If the array is empty, the polygon is ignored, and the default hitbox is used.
*/
public var polygon:Null<Array<Float>> = null;
/**
* The input associated with the button, using `Int` as the type.
*/
var input:FlxInput<Int>;
/**
* The input currently pressing this button, if none, it's `null`.
* Needed to check for its release.
*/
var currentInput:IFlxInput;
/**
* The ID of the touch object that pressed this button.
*/
var touchID:Int = -1;
/**
* Whether the button should skip calling onDownHandler() on touch.pressed.
*/
public var ignoreDownHandler:Bool = false;
/**
* Creates a new `FunkinButton` object.
*
* @param x The x position of the button.
* @param y The y position of the button.
*/
public function new(x:Float = 0, y:Float = 0):Void
{
super(x, y);
status = FunkinButtonStatus.NORMAL;
solid = false;
immovable = true;
#if FLX_DEBUG
ignoreDrawDebug = true;
#end
scrollFactor.set();
input = new FlxInput(0);
}
/**
* Called by the game state when the state is changed (if this object belongs to the state).
*/
public override function destroy():Void
{
deadZones = FlxDestroyUtil.destroyArray(deadZones);
currentInput = null;
input = null;
buttonsTouchID.remove(touchID);
touchID = -1;
super.destroy();
}
/**
* Called by the game loop automatically, handles touch over and click detection.
*/
public override function update(elapsed:Float):Void
{
super.update(elapsed);
#if FLX_POINTER_INPUT
// Update the button, but only if touches are enabled
if (visible)
{
final overlapFound:Bool = checkTouchOverlap();
final touchReleased:Bool = (currentTouch != null && currentTouch.justReleased);
if ((currentInput != null && currentInput.justReleased || (!limitToBounds && touchReleased)) && overlapFound)
{
onUpHandler();
}
if (status != FunkinButtonStatus.NORMAL && (!overlapFound || (currentInput != null && currentInput.justReleased)))
{
if (limitToBounds || (!limitToBounds && touchReleased)) onOutHandler();
}
}
#end
input.update();
}
function checkTouchOverlap(?touch:FlxTouch):Bool
{
final touches:Array<FlxTouch> = touch == null ? FlxG.touches.list : [touch];
for (camera in cameras)
{
for (touch in touches)
{
final worldPos:FlxPoint = touch.getWorldPosition(camera, _point);
for (zone in deadZones)
{
if (zone != null && zone.overlapsPoint(worldPos, true, camera)) return false;
}
function updateTouchID():Void
{
touchID = touch.touchPointID;
if (buttonsTouchID.exists(touchID) && buttonsTouchID.get(touchID) != this)
{
final prevButton:Null<FunkinButton> = buttonsTouchID.get(touchID);
if (input != null && prevButton != null && prevButton.input != null && !prevButton.limitToBounds) prevButton.onOutHandler();
}
buttonsTouchID.set(touchID, this);
updateStatus(touch);
}
if (polygon != null && polygon.length >= 6 && polygon.length % 2 == 0)
{
if (polygonOverlapsPoint(worldPos, false, camera))
{
updateTouchID();
return true;
}
}
else if (radius > 0)
{
if (circleOverlapsPoint(worldPos, camera))
{
updateTouchID();
return true;
}
}
else
{
if (overlapsPoint(worldPos, true, camera))
{
updateTouchID();
return true;
}
}
}
}
return false;
}
function circleOverlapsPoint(point:FlxPoint, ?camera:FlxCamera):Bool
{
if (camera == null) camera = FlxG.camera;
final xPos = point.x - camera.scroll.x;
final yPos = point.y - camera.scroll.y;
getScreenPosition(_point, camera);
point.putWeak();
final distanceX = xPos - (_point.x + (width / 2));
final distanceY = yPos - (_point.y + (height / 2));
final distance = Math.sqrt((distanceX * distanceX) + (distanceY * distanceY));
return distance <= radius;
}
function polygonOverlapsPoint(point:FlxPoint, inScreenSpace:Bool = false, ?camera:FlxCamera):Bool
{
if (polygon == null || polygon.length < 6 || polygon.length % 2 != 0) return false;
if (!inScreenSpace) return isPointInPolygon(polygon, point, FlxPoint.weak(x, y));
if (camera == null) camera = FlxG.camera;
final pos:FlxPoint = FlxPoint.weak(point.x - camera.scroll.x, point.y - camera.scroll.y);
point.putWeak();
return isPointInPolygon(polygon, pos, getScreenPosition(_point, camera));
}
static function isPointInPolygon(vertices:Array<Float>, point:FlxPoint, ?offset:FlxPoint):Bool
{
if (offset == null) offset = FlxPoint.weak();
var inside:Bool = false;
final numsPoints:Int = Math.floor(vertices.length / 2);
for (i in 0...numsPoints)
{
final vertex1:FlxPoint = FlxPoint.weak(vertices[i * 2] + offset.x, vertices[i * 2 + 1] + offset.y);
final vertex2:FlxPoint = FlxPoint.weak(vertices[(i + 1) % numsPoints * 2] + offset.x, vertices[(i + 1) % numsPoints * 2 + 1] + offset.y);
if (checkRayIntersection(vertex1, vertex2, point))
{
inside = !inside;
}
}
point.putWeak();
offset.putWeak();
return inside;
}
static inline function checkRayIntersection(vertex1:FlxPoint, vertex2:FlxPoint, point:FlxPoint):Bool
{
final result:Bool = (vertex1.y > point.y) != (vertex2.y > point.y)
&& point.x < (vertex1.x + ((point.y - vertex1.y) / (vertex2.y - vertex1.y)) * (vertex2.x - vertex1.x));
vertex1.putWeak();
vertex2.putWeak();
return result;
}
function isPressed(check:Bool):Bool
{
return !(status != FunkinButtonStatus.NORMAL && (!check || (currentInput != null && currentInput.justReleased)));
}
function updateStatus(newInput:IFlxInput):Void
{
if (newInput.justPressed)
{
currentInput = newInput;
onDownHandler();
}
else if (status == FunkinButtonStatus.NORMAL && !ignoreDownHandler)
{
if (newInput.pressed)
{
onDownHandler();
}
}
}
function onUpHandler():Void
{
status = FunkinButtonStatus.NORMAL;
input.release();
buttonsTouchID.remove(touchID);
touchID = -1;
currentInput = null;
onUp.dispatch();
}
function onDownHandler():Void
{
status = FunkinButtonStatus.PRESSED;
input.press();
onDown.dispatch();
}
function onOutHandler():Void
{
status = FunkinButtonStatus.NORMAL;
input.release();
buttonsTouchID.remove(touchID);
touchID = -1;
onOut.dispatch();
}
#if FLX_DEBUG
public override function drawDebugOnCamera(camera:FlxCamera):Void
{
if (polygon != null && polygon.length >= 6 && polygon.length % 2 == 0)
{
if (!camera.visible || !camera.exists || !isOnScreen(camera)) return;
getScreenPosition(_point, camera);
final gfx:Graphics = beginDrawDebug(camera);
final boundingBoxColor:Null<FlxColor> = getDebugBoundingBoxColor(allowCollisions);
if (boundingBoxColor != null) drawDebugPolygonColor(gfx, polygon, boundingBoxColor);
endDrawDebug(camera);
}
else if (radius > 0)
{
if (!camera.visible || !camera.exists || !isOnScreen(camera)) return;
getScreenPosition(_point, camera);
final gfx:Graphics = beginDrawDebug(camera);
final boundingBoxColor:Null<FlxColor> = getDebugBoundingBoxColor(allowCollisions);
if (boundingBoxColor != null) drawDebugCircleColor(gfx, boundingBoxColor);
endDrawDebug(camera);
}
else
{
super.drawDebugOnCamera(camera);
}
}
function drawDebugCircleColor(gfx:Graphics, color:FlxColor):Void
{
gfx.lineStyle(2, color, 0.75);
gfx.drawCircle(radius, radius, radius);
}
function drawDebugPolygonColor(gfx:Graphics, vertices:Array<Float>, color:FlxColor):Void
{
gfx.lineStyle(2, color, 0.75);
for (i in 0...Math.floor(vertices.length / 2))
{
if (i == 0)
{
gfx.moveTo(vertices[i * 2] + _point.x, vertices[i * 2 + 1] + _point.y);
}
else
{
gfx.lineTo(vertices[i * 2] + _point.x, vertices[i * 2 + 1] + _point.y);
}
}
}
#end
inline function get_justReleased():Bool
{
return input.justReleased;
}
inline function get_released():Bool
{
return input.released;
}
inline function get_pressed():Bool
{
return input.pressed;
}
inline function get_justPressed():Bool
{
return input.justPressed;
}
inline function get_currentTouch():Null<FlxTouch>
{
return FlxG.touches.getByID(touchID);
}
}

View file

@ -1,683 +0,0 @@
package funkin.mobile.ui;
import flixel.FlxG;
import flixel.graphics.FlxGraphic;
import flixel.group.FlxSpriteGroup;
import flixel.input.actions.FlxActionInput;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
import flixel.util.FlxDestroyUtil;
import flixel.util.FlxSignal;
import funkin.graphics.shaders.HSVShader;
import funkin.graphics.FunkinSprite;
import funkin.mobile.input.ControlsHandler;
import funkin.play.notes.NoteDirection;
import openfl.display.BitmapData;
import openfl.display.Shape;
import openfl.geom.Matrix;
import openfl.Vector;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.play.notes.notestyle.NoteStyle;
import funkin.data.animation.AnimationData;
import funkin.util.assets.FlxAnimationUtil;
import funkin.ui.FullScreenScaleMode;
enum FunkinHintAlphaStyle
{
INVISIBLE_TILL_PRESS;
VISIBLE_TILL_PRESS;
}
/**
* The `FunkinHint` class represents a button with HSV color properties, allowing hue and saturation adjustments.
*/
@:nullSafety
class FunkinHint extends FunkinButton
{
/**
* A map defining different alpha styles for hint visibility during press and release states.
*
* Each style is represented as a key with an associated array of two alpha values:
* - The first value corresponds to the alpha when the hint is pressed.
* - The second value corresponds to the alpha when the hint is not pressed.
* - The third value corresponds to the duratuon it'll take to tween between the two values.
*/
static final HINT_ALPHA_STYLE:Map<FunkinHintAlphaStyle, Array<Float>> = [
INVISIBLE_TILL_PRESS => [0.3, 0.00001, 0.01],
VISIBLE_TILL_PRESS => [0.4, 0.2, 0.08]
];
/**
* Indicates whether the hint is pixel.
*/
public var isPixel:Bool = false;
/**
* The direction of the note associated with the button.
*/
var noteDirection:NoteDirection;
/**
* The label associated with the button.
*/
var label:Null<FunkinSprite>;
/**
* The tween used to animate the alpha changes of the button.
*/
var labelAlphaTween:Null<FlxTween>;
/**
* The HSV shader used to adjust the hue and saturation of the button.
*/
var hsvShader:HSVShader;
/**
* The tween used to animate the alpha changes of the button.
*/
var alphaTween:Null<FlxTween>;
var followTarget:Null<FunkinSprite>;
var followTargetSize:Bool = false;
/**
* Creates a new `FunkinHint` object.
*
* @param x The x position of the button.
* @param y The y position of the button.
* @param noteDirection The direction of the note the button represents (e.g. left, right).
* @param label An graphic to display as the label on the button.
*/
public function new(x:Float, y:Float, noteDirection:NoteDirection, label:Null<FlxGraphic>):Void
{
super(x, y);
this.noteDirection = noteDirection;
if (label != null)
{
this.label = new FunkinSprite(x, y);
this.label.loadGraphic(label);
}
hsvShader = new HSVShader();
hsvShader.hue = 1.0;
hsvShader.saturation = 1.0;
hsvShader.value = 1.0;
shader = hsvShader;
}
/**
* Initializes alpha tween animations for the button.
*
* @param style The alpha style to use.
*/
public function initTween(style:FunkinHintAlphaStyle):Void
{
final hintAlpha:Null<Array<Float>> = HINT_ALPHA_STYLE.get(style);
final swapValues:Bool = style == VISIBLE_TILL_PRESS;
if (hintAlpha == null || hintAlpha.length < 2) return;
function createTween(targetAlpha:Float, transitionTime:Float, isPressed:Bool):Void
{
alphaTween?.cancel();
alphaTween = FlxTween.tween(this, {alpha: targetAlpha}, transitionTime, {ease: FlxEase.circInOut});
if (label != null)
{
labelAlphaTween?.cancel();
labelAlphaTween = FlxTween.tween(label, {alpha: (hintAlpha[0] + hintAlpha[1]) - targetAlpha}, transitionTime, {ease: FlxEase.circInOut});
}
}
onDown.add(createTween.bind(hintAlpha[swapValues ? 1 : 0], hintAlpha[2], true));
onUp.add(createTween.bind(hintAlpha[swapValues ? 0 : 1], hintAlpha[2], false));
onOut.add(createTween.bind(hintAlpha[swapValues ? 0 : 1], hintAlpha[2], false));
alpha = hintAlpha[swapValues ? 0 : 1];
if (label != null && hintAlpha != null) label.alpha = hintAlpha[0];
}
/**
* Makes the hitbox follow the specified sprite.
*
* @param sprite The FunkinSprite instance that the hitbox should follow.
* @param followTargetSize A boolean indicating whether the hitbox should adjust to the target's size. Default is true.
*/
public function follow(sprite:FunkinSprite, followTargetSize:Bool = true):Void
{
this.followTargetSize = followTargetSize;
followTarget = sprite;
}
/**
* Desaturates the button, setting its saturation to 0.2.
*/
public function desaturate():Void
{
hsvShader.saturation = 0.2;
}
/**
* Sets the hue of the button.
*
* @param hue The new hue value.
*/
public function setHue(hue:Float):Void
{
hsvShader.hue = hue;
}
public override function update(elapsed:Float):Void
{
super.update(elapsed);
if (followTarget != null)
{
final widthMultiplier:Float = isPixel ? 1.35 : 1.35;
final heightMultiplier:Float = 8;
final xOffset:Float = isPixel ? 43.265 : 0;
final yOffset:Float = isPixel ? 57.65 : 0;
// TODO: THIS feels off when playing on regular notes but it's fine for pixel notes? Hard to explain needs more testing
if (followTargetSize)
{
setSize(followTarget.width * widthMultiplier + (isPixel ? 93.05 : 0), followTarget.height * heightMultiplier + (isPixel ? 118 : 0));
}
setPosition((followTarget.x - (followTarget.width * ((widthMultiplier - 1) / 2))) - xOffset, (followTarget.y - 220) - yOffset);
}
}
public override function draw():Void
{
super.draw();
if (label != null && label.visible)
{
label.cameras = _cameras;
label.draw();
}
}
#if FLX_DEBUG
public override function drawDebug():Void
{
super.drawDebug();
if (label != null) label.drawDebug();
}
#end
/**
* Cleans up memory used by the `FunkinHint`.
*/
public override function destroy():Void
{
if (alphaTween != null) alphaTween = FlxDestroyUtil.destroy(alphaTween);
if (labelAlphaTween != null) labelAlphaTween = FlxDestroyUtil.destroy(labelAlphaTween);
if (label != null) label = FlxDestroyUtil.destroy(label);
super.destroy();
}
override function set_x(v:Float):Float
{
super.set_x(v);
if (label != null) label.x = x;
return x;
}
override function set_y(v:Float):Float
{
super.set_y(v);
if (label != null) label.y = y;
return y;
}
}
enum abstract FunkinHitboxControlSchemes(String) from String to String
{
var FourLanes = 'Four Lanes';
var DoubleThumbTriangle = 'Double Thumb Triangle';
var DoubleThumbSquare = 'Double Thumb Square';
var DoubleThumbDPad = 'Double Thumb DPad';
var Arrows = 'Arrows';
}
/**
* This class represents a zone with four buttons, designed to be easily customizable in layout.
*/
@:nullSafety
class FunkinHitbox extends FlxTypedSpriteGroup<FunkinHint>
{
/**
* Indicates whether the hitbox is pixel.
*/
public var isPixel(default, set):Bool = false;
/**
* A `FlxTypedSignal` that triggers every time a button is pressed.
*/
public var onHintDown:FlxTypedSignal<FunkinHint->Void> = new FlxTypedSignal<FunkinHint->Void>();
/**
* A `FlxTypedSignal` that triggers every time a button is released.
*/
public var onHintUp:FlxTypedSignal<FunkinHint->Void> = new FlxTypedSignal<FunkinHint->Void>();
/**
* The list of tracked inputs for the hitbox.
*/
var trackedInputs:Array<FlxActionInput> = [];
/**
* Creates a new `FunkinHitbox` object.
*/
public function new(?schemeOverride:String, ?showGradint:Bool = true, ?directionsOverride:Array<NoteDirection>, ?colorsOverride:Array<FlxColor>):Void
{
super();
final hintsColors:Array<FlxColor> = (colorsOverride == null || colorsOverride.length == 0) ? [0xFFC34B9A, 0xFF00FFFF, 0xFF12FB06, 0xFFF9393F] : colorsOverride;
final hintsNoteDirections:Array<NoteDirection> = (directionsOverride == null || directionsOverride.length == 0) ? [NoteDirection.LEFT, NoteDirection.DOWN, NoteDirection.UP, NoteDirection.RIGHT] : directionsOverride;
#if mobile
final controlsScheme:String = (schemeOverride == null || schemeOverride.length == 0) ? Preferences.controlsScheme : schemeOverride;
switch (controlsScheme)
{
case FunkinHitboxControlSchemes.FourLanes:
final hintWidth:Int = Math.floor(FlxG.width / hintsNoteDirections.length);
final hintHeight:Int = FlxG.height;
for (i in 0...hintsNoteDirections.length)
{
add(createHintLane(i * hintWidth, 0, hintsNoteDirections[i % hintsNoteDirections.length], hintWidth, hintHeight,
hintsColors[i % hintsColors.length], true, showGradint));
}
case FunkinHitboxControlSchemes.DoubleThumbTriangle:
final screenHalf:Int = Math.floor(FlxG.width / 2);
for (i in 0...2)
{
final xOffset:Int = (i == 1) ? screenHalf : 0;
add(createHintTriangle(xOffset, 0, hintsNoteDirections[0], Math.floor(FlxG.width / 4), FlxG.height, hintsColors[0], showGradint));
add(createHintTriangle(xOffset, FlxG.height / 2, hintsNoteDirections[1], Math.floor(FlxG.width / 2), Math.floor(FlxG.height / 2), hintsColors[1],
showGradint));
add(createHintTriangle(xOffset, 0, hintsNoteDirections[2], Math.floor(FlxG.width / 2), Math.floor(FlxG.height / 2), hintsColors[2], showGradint));
add(createHintTriangle(xOffset + Math.floor(FlxG.width / 4), 0, hintsNoteDirections[3], Math.floor(FlxG.width / 4), FlxG.height, hintsColors[3],
showGradint));
}
case FunkinHitboxControlSchemes.DoubleThumbSquare:
final screenHalf:Int = Math.floor(FlxG.width / 2);
final hintWidth:Int = Math.floor((FlxG.width / hintsNoteDirections.length) / 2);
final hintHeight:Int = FlxG.height;
final boxWidth:Int = Math.floor(hintWidth * 2);
final boxHeight:Int = Math.floor(hintHeight / 2);
for (i in 0...2)
{
final xOffset:Int = (i == 1) ? screenHalf : 0;
for (j in 0...hintsNoteDirections.length)
{
if (j == 1 || j == 2)
{
add(createHintLane(xOffset + hintWidth, (j == 1) ? boxHeight : 0, hintsNoteDirections[j], boxWidth, boxHeight,
hintsColors[j % hintsColors.length], false, showGradint));
}
else
{
add(createHintLane(xOffset + (j == 0 ? 0 : hintWidth + boxWidth), 0, hintsNoteDirections[j], hintWidth, hintHeight,
hintsColors[j % hintsColors.length], false, showGradint));
}
}
}
case FunkinHitboxControlSchemes.DoubleThumbDPad:
final hintSize:Int = 75;
final outlineThickness:Int = 5;
final hintsAngles:Array<Float> = [Math.PI, Math.PI / 2, Math.PI * 1.5, 0];
final hintsZoneRadius:Int = 115;
for (i in 0...2)
{
for (j in 0...hintsAngles.length)
{
final x:Float = ((i == 1) ? FlxG.width - (hintSize * 4) : hintSize * 2) + Math.cos(hintsAngles[j]) * hintsZoneRadius;
final y:Float = (FlxG.height - (hintSize * 3.75)) + Math.sin(hintsAngles[j]) * hintsZoneRadius;
add(createHintCircle(i == 0 ? x + FullScreenScaleMode.gameNotchSize.x : x - FullScreenScaleMode.gameNotchSize.x, y,
hintsNoteDirections[j % hintsNoteDirections.length], hintSize, outlineThickness, hintsColors[j % hintsColors.length]));
}
}
case FunkinHitboxControlSchemes.Arrows:
final hintWidth:Int = 146;
final hintHeight:Int = 149;
final noteSpacing:Int = 80;
final xPos:Int = Math.floor((FlxG.width - (hintWidth + noteSpacing) * hintsNoteDirections.length) / 2);
final yPos:Int = Math.floor(FlxG.height - hintHeight * 2 - 24);
for (i in 0...hintsNoteDirections.length)
{
add(createHintTransparentNote(xPos + i * hintWidth + noteSpacing * i, yPos, hintsNoteDirections[i % hintsNoteDirections.length], hintWidth,
hintHeight));
}
}
#end
scrollFactor.set();
ControlsHandler.setupHitbox(PlayerSettings.player1.controls, this, trackedInputs);
}
public function getFirstHintByDirection(direction:NoteDirection):Null<FunkinHint>
{
var result:Null<FunkinHint> = null;
forEachOfType(FunkinHint, function(hint:FunkinHint):Void {
@:privateAccess
if (hint.noteDirection == direction) result = hint;
});
return result;
}
/**
* Creates a new `FunkinHint` lane button along side a graphic label with specified properties.
*
* @param x The x position of the button.
* @param y The y position of the button.
* @param noteDirection The direction of the note the button represents (e.g. left, right).
* @param width The width of the button.
* @param height The height of the button.
* @param id The ID of the button.
* @param color The color of the button.
* @return A new `FunkinHint` object.
*/
function createHintLane(x:Float, y:Float, noteDirection:NoteDirection, width:Int, height:Int, color:FlxColor = 0xFFFFFFFF, label:Bool = true,
gradient:Bool = true):FunkinHint
{
final hint:FunkinHint = new FunkinHint(x, y, noteDirection, label ? createHintLaneLabelGraphic(width, height, Math.floor(height * 0.035), color) : null);
hint.loadGraphic(createHintLaneGraphic(width, height, color, gradient));
hint.onDown.add(onHintDown.dispatch.bind(hint));
hint.onUp.add(onHintUp.dispatch.bind(hint));
hint.onOut.add(onHintUp.dispatch.bind(hint));
hint.initTween(INVISIBLE_TILL_PRESS);
return hint;
}
/**
* Creates a new `FunkinHint` triangle button with specified properties.
*
* @param x The x position of the triangle button.
* @param y The y position of the triangle button.
* @param noteDirection The direction of the note the button represents (e.g. left, right).
* @param size The size of the triangle (base length).
* @param upright A boolean indicating if the triangle is upright (true) or inverted (false).
* @param id The unique ID of the triangle button.
* @param color The color of the triangle button (default is white).
* @return A new `FunkinHint` triangle object.
*/
function createHintTriangle(x:Float, y:Float, noteDirection:NoteDirection, width:Int, height:Int, color:FlxColor = 0xFFFFFFFF,
gradient:Bool = true):FunkinHint
{
final hint:FunkinHint = new FunkinHint(x, y, noteDirection, null);
hint.loadGraphic(createHintTriangleGraphic(width, height, noteDirection, color, gradient));
hint.onDown.add(onHintDown.dispatch.bind(hint));
hint.onUp.add(onHintUp.dispatch.bind(hint));
hint.onOut.add(onHintUp.dispatch.bind(hint));
hint.initTween(INVISIBLE_TILL_PRESS);
hint.polygon = getTriangleVertices(width, height, noteDirection);
return hint;
}
/**
* Creates a new `FunkinHint` circular button with specified properties.
*
* @param x The x position of the circular button.
* @param y The y position of the circular button.
* @param noteDirection The direction of the note the button represents (e.g., left, right).
* @param radius The radius of the circular button.
* @param outlineThickness The thickness of the outline for the circle.
* @param color The color of the circular button (default is white).
* @return A new `FunkinHint` circular object.
*/
function createHintCircle(x:Float, y:Float, noteDirection:NoteDirection, radius:Float, outlineThickness:Int, color:FlxColor = 0xFFFFFFFF):FunkinHint
{
final hint:FunkinHint = new FunkinHint(x, y, noteDirection, null);
hint.loadGraphic(createHintCircleGraphic(radius, outlineThickness, color));
hint.limitToBounds = false;
hint.radius = radius;
hint.onDown.add(onHintDown.dispatch.bind(hint));
hint.onUp.add(onHintUp.dispatch.bind(hint));
hint.onOut.add(onHintUp.dispatch.bind(hint));
hint.initTween(VISIBLE_TILL_PRESS);
return hint;
}
/**
* Creates a new `FunkinHint` representing a transparent note corresponding to the note from the scene.
* @param x The x position of the button.
* @param y The y position of the button.
* @param noteDirection The direction of the note the button represents (e.g. left, right).
* @param width The width of the button.
* @param height The height of the button.
* @return A new `FunkinHint` object.
*/
function createHintTransparentNote(x:Float, y:Float, noteDirection:NoteDirection, width:Int, height:Int):FunkinHint
{
final hint:FunkinHint = new FunkinHint(x, y, noteDirection, null);
hint.alpha = 0;
hint.setSize(width, height);
hint.onDown.add(onHintDown.dispatch.bind(hint));
hint.onUp.add(onHintUp.dispatch.bind(hint));
hint.onOut.add(onHintUp.dispatch.bind(hint));
var noteStyle:NoteStyle = NoteStyleRegistry.instance.fetchDefault();
@:privateAccess
@:nullSafety(Off)
{
hint.frames = Paths.getSparrowAtlas(noteStyle.getStrumlineAssetPath() ?? '', noteStyle.getAssetLibrary(noteStyle.getStrumlineAssetPath(true)));
FlxAnimationUtil.addAtlasAnimations(hint, noteStyle.getStrumlineAnimationData(noteDirection));
}
hint.animation.play('static', true);
hint.onDown.add(() -> {
hint.animation.play('press', true);
hint.centerOrigin();
hint.centerOffsets();
});
hint.onUp.add(() -> {
hint.animation.play('static', true);
hint.centerOrigin();
hint.centerOffsets();
});
hint.onOut.add(() -> {
hint.animation.play('static', true);
hint.centerOrigin();
hint.centerOffsets();
});
hint.centerOffsets();
hint.centerOrigin();
return hint;
}
/**
* Creates a lane graphic for a hint button.
*
* @param width The width of the graphic.
* @param height The height of the graphic.
* @param baseColor The base color of the graphic.
* @return A `FlxGraphic` object representing the button graphic.
*/
function createHintLaneGraphic(width:Int, height:Int, baseColor:FlxColor = 0xFFFFFFFF, gradient:Bool = true):FlxGraphic
{
final shape:Shape = new Shape();
if (gradient)
{
final matrix:Matrix = new Matrix();
matrix.createGradientBox(width, height, 0, 0, 0);
shape.graphics.beginGradientFill(RADIAL, [baseColor.to24Bit(), baseColor.to24Bit()], [0, baseColor.alphaFloat], [60, 255], matrix, PAD, RGB, 0);
}
else
{
shape.graphics.beginFill(baseColor.to24Bit(), baseColor.alphaFloat);
}
shape.graphics.drawRect(0, 0, width, height);
shape.graphics.endFill();
final graphicData:BitmapData = new BitmapData(width, height, true, 0);
graphicData.draw(shape, true);
return FlxGraphic.fromBitmapData(graphicData, false, null, false);
}
function createHintLaneLabelGraphic(width:Int, height:Int, labelHeight:Int, baseColor:FlxColor = 0xFFFFFFFF):FlxGraphic
{
final shape:Shape = new Shape();
shape.graphics.beginFill(0, 0);
shape.graphics.drawRect(0, 0, width, height);
shape.graphics.endFill();
final matrix:Matrix = new Matrix();
matrix.createGradientBox(width, labelHeight, Math.PI / 2, 0, 0);
shape.graphics.beginGradientFill(LINEAR, [baseColor.to24Bit(), baseColor.to24Bit()], [baseColor.alphaFloat, 0], [0, 255], matrix);
shape.graphics.drawRect(0, 0, width, labelHeight);
shape.graphics.endFill();
final matrix:Matrix = new Matrix();
matrix.createGradientBox(width, labelHeight, Math.PI / 2, 0, height - labelHeight);
shape.graphics.beginGradientFill(LINEAR, [baseColor.to24Bit(), baseColor.to24Bit()], [0, baseColor.alphaFloat], [0, 255], matrix);
shape.graphics.drawRect(0, height - labelHeight, width, labelHeight);
shape.graphics.endFill();
final graphicData:BitmapData = new BitmapData(width, height, true, 0);
graphicData.draw(shape, true);
return FlxGraphic.fromBitmapData(graphicData, false, null, false);
}
/**
* Creates a triangle graphic for a hint button.
*
* @param size The base length of the triangle.
* @param upright A boolean indicating if the triangle is upright (true) or inverted (false).
* @param baseColor The base color of the triangle graphic (default is white).
* @return A `FlxGraphic` object representing the triangle button graphic.
*/
function createHintTriangleGraphic(width:Int, height:Int, facing:NoteDirection, baseColor:FlxColor = 0xFFFFFFFF, gradient:Bool = true):FlxGraphic
{
final shape:Shape = new Shape();
if (gradient)
{
final matrix:Matrix = new Matrix();
matrix.createGradientBox(width, height, 0, 0, 0);
shape.graphics.beginGradientFill(RADIAL, [baseColor.to24Bit(), baseColor.to24Bit()], [0, baseColor.alphaFloat], [60, 255], matrix, PAD, RGB, 0);
}
else
{
shape.graphics.beginFill(baseColor.to24Bit(), baseColor.alphaFloat);
}
shape.graphics.drawRect(width / 2, height / 2, width / 2, height / 2);
shape.graphics.drawTriangles(Vector.ofArray(getTriangleVertices(width, height, facing)), Vector.ofArray([0, 1, 2]));
shape.graphics.endFill();
final graphicData:BitmapData = new BitmapData(width, height, true, 0);
graphicData.draw(shape, true);
return FlxGraphic.fromBitmapData(graphicData, false, null, false);
}
/**
* Creates a circular graphic for a hint button.
*
* @param radius The radius of the circle.
* @param baseColor The base color of the circle graphic (default is white).
* @param outlineThickness The thickness of the outline for the circle.
* @return A `FlxGraphic` object representing the circular button graphic.
*/
function createHintCircleGraphic(radius:Float, outlineThickness:Int, baseColor:FlxColor = 0xFFFFFFFF):FlxGraphic
{
var brightColor:FlxColor = baseColor;
brightColor.brightness += 0.6;
if (baseColor.brightness >= 0.75) baseColor.alphaFloat -= baseColor.brightness * 0.35;
final shape:Shape = new Shape();
shape.graphics.beginFill(baseColor.to24Bit(), baseColor.alphaFloat);
shape.graphics.lineStyle(outlineThickness, brightColor.to24Bit(), brightColor.alpha);
shape.graphics.drawCircle(radius, radius, radius);
shape.graphics.endFill();
final matrix:Matrix = new Matrix();
matrix.translate(outlineThickness, outlineThickness);
final graphicData:BitmapData = new BitmapData(Math.floor((radius + outlineThickness) * 2), Math.floor((radius + outlineThickness) * 2), true, 0);
graphicData.draw(shape, matrix, true);
return FlxGraphic.fromBitmapData(graphicData, false, null, false);
}
/**
* Сalculates vertices in a given direction
* @param width width of triangle
* @param height height of triangle
* @param facing The side the triangle faces
* @return array of vertices
*/
function getTriangleVertices(width:Int, height:Int, facing:NoteDirection):Array<Float>
{
if (facing == UP) facing = DOWN;
else if (facing == DOWN) facing = UP;
return switch (facing)
{
case UP: [width / 2, 0, 0, height, width, height];
case DOWN: [0, 0, width, 0, width / 2, height];
case LEFT: [0, 0, width, height / 2, 0, height];
case RIGHT: [width, 0, 0, height / 2, width, height];
}
}
/**
* Cleans up memory used by the `FunkinHitbox`.
*/
public override function destroy():Void
{
if (trackedInputs != null && trackedInputs.length > 0) ControlsHandler.removeCachedInput(PlayerSettings.player1.controls, trackedInputs);
FlxDestroyUtil.destroy(onHintDown);
FlxDestroyUtil.destroy(onHintUp);
super.destroy();
}
@:noCompletion
function set_isPixel(value:Bool):Bool
{
isPixel = value;
forEachOfType(FunkinHint, function(hint:FunkinHint):Void {
hint.isPixel = value;
});
return value;
}
}

View file

@ -1,125 +0,0 @@
package funkin.mobile.ui.mainmenu;
import flixel.FlxG;
import flixel.tweens.FlxTween;
import flixel.util.FlxSignal;
import flixel.util.FlxTimer;
import funkin.audio.FunkinSound;
import funkin.util.HapticUtil;
class FunkinOptionsButton extends FunkinButton
{
public var onConfirmStart(default, null):FlxSignal = new FlxSignal();
public var onConfirmEnd(default, null):FlxSignal = new FlxSignal();
var confirming:Bool = false;
var instant:Bool = false;
var held:Bool = false;
/**
* Creates a new FunkinOptionsButton instance.
*
* @param x The x position of the object.
* @param y The y position of the object.
* @param confirmCallback An optional callback function that will be triggered when the object is clicked.
* @param instant An optional flag that makes the button not play the full animation before calling the callback.
*/
public function new(?x:Float = 0, ?y:Float = 0, ?confirmCallback:Void->Void, instant:Bool = false):Void
{
super(x, y);
frames = Paths.getSparrowAtlas("mainmenu/optionsButton");
animation.addByIndices('idle', 'options', [0], "", 24, false);
animation.addByIndices('hold', 'options', [3], "", 24, false);
animation.addByIndices('confirm', 'options', [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], "", 24, false);
animation.play("idle");
scale.set(0.7, 0.7);
updateHitbox();
this.instant = instant;
this.ignoreDownHandler = true;
onUp.add(playConfirmAnim);
onDown.add(playHoldAnim);
onOut.add(playOutAnim);
onConfirmEnd.add(confirmCallback);
}
function playHoldAnim():Void
{
if (confirming || held) return;
held = true;
FlxTween.cancelTweensOf(this);
HapticUtil.vibrate(0, 0.01, 0.2);
animation.play('hold');
}
function playConfirmAnim():Void
{
if (instant)
{
onConfirmEnd.dispatch();
return;
}
else if (confirming)
{
return;
}
confirming = true;
FlxTween.cancelTweensOf(this);
HapticUtil.vibrate(0, 0.05, 0.5);
animation.play('confirm');
FunkinSound.playOnce(Paths.sound('confirmMenu'));
new FlxTimer().start(0.05, function(_) {
HapticUtil.vibrate(0, 0.01, 0.2);
}, 4);
onConfirmStart.dispatch();
animation.onFinish.addOnce(function(name:String) {
if (name != 'confirm') return;
onConfirmEnd.dispatch();
});
}
function playOutAnim():Void
{
if (confirming) return;
FlxTween.cancelTweensOf(this);
HapticUtil.vibrate(0, 0.01, 0.2);
animation.play('idle');
}
public function resetCallbacks():Void
{
onUp.removeAll();
onDown.removeAll();
onOut.removeAll();
confirming = false;
held = false;
onUp.add(playConfirmAnim);
onDown.add(playHoldAnim);
onOut.add(playOutAnim);
}
override function destroy():Void
{
super.destroy();
onConfirmStart.removeAll();
onConfirmEnd.removeAll();
if (animation != null && animation.onFinish != null) animation.onFinish.removeAll();
}
}

View file

@ -1,363 +0,0 @@
package funkin.mobile.ui.options;
import flixel.addons.transition.FlxTransitionableState;
import flixel.math.FlxMath;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
import flixel.FlxG;
import funkin.mobile.ui.options.objects.SchemeMenuButton;
import funkin.mobile.ui.options.objects.HitboxShowcase;
import funkin.mobile.ui.FunkinHitbox;
import funkin.util.TouchUtil;
import funkin.util.MathUtil;
import funkin.ui.MusicBeatSubState;
import funkin.ui.AtlasText;
import funkin.ui.FullScreenScaleMode;
import funkin.graphics.shaders.HSVShader;
import funkin.graphics.FunkinSprite;
import funkin.graphics.FunkinCamera;
import funkin.audio.FunkinSound;
import funkin.Preferences;
/**
* Represents the controls scheme menu.
* In this menu, you can change your controls scheme.
*/
class ControlsSchemeMenu extends MusicBeatSubState
{
/**
* Text that displays current scheme's name.
*/
var schemeNameText:AtlasText;
/**
* A camera for buttons.
*/
var camButtons:FunkinCamera;
/**
* A camera for hitbox showcases group.
*/
var camHitboxes:FunkinCamera;
/**
* A button that is changed depending if you are in the hitbox demo or not.
*/
var currentButton:SchemeMenuButton;
/**
* Group of hitbox showcase selection items.
*/
var hitboxShowcases:FlxTypedSpriteGroup<HitboxShowcase>;
/**
* An object used for selecting the current hitbox scheme.
*/
var itemNavHitbox:FunkinSprite;
/**
* Returns true, if player is currently in hitbox demonstration.
*/
var isInDemo:Bool;
/**
* An array of every single scheme.
*/
final availableSchemes:Array<String> = [
FunkinHitbox.FunkinHitboxControlSchemes.Arrows,
FunkinHitbox.FunkinHitboxControlSchemes.FourLanes,
FunkinHitbox.FunkinHitboxControlSchemes.DoubleThumbTriangle,
FunkinHitbox.FunkinHitboxControlSchemes.DoubleThumbSquare,
FunkinHitbox.FunkinHitboxControlSchemes.DoubleThumbDPad
];
/**
* Current selected index
*/
var currentIndex:Int = 0;
/**
* Touch X position when touch was just pressed. Resets on release.
*/
var dragStartingX:Int;
/**
* Touch X distance between dragStartingX and current touch position. Resets on release.
*/
var dragDistance:Int;
/**
* Represents the background shader for the menu, utilizing HSV color adjustments.
*/
var hsv:HSVShader = new HSVShader();
public override function create():Void
{
super.create();
FlxG.state.persistentDraw = false;
FlxG.state.persistentUpdate = false;
hsv.hue = -0.6;
hsv.saturation = 0.9;
hsv.value = 3.6;
final menuBG:FunkinSprite = FunkinSprite.create('menuBG');
menuBG.shader = hsv;
menuBG.setGraphicSize(Std.int(FlxG.width * 1.1));
menuBG.updateHitbox();
menuBG.screenCenter();
menuBG.scrollFactor.set(0, 0);
add(menuBG);
for (i in 0...availableSchemes.length)
{
if (availableSchemes[i] == Preferences.controlsScheme)
{
currentIndex = i;
break;
}
}
schemeNameText = new AtlasText(FlxG.width * 0.05, FlxG.height * 0.05, availableSchemes[currentIndex], AtlasFont.BOLD);
add(schemeNameText);
setupCameras();
setupHitboxShowcases();
createButton(false);
}
/**
* Setups every needed camera.
*/
function setupCameras():Void
{
final mainCamera:FunkinCamera = new FunkinCamera('SchemeMenuCamera');
mainCamera.bgColor = FlxColor.BLACK;
FlxG.cameras.add(mainCamera);
if (camControls != null) FlxG.cameras.remove(camControls);
camControls = new FunkinCamera('camControls');
camControls.bgColor = 0x0;
FlxG.cameras.add(camControls, false);
camButtons = new FunkinCamera('camButtons');
camButtons.bgColor = 0x0;
FlxG.cameras.add(camButtons, false);
camHitboxes = new FunkinCamera('camHitboxes');
camHitboxes.setScale(0.5, 0.5);
camHitboxes.bgColor = 0x0;
FlxG.cameras.add(camHitboxes, false);
}
/**
* Setups the hitbox showcase items.
*/
function setupHitboxShowcases():Void
{
hitboxShowcases = new FlxTypedSpriteGroup<HitboxShowcase>();
hitboxShowcases.x = (-1500 * currentIndex) + (-1500 / (availableSchemes.length + 1) * currentIndex);
for (i in 0...availableSchemes.length)
{
final hitboxShowcase:HitboxShowcase = new HitboxShowcase(0, 0, i, currentIndex, availableSchemes[i], onSelectHitbox);
hitboxShowcase.x = Math.floor(FlxG.width * -0.16 + (1500 * (i * FullScreenScaleMode.wideScale.x)));
hitboxShowcases.add(hitboxShowcase);
}
hitboxShowcases.cameras = [camHitboxes];
add(hitboxShowcases);
itemNavHitbox = new FunkinSprite(FlxG.width * 0.295).makeSolidColor(Std.int(FlxG.width * 0.25), Std.int(FlxG.height * 0.25), FlxColor.GREEN);
itemNavHitbox.cameras = [camButtons];
itemNavHitbox.updateHitbox();
itemNavHitbox.screenCenter(Y);
itemNavHitbox.visible = false;
add(itemNavHitbox);
}
/**
* Creates or recreates a scheme menu button.
* @param isDemoScreen Returns true, if player is currently in hitbox demo.
*/
function createButton(isDemoScreen:Bool):Void
{
if (currentButton != null) remove(currentButton);
if (isDemoScreen)
{
currentButton = new SchemeMenuButton(FlxG.width * 0.83, FlxG.height * 0.03, 'BACK', onHitboxDemoBack);
currentButton.text.x -= 5;
}
else
{
currentButton = new SchemeMenuButton(FlxG.width * 0.83, FlxG.height * 0.83, 'DEMO', onHitboxDemo);
currentButton.text.x -= 10;
}
add(currentButton);
}
/**
* Called when current hitbox has been selected.
*/
function onSelectHitbox():Void
{
currentButton.busy = true;
Preferences.controlsScheme = availableSchemes[currentIndex];
FlxTransitionableState.skipNextTransIn = true;
FlxTransitionableState.skipNextTransOut = true;
FlxG.switchState(() -> new funkin.ui.options.OptionsState());
}
/**
* Called when the current button is pressed and player is not in demo right now.
*/
function onHitboxDemo():Void
{
isInDemo = true;
FlxTween.tween(hsv, {hue: 0, saturation: 0, value: 0.5}, 0.5);
hitboxShowcases.forEach(function(hitboxShowcase:HitboxShowcase) {
hitboxShowcase.visible = false;
});
schemeNameText.visible = false;
createButton(true);
addHitbox(true, false, availableSchemes[currentIndex]);
hitbox.forEachAlive(function(hint:FunkinHint) {
if (availableSchemes[currentIndex] == FunkinHitboxControlSchemes.Arrows) hint.alpha = 1;
if (!hint.deadZones.contains(cast(currentButton.body, FunkinSprite))) hint.deadZones.push(cast(currentButton.body, FunkinSprite));
});
}
/**
* Called when the current button is pressed and player is in demo right now.
*/
function onHitboxDemoBack():Void
{
isInDemo = false;
FlxTween.tween(hsv, {hue: -0.6, saturation: 0.9, value: 3.6}, 0.5);
hitboxShowcases.forEach(function(hitboxShowcase:HitboxShowcase) {
hitboxShowcase.visible = true;
});
schemeNameText.visible = true;
createButton(false);
if (hitbox != null) hitbox.exists = false;
}
/**
* Updates selection using currentIndex.
* @param change Used to change currentIndex.
*/
function setSelection(index:Int):Void
{
final newIndex:Int = Math.floor(FlxMath.bound(index, 0, hitboxShowcases.length - 1));
if (currentIndex != newIndex)
{
currentIndex = newIndex;
}
else
{
return;
}
FunkinSound.playOnce(Paths.sound('scrollMenu'), 0.4);
schemeNameText.text = availableSchemes[currentIndex];
hitboxShowcases.forEach(function(hitboxShowcase:HitboxShowcase) {
hitboxShowcase.selectionIndex = currentIndex;
});
}
#if FEATURE_TOUCH_CONTROLS
/**
* Handles touch dragging.
*/
function handleDrag():Void
{
if (TouchUtil.justPressed && TouchUtil.touch != null) dragStartingX = TouchUtil.touch.x;
if (TouchUtil.pressed && TouchUtil.touch != null) dragDistance = TouchUtil.touch.x - dragStartingX;
if (TouchUtil.justReleased)
{
dragStartingX = 0;
dragDistance = 0;
}
// trace(dragDistance);
}
/**
* Handles all the touch inputs.
*/
function handleInputs():Void
{
if (isInDemo) return;
if (currentButton.busy) return;
handleDrag();
if (TouchUtil.pressAction(itemNavHitbox))
{
hitboxShowcases.members[currentIndex].onPress();
currentButton.busy = true;
}
}
/**
* HitboxShowcases X position when the player just pressed on the screen.
* Used for dragging.
*/
var originX:Float;
public override function update(elapsed:Float):Void
{
super.update(elapsed);
handleInputs();
if (TouchUtil.justPressed) originX = hitboxShowcases.x;
if (TouchUtil.pressed && dragDistance != 0)
{
final showcasesTargetX:Float = originX + dragDistance * 10;
hitboxShowcases.x = MathUtil.smoothLerpPrecision(hitboxShowcases.x, showcasesTargetX, elapsed, 0.5);
final minShowcasesX:Float = -1500 * availableSchemes.length;
hitboxShowcases.x = FlxMath.bound(hitboxShowcases.x, minShowcasesX, 400);
final targetIndex:Int = Math.round(hitboxShowcases.x / -1500);
if (currentIndex != targetIndex) setSelection(targetIndex);
}
else
{
hitboxShowcases.x = MathUtil.smoothLerpPrecision(hitboxShowcases.x, (-1500 * currentIndex) + (-1500 / (availableSchemes.length + 1) * currentIndex), elapsed, 0.5);
}
#end
}
}

View file

@ -1,120 +0,0 @@
package funkin.mobile.ui.options.objects;
import flixel.addons.display.shapes.FlxShapeBox;
import flixel.group.FlxSpriteGroup;
import flixel.effects.FlxFlicker;
import flixel.util.FlxSignal;
import flixel.util.FlxColor;
import flixel.FlxG;
import funkin.mobile.ui.FunkinHitbox;
import funkin.audio.FunkinSound;
import funkin.util.MathUtil;
/**
* Represents a showcase hitbox in the scheme menu.
*/
class HitboxShowcase extends FlxSpriteGroup
{
/**
* An array of values for lerping object's alpha.
*/
static final HITBOX_SHOWCASE_ALPHA:Array<Float> = [0.3, 1];
/**
* Object's own index.
*/
public var index:Int;
/**
* Current selection's index from menu where this object is used.
*/
public var selectionIndex:Int;
/**
* Indicates if object's index is equal to current selection's index.
*/
public var selected(get, never):Bool;
/**
* Signal dispatched when the object is selected. Additional behavior can be added by subscribing to this signal.
*/
public var onSelect(default, null):FlxSignal = new FlxSignal();
/**
* Indicates if the object is currently processing a selection (to avoid multiple triggers).
*/
public var busy:Bool = false;
/**
* Creates a new HitboxShowcase instance.
*
* @param x The x position of the object.
* @param y The y position of the object.
* @param index An integer used as object's index.
* @param selectionIndex Menu's current selection index.
* @param controlsScheme Hitbox's controls scheme.
* @param onClick An optional callback function that will be triggered when the object is clicked.
*/
public function new(x:Int = 0, y:Int = 0, index:Int, selectionIndex:Int = 0, controlsScheme:String, ?onClick:Void->Void):Void
{
super(x, y);
this.index = index;
this.selectionIndex = selectionIndex;
setupObjects(controlsScheme);
alpha = HITBOX_SHOWCASE_ALPHA[selected ? 1 : 0];
if (onClick != null) onSelect.add(onClick);
}
/**
* Creates and setups every needed object.
*
* @param controlsScheme Hitbox's controls scheme.
*/
function setupObjects(controlsScheme:String):Void
{
final bg:FlxShapeBox = new FlxShapeBox(0, 0, FlxG.width + 2, FlxG.height + 2, {thickness: 6, color: FlxColor.BLACK}, FlxColor.GRAY);
bg.screenCenter();
add(bg);
final hitbox:FunkinHitbox = new FunkinHitbox(controlsScheme, false);
hitbox.forEachAlive(function(hint:FunkinHint):Void {
if (controlsScheme != FunkinHitbox.FunkinHitboxControlSchemes.FourLanes) hint.alpha = 0.3;
});
hitbox.active = false;
add(hitbox);
}
/**
* Called when the object is both selected and pressed.
*/
public function onPress():Void
{
if (!busy)
{
busy = true;
FunkinSound.playOnce(Paths.sound('confirmMenu'));
FlxFlicker.flicker(this, 1, 0.06, true, false, function(_) {
busy = false;
onSelect.dispatch();
});
}
}
public override function update(elapsed:Float):Void
{
super.update(elapsed);
alpha = MathUtil.smoothLerpPrecision(alpha, HITBOX_SHOWCASE_ALPHA[selected ? 1 : 0], elapsed, 0.2);
}
function get_selected():Bool
{
return index == selectionIndex;
}
}

View file

@ -1,78 +0,0 @@
package funkin.mobile.ui.options.objects;
import flixel.addons.display.shapes.FlxShapeBox;
import flixel.effects.FlxFlicker;
import flixel.group.FlxSpriteGroup;
import flixel.util.FlxSignal;
import flixel.util.FlxColor;
import funkin.util.TouchUtil;
import funkin.util.SwipeUtil;
import funkin.audio.FunkinSound;
import funkin.ui.AtlasText;
/**
* Represents a button in the scheme menu, specifically designed for mobile touch input.
* The button displays text and allows selection through touch or an external callback.
*/
class SchemeMenuButton extends FlxSpriteGroup
{
/**
* The visual body of the button.
*/
public var body:Null<FlxShapeBox>;
/**
* The text displayed on the button.
*/
public var text:Null<AtlasText>;
/**
* Signal dispatched when the button is selected. Additional behavior can be added by subscribing to this signal.
*/
public var onSelect(default, null):FlxSignal = new FlxSignal();
/**
* Indicates if the button is currently processing a selection (to avoid multiple triggers).
*/
public var busy:Bool = false;
/**
* Creates a new SchemeMenuButton instance.
*
* @param xPos The x position of the button.
* @param yPos The y position of the button.
* @param labelText The text displayed on the button.
* @param onClick An optional callback function that will be triggered when the button is clicked.
*/
public function new(?xPos:Float = 0, ?yPos:Float = 0, labelText:String, ?onClick:Void->Void):Void
{
super(xPos, yPos);
body = new FlxShapeBox(0, 0, 200, 100, {thickness: 4, color: FlxColor.BLACK}, FlxColor.WHITE);
add(body);
text = new AtlasText(-150, -75, labelText, AtlasFont.DEFAULT);
add(text);
updateHitbox();
if (onClick != null) onSelect.add(onClick);
}
public override function update(elapsed:Float):Void
{
super.update(elapsed);
if (!busy && (TouchUtil.pressAction(this) && !SwipeUtil.swipeAny))
{
busy = true;
FunkinSound.playOnce(Paths.sound('confirmMenu'));
FlxFlicker.flicker(this, 1, 0.06, true, false, function(_) {
busy = false;
onSelect.dispatch();
});
}
}
}

View file

@ -1,309 +0,0 @@
package funkin.mobile.util;
#if FEATURE_MOBILE_ADVERTISEMENTS
import extension.admob.Admob;
import extension.admob.AdmobBannerAlign;
import extension.admob.AdmobBannerSize;
import extension.admob.AdmobEvent;
import flixel.FlxG;
import funkin.play.cutscene.VideoCutscene;
import funkin.util.macro.EnvironmentConfigMacro;
/**
* Provides utility functions for working with admob advertisements.
*/
@:nullSafety
class AdMobUtil
{
/**
* Counter that tracks the number of times a blueball event or a victory occurs.
*/
public static var PLAYING_COUNTER:UInt = 0;
/**
* The maximum number of actions or events allowed before an advertisement is shown.
*/
public static final MAX_BEFORE_AD:UInt = 3;
#if NO_TESTING_ADS
/**
* AdMob publisher ID used for the application.
*/
static final ADMOB_PUBLISHER:String = EnvironmentConfigMacro.environmentConfig.get("MOBILE_GLOBAL_ADMOB_PUBLISHER");
/**
* Test ad unit IDs for development and testing purposes.
* These IDs are provided by Google AdMob for testing ads without incurring costs.
* They should not be used in production applications.
*/
/**
* Ad unit ID for displaying banner ads.
*/
static final BANNER_AD_UNIT_ID:String = #if mobile EnvironmentConfigMacro.environmentConfig.get(#if android "ANDROID_ADMOB_BANNER_ID" #else "IOS_ADMOB_BANNER_ID" #end) #else "" #end;
/**
* Ad unit ID for displaying interstitial ads.
*/
static final INTERSTITIAL_AD_UNIT_ID:String = #if mobile EnvironmentConfigMacro.environmentConfig.get(#if android "ANDROID_ADMOB_INTERSTITIAL_ID" #else "IOS_ADMOB_INTERSTITIAL_ID" #end) #else "" #end;
/**
* Ad unit ID for displaying rewarded ads.
*/
static final REWARDED_AD_UNIT_ID:String = "";
#else
/**
* AdMob publisher ID used for the application.
* This ID is a test publisher ID provided by Google AdMob.
* Replace with your actual publisher ID for production.
*/
static final ADMOB_PUBLISHER:String = "ca-app-pub-3940256099942544";
/**
* Ad unit ID for displaying banner ads.
* Test IDs are used for Android and iOS platforms, while non-supported platforms default to an empty string.
* Replace with your actual banner ad unit ID for production.
*
* - Android: "9214589741" (test ad unit ID)
* - iOS: "2435281174" (test ad unit ID)
*/
static final BANNER_AD_UNIT_ID:String = #if android "9214589741" #elseif ios "2435281174" #else "" #end;
/**
* Ad unit ID for displaying interstitial ads.
* Test IDs are used for Android and iOS platforms, while non-supported platforms default to an empty string.
* Replace with your actual interstitial ad unit ID for production.
*
* - Android: "1033173712" (test ad unit ID)
* - iOS: "4411468910" (test ad unit ID)
*/
static final INTERSTITIAL_AD_UNIT_ID:String = #if android "1033173712" #elseif ios "4411468910" #else "" #end;
/**
* Ad unit ID for displaying rewarded ads.
* Test IDs are used for Android and iOS platforms, while non-supported platforms default to an empty string.
* Replace with your actual interstitial video ad unit ID for production.
*
* - Android: "8691691433" (test ad unit ID)
* - iOS: "5135589807" (test ad unit ID)
*/
static final REWARDED_AD_UNIT_ID:String = #if android "8691691433" #elseif ios "5135589807" #else "" #end;
#end
/**
* Initializes the AdMob SDK and sets up event listeners for interstitial and rewarded ads.
*
* The listeners display ads automatically when they are loaded.
*/
public static function init():Void
{
Admob.onEvent.add(function(event:AdmobEvent):Void {
#if ios
if (event.name == AdmobEvent.AVM_WILL_PLAY_AUDIO)
{
if (FlxG.sound.music != null) FlxG.sound.music.pause();
for (sound in FlxG.sound.list)
{
if (sound != null) sound.pause();
}
#if hxvlc
@:privateAccess
if (VideoCutscene.vid != null) VideoCutscene.vid.pause();
#end
}
else if (event.name == AdmobEvent.AVM_DID_STOP_PLAYING_AUDIO)
{
if (FlxG.sound.music != null) FlxG.sound.music.resume();
for (sound in FlxG.sound.list)
{
if (sound != null) sound.resume();
}
#if hxvlc
@:privateAccess
if (VideoCutscene.vid != null) VideoCutscene.vid.resume();
#end
}
#end
trace(event.toString());
});
Admob.configureConsentMetadata(Admob.getTCFConsentForPurpose(0) == 1, StringTools.startsWith(Admob.getUSPrivacy(), '1Y'));
Admob.init(#if TESTING_ADS true #else false #end);
}
/**
* Adds a banner ad at the specified size and alignment.
* @param size The size of the banner ad, defaulting to the standard banner size.
* @param align The alignment of the banner ad, defaulting to the bottom of the screen.
*/
public static inline function addBanner(size:Int = AdmobBannerSize.BANNER, align:Int = AdmobBannerAlign.BOTTOM_CENTER):Void
{
#if FEATURE_MOBILE_IAP
if (InAppPurchasesUtil.isPurchased(InAppPurchasesUtil.UPGRADE_PRODUCT_ID)) return;
#end
Admob.showBanner([AdMobUtil.ADMOB_PUBLISHER, AdMobUtil.BANNER_AD_UNIT_ID].join('/'), size, align);
}
/**
* Removes the currently displayed banner ad, if any.
*/
public static inline function removeBanner():Void
{
#if FEATURE_MOBILE_IAP
if (InAppPurchasesUtil.isPurchased(InAppPurchasesUtil.UPGRADE_PRODUCT_ID)) return;
#end
Admob.hideBanner();
}
/**
* Loads an interstitial ad using AdMob.
*
* @param onInterstitialFinish Callback function to be called when the rewarded ad has been completed by the user.
*/
public static function loadInterstitial(onInterstitialFinish:Void->Void):Void
{
#if FEATURE_MOBILE_IAP
if (InAppPurchasesUtil.isPurchased(InAppPurchasesUtil.UPGRADE_PRODUCT_ID))
{
if (onInterstitialFinish != null) onInterstitialFinish();
return;
}
#end
function interstitialEvent(event:AdmobEvent):Void
{
if (event.name == AdmobEvent.INTERSTITIAL_LOADED)
{
Admob.showInterstitial();
}
else if (event.name == AdmobEvent.INTERSTITIAL_DISMISSED
|| event.name == AdmobEvent.INTERSTITIAL_FAILED_TO_LOAD
|| event.name == AdmobEvent.INTERSTITIAL_FAILED_TO_SHOW)
{
if (onInterstitialFinish != null) onInterstitialFinish();
Admob.onEvent.remove(interstitialEvent);
}
}
Admob.onEvent.add(interstitialEvent);
Admob.loadInterstitial([AdMobUtil.ADMOB_PUBLISHER, AdMobUtil.INTERSTITIAL_AD_UNIT_ID].join('/'));
}
/**
* Loads a rewarded ad using Admob.
*
* @param onRewardedFinish Callback function to be called when the rewarded ad has been completed by the user.
*/
public static function loadRewarded(onRewardedFinish:Void->Void):Void
{
#if FEATURE_MOBILE_IAP
if (InAppPurchasesUtil.isPurchased(InAppPurchasesUtil.UPGRADE_PRODUCT_ID))
{
if (onRewardedFinish != null) onRewardedFinish();
return;
}
#end
function rewardedEvent(event:AdmobEvent):Void
{
if (event.name == AdmobEvent.REWARDED_LOADED)
{
Admob.showRewarded();
}
else if (event.name == AdmobEvent.REWARDED_DISMISSED
|| event.name == AdmobEvent.REWARDED_FAILED_TO_LOAD
|| event.name == AdmobEvent.REWARDED_FAILED_TO_SHOW)
{
if (onRewardedFinish != null) onRewardedFinish();
Admob.onEvent.remove(rewardedEvent);
}
}
Admob.onEvent.add(rewardedEvent);
Admob.loadRewarded([AdMobUtil.ADMOB_PUBLISHER, AdMobUtil.REWARDED_AD_UNIT_ID].join('/'));
}
/**
* Sets the volume level for ads with sound, allowing control over ad audio playback.
* @param volume A Float representing the desired volume (0 = mute, 1 = full volume).
*/
public static inline function setVolume(volume:Float):Void
{
Admob.setVolume(volume);
}
/**
* Checks whether consent for a specific advertising purpose has been granted.
* @param purpose The purpose for which consent is required.
* @return An Int indicating consent status (-1 for no consent, 1 for granted).
*/
public static inline function getTCFConsentForPurpose(purpose:Int):Int
{
return Admob.getTCFConsentForPurpose(purpose);
}
/**
* Checks if the user has given consent for all ad purposes.
* This is typically required for GDPR compliance, where each purpose (0-9) needs to be individually consented.
* @return Bool indicating whether the user has consented to all purposes.
*/
public static function hasFullTCFConsent():Bool
{
for (purpose in 0...Admob.getTCFPurposeConsent().length)
{
if (Admob.getTCFConsentForPurpose(purpose) != 1) return false;
}
return true;
}
/**
* Retrieves the current user's consent status as a string.
* Useful for GDPR compliance to understand if ads can be personalized.
* @return A String with the consent status.
*/
public static inline function getTCFPurposeConsent():String
{
return Admob.getTCFPurposeConsent();
}
/**
* Determines if showing a privacy options form is required based on regional laws.
* @return A Bool indicating if a privacy options form is required (true if required, false if not required).
*/
public static inline function isPrivacyOptionsRequired():Bool
{
return Admob.isPrivacyOptionsRequired();
}
/**
* Displays the privacy options form to the user, allowing them to adjust consent.
* Useful for GDPR and other privacy regulations compliance.
*/
public static inline function showPrivacyOptionsForm():Void
{
Admob.showPrivacyOptionsForm();
}
/**
* Opens the Ad Inspector interface.
* This method works for test devices registered programmatically or in the AdMob UI.
*/
public static inline function openAdInspector():Void
{
Admob.openAdInspector();
}
}
#end

View file

@ -1,345 +0,0 @@
package funkin.mobile.util;
#if FEATURE_MOBILE_IAP
#if android
import extension.androidtools.widget.Toast;
import extension.iapcore.android.IAPAndroid;
import extension.iapcore.android.IAPProductDetails;
import extension.iapcore.android.IAPPurchase;
import extension.iapcore.android.IAPPurchaseState;
import extension.iapcore.android.IAPResponseCode;
import extension.iapcore.android.IAPResult;
#elseif ios
import extension.iapcore.ios.IAPError;
import extension.iapcore.ios.IAPIOS;
import extension.iapcore.ios.IAPProductDetails;
import extension.iapcore.ios.IAPPurchase;
import extension.iapcore.ios.IAPPurchaseState;
#end
/**
* Provides utility functions for working with in-app purchases.
*/
@:nullSafety
class InAppPurchasesUtil
{
/**
* The product ID used for the "No Ads" in-app purchase upgrade.
*/
public static final UPGRADE_PRODUCT_ID:String = 'no_ads';
public static var hasInitialized:Bool = false;
/**
* A static variable that holds an array of currently loaded product details for in-app purchases.
*/
static var currentProductDetails:Array<IAPProductDetails> = [];
/**
* A static variable that holds an array of currently purchased for in-app purchases.
*/
static var currentPurchased:Array<IAPPurchase> = [];
/**
* Initializes the in-app purchases utility.
*/
public static function init():Void
{
#if android
IAPAndroid.onLog.add(function(message:String):Void {
trace(message);
});
IAPAndroid.onBillingSetupFinished.add(function(result:IAPResult):Void {
if (result.getResponseCode() != IAPResponseCode.OK)
{
trace('Billing setup failed "$result"!');
return;
}
IAPAndroid.queryPurchases();
IAPAndroid.queryProductDetails([UPGRADE_PRODUCT_ID]);
});
IAPAndroid.onBillingServiceDisconnected.add(function():Void {
trace("Billing service disconnected!");
});
IAPAndroid.onProductDetailsResponse.add(function(result:IAPResult, productDetails:Array<IAPProductDetails>):Void {
if (result.getResponseCode() == IAPResponseCode.OK)
{
hasInitialized = true;
currentProductDetails = productDetails;
}
else
{
hasInitialized = false;
trace('Failed to fetch product details: "$result"');
}
});
IAPAndroid.onQueryPurchasesResponse.add(function(result:IAPResult, purchases:Array<IAPPurchase>):Void {
if (result.getResponseCode() == IAPResponseCode.OK) handlePurchases(purchases);
else
{
trace('Failed to query purchases: "$result"');
}
});
IAPAndroid.onPurchasesUpdated.add(function(result:IAPResult, purchases:Array<IAPPurchase>):Void {
if (result.getResponseCode() == IAPResponseCode.OK) handlePurchases(purchases);
else
{
trace('Failed to update purchases: "$result"');
}
});
IAPAndroid.onAcknowledgePurchaseResponse.add(function(result:IAPResult):Void {
if (result.getResponseCode() == IAPResponseCode.OK) trace('Purchase acknowledged successfully!');
else
{
trace('Failed to acknowledge purchase: $result');
}
});
IAPAndroid.init();
IAPAndroid.startConnection();
#else
IAPIOS.onProductDetailsReceived.add(function(productDetails:Array<IAPProductDetails>):Void {
if (productDetails != null)
{
currentProductDetails = productDetails;
}
});
IAPIOS.onProductDetailsFailed.add(function(error:IAPError):Void {
hasInitialized = false;
});
IAPIOS.onPurchasesUpdated.add(function(purchases:Array<IAPPurchase>):Void {
handlePurchases(purchases);
trace("iOS purchases updated: " + purchases.length);
hasInitialized = true;
trace("hasInitialized: " + hasInitialized);
});
IAPIOS.init();
IAPIOS.restorePurchases();
IAPIOS.requestProducts([UPGRADE_PRODUCT_ID]);
#end
}
/**
* Restores previously made in-app purchases for the current user.
*/
public static function restorePurchases():Void
{
#if android
IAPAndroid.queryPurchases();
#else
IAPIOS.restorePurchases();
#end
}
/**
* Initiates the purchase process for the specified item.
*
* @param id The identifier of the item to be purchased.
* @param onPurchased The function to be called when the the product is purchased.
*/
public static function purchase(id:String, onPurchased:Void->Void):Void
{
for (product in currentProductDetails)
{
#if android
if (product.getProductId() == id)
{
function purchasesUpdatedEvent(result:IAPResult, purchases:Array<IAPPurchase>):Void
{
if (result.getResponseCode() == IAPResponseCode.OK)
{
for (purchase in purchases)
{
if (purchase.getProducts().contains(id))
{
if (purchase.getPurchaseState() == IAPPurchaseState.PURCHASED)
{
if (onPurchased != null) onPurchased();
IAPAndroid.onPurchasesUpdated.remove(purchasesUpdatedEvent);
}
}
}
}
else
{
IAPAndroid.onPurchasesUpdated.remove(purchasesUpdatedEvent);
}
final debugMessage:Null<String> = result.getDebugMessage();
if (debugMessage != null && debugMessage.length > 0)
{
Toast.makeText(debugMessage, Toast.LENGTH_SHORT);
}
}
if (!IAPAndroid.onPurchasesUpdated.has(purchasesUpdatedEvent))
{
IAPAndroid.onPurchasesUpdated.add(purchasesUpdatedEvent);
}
IAPAndroid.launchPurchaseFlow(product);
return;
}
#elseif ios
if (product.getProductIdentifier() == id)
{
function purchasesUpdatedEvent(purchases:Array<IAPPurchase>):Void
{
for (purchase in purchases)
{
if (purchase.getPaymentProductIdentifier() == id)
{
switch (purchase.getTransactionState())
{
case IAPPurchaseState.PURCHASED:
if (onPurchased != null) onPurchased();
IAPIOS.onPurchasesUpdated.remove(purchasesUpdatedEvent);
case IAPPurchaseState.FAILED:
IAPIOS.onPurchasesUpdated.remove(purchasesUpdatedEvent);
default:
}
}
}
}
if (!IAPIOS.onPurchasesUpdated.has(purchasesUpdatedEvent))
{
IAPIOS.onPurchasesUpdated.add(purchasesUpdatedEvent);
}
IAPIOS.purchaseProduct(product);
return;
}
#end
}
trace("Didn't find product details for ID: " + id);
}
/**
* Checks if the specified product ID is already purchased.
*
* @param id The product ID to check.
*
* @return `true` if the product is already purchased and acknowledged, false otherwise.
*/
public static function isPurchased(id:String):Bool
{
for (purchase in currentPurchased)
{
#if android
if (purchase.getProducts().contains(id))
{
return true;
}
#elseif ios
if (purchase.getPaymentProductIdentifier() == id)
{
return true;
}
#end
}
return false;
}
@:noCompletion
private static function handlePurchases(purchases:Array<IAPPurchase>):Void
{
for (purchase in purchases)
{
#if android
if (purchase.getPurchaseState() == IAPPurchaseState.PURCHASED)
{
if (!purchase.isAcknowledged())
{
IAPAndroid.acknowledgePurchase(purchase.getPurchaseToken());
}
var alreadyTracked:Bool = false;
for (existing in currentPurchased)
{
if (existing.getPurchaseToken() == purchase.getPurchaseToken())
{
alreadyTracked = true;
break;
}
}
if (!alreadyTracked)
{
currentPurchased.push(purchase);
trace('Android purchase tracked: ${purchase.getPurchaseToken()}');
}
else
{
trace('Android purchase already tracked: ${purchase.getPurchaseToken()}');
}
}
else
{
trace('Android purchase not completed: ${purchase.getPurchaseState()}');
}
#elseif ios
trace('Transaction ID: ${purchase.getTransactionIdentifier()}');
trace('Transaction Date: ${purchase.getTransactionDate()}');
trace('Transaction Payment Product ID: ${purchase.getPaymentProductIdentifier()}');
var alreadyTracked:Bool = false;
for (existing in currentPurchased)
{
if (existing.getTransactionIdentifier() == purchase.getTransactionIdentifier())
{
alreadyTracked = true;
break;
}
}
switch (purchase.getTransactionState())
{
case IAPPurchaseState.PURCHASING:
trace('iOS purchase is in progress.');
case IAPPurchaseState.DEFERRED:
trace('iOS purchase is deferred.');
case IAPPurchaseState.FAILED:
trace('iOS purchase failed: ${purchase.getTransactionError()}.');
case IAPPurchaseState.PURCHASED | IAPPurchaseState.RESTORED:
trace('iOS purchase successful or restored.');
if (!alreadyTracked)
{
currentPurchased.push(purchase);
trace('iOS purchase tracked: ${purchase.getTransactionIdentifier()}');
IAPIOS.finishPurchase(purchase);
}
else
{
trace('iOS purchase already tracked: ${purchase.getTransactionIdentifier()}');
}
}
#end
}
}
}
#end

View file

@ -1,72 +0,0 @@
package funkin.mobile.util;
#if FEATURE_MOBILE_IAR
#if android
import extension.iarcore.android.IARAndroid as IAR;
#elseif ios
import extension.iarcore.ios.IARIOS as IAR;
#end
#end
/**
* Provides utility functions for working with in-app reviews.
* @see https://developer.android.com/guide/playcore/in-app-review
*/
@:nullSafety
class InAppReviewUtil
{
/**
* Chance for exiting the Results screen to display a prompt to review the game, as a percent.
*/
public static var ODDS:UInt = 5;
/**
* Initializes callbacks tied to the In-App Review functionality.
*/
public static function init():Void
{
#if FEATURE_MOBILE_IAR
#if android
trace('[IAR] Initializing callbacks...');
IAR.onLog.add(function(message:String):Void {
trace('[IAR] Error occurred: "$message"');
});
IAR.onReviewCompleted.add(function(success:Bool):Void {
trace('[IAR] Review completed: "${success ? 'Success' : 'Failure'}"');
});
IAR.onReviewError.add(function(message:String):Void {
trace('[IAR] Review failed: "$message"');
});
#end
#else
trace('[IAR] IAR is disabled...');
#end
}
/**
* When called, displays a card which prompts the user to provide a review of the game,
* which will be posted to the respective app store.
*
* Google Play will throttle this for us t
*/
public static function requestReview():Void
{
#if FEATURE_MOBILE_IAR
trace('[IAR] Sending in-app review request...');
#if android
IAR.init();
#if FEATURE_DEBUG_FUNCTIONS
IAR.requestAndLaunchFakeReviewFlow();
#else
IAR.requestAndLaunchReviewFlow();
#end
#else
IAR.requestReview();
#end
#else
trace('[IAR] IAR is disabled...');
#end
}
}

View file

@ -1,101 +0,0 @@
package funkin.mobile.util;
#if ios
import funkin.external.ios.ScreenUtil as NativeScreenUtil;
#elseif android
import funkin.external.android.ScreenUtil as NativeScreenUtil;
#end
import lime.math.Rectangle;
import lime.system.System;
/**
* A Utility class to get mobile screen related informations.
*/
class ScreenUtil
{
/**
* Get `Rectangle` Object that contains the dimensions of the screen's Notch.
* Scales the dimensions to return coords in pixels, not points
* @return Rectangle
*/
public static function getNotchRect():Rectangle
{
final notchRect:Rectangle = new Rectangle();
#if android
final rectDimensions:Array<Array<Float>> = [[], [], [], []];
// Push all the dimensions of the cutouts into an array
for (rect in NativeScreenUtil.getCutoutDimensions())
{
rectDimensions[0].push(rect.x);
rectDimensions[1].push(rect.y);
rectDimensions[2].push(rect.width);
rectDimensions[3].push(rect.height);
}
// Put all the dimensions into the rectangle
for (i => dimensions in rectDimensions)
{
for (dimension in dimensions)
{
switch (i)
{
case 0:
notchRect.x += dimension;
case 1:
notchRect.y += dimension;
case 2:
notchRect.width += dimension;
case 3:
notchRect.height += dimension;
}
}
}
#elseif ios
var topInset:Float = -1;
var leftInset:Float = -1;
var rightInset:Float = -1;
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)
{
case DISPLAY_ORIENTATION_LANDSCAPE: // landscape
notchRect.width = leftInset + rightInset;
notchRect.height = bottomInset - topInset;
notchRect.y = topInset;
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
case DISPLAY_ORIENTATION_PORTRAIT: // portrait
notchRect.width = deviceWidth;
notchRect.height = topInset;
case DISPLAY_ORIENTATION_PORTRAIT_FLIPPED: // portrait
notchRect.width = deviceWidth;
notchRect.height = bottomInset;
notchRect.y = deviceHeight - notchRect.height; // move notchRect if we are flipped, the notch is at the bottom of screen
default: // display orientation unknown? perhaps this occurs on desktop
}
#end
return notchRect;
}
}

View file

@ -1,54 +0,0 @@
package funkin.modding;
import haxe.ds.StringMap;
/**
* Temporary persistent data storage for mods to use.
*/
@:nullSafety
class ModStore
{
/**
* All registered stores for this session.
*/
public static final stores:StringMap<Dynamic> = new StringMap<Dynamic>();
/**
* Attempts to register a new store with the given ID and return it.
* If a store with the same ID already exists, that store will be returned instead (discards `data`).
*
* @id The unique ID for this store.
* @data Optional initial data, uses an empty object by default.
* @return The store data at the given ID.
*/
public static function register(id:String, ?data:Dynamic):Dynamic
{
if (stores.exists(id)) return stores.get(id);
stores.set(id, data ??= {});
return data;
}
/**
* Helper function to get a store by ID.
*
* @id The target ID of the store.
* @return The store data, or `null` if the store did not exist.
*/
public static function get(id:String):Null<Dynamic>
{
return stores.get(id);
}
/**
* Helper function to remove a store by ID and return it.
*
* @id The target ID of the store.
* @return The store data, or `null` if the store did not exist.
*/
public static function remove(id:String):Null<Dynamic>
{
var data:Null<Dynamic> = stores.get(id);
stores.remove(id);
return data;
}
}

View file

@ -2,7 +2,6 @@ package funkin.modding;
import polymod.Polymod;
@:nullSafety
class PolymodErrorHandler
{
/**
@ -48,7 +47,7 @@ class PolymodErrorHandler
logError(error.message);
// Last word is the class name.
var className:Null<String> = error.message.split(' ').pop();
var className:String = error.message.split(' ').pop();
var msg:String = 'Import error in ${error.origin}';
msg += '\nCould not import unknown class ${className}';
msg += '\nCheck to ensure the class exists and is spelled correctly.';

View file

@ -26,19 +26,13 @@ import polymod.Polymod;
/**
* A class for interacting with Polymod, the atomic modding framework for Haxe.
*/
@:nullSafety
class PolymodHandler
{
/**
* The API version for the current version of the game. Since 0.5.0, we've just made this the game version!
* Minor updates rarely impact mods but major versions sometimes do.
* Minor updates rarely impact mods but major versions often do.
*/
public static var API_VERSION(get, never):String;
static function get_API_VERSION():String
{
return Constants.VERSION;
}
// static final API_VERSION:String = Constants.VERSION;
/**
* The Semantic Versioning rule
@ -46,7 +40,7 @@ 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.6.3 <0.8.0";
static final API_VERSION_RULE:String = ">=0.6.3 <0.7.0";
/**
* Where relative to the executable that mods are located.
@ -71,7 +65,7 @@ class PolymodHandler
public static var loadedModIds:Array<String> = [];
// Use SysZipFileSystem on native and MemoryZipFilesystem on web.
// Use SysZipFileSystem on desktop and MemoryZipFilesystem on web.
static var modFileSystem:Null<ZipFileSystem> = null;
/**
@ -272,19 +266,18 @@ class PolymodHandler
#if FEATURE_NEWGROUNDS
// `funkin.api.newgrounds.Leaderboards` allows for submitting cheated scores.
// We still grant read-only access.
Polymod.addImportAlias('funkin.api.newgrounds.Leaderboards', funkin.api.newgrounds.Leaderboards.LeaderboardsSandboxed);
// `funkin.api.newgrounds.Medals` allows for unfair granting of medals.
// We still grant read-only access.
Polymod.addImportAlias('funkin.api.newgrounds.Medals', funkin.api.newgrounds.Medals.MedalsSandboxed);
// `funkin.api.newgrounds.NewgroundsClientSandboxed` allows for submitting cheated data.
// We still grant read-only access.
Polymod.addImportAlias('funkin.api.newgrounds.NewgroundsClient', funkin.api.newgrounds.NewgroundsClient.NewgroundsClientSandboxed);
#end
#if FEATURE_DISCORD_RPC
Polymod.addImportAlias('funkin.api.discord.DiscordClient', funkin.api.discord.DiscordClient.DiscordClientSandboxed);
#end
// Add blacklisting for prohibited classes and packages.
@ -308,48 +301,6 @@ class PolymodHandler
// Unserializer.DEFAULT_RESOLVER.resolveClass() can access blacklisted packages
Polymod.blacklistImport('haxe.Unserializer');
// `flixel.util.FlxSave`
// FlxSave.resolveFlixelClasses() can access blacklisted packages
Polymod.blacklistImport('flixel.util.FlxSave');
// Disable access to AdMob Util
Polymod.blacklistImport('funkin.mobile.util.AdMobUtil');
// Disable access to In-App Purchases Util
Polymod.blacklistImport('funkin.mobile.util.InAppPurchasesUtil');
// Disable access to Admob Extension
for (cls in ClassMacro.listClassesInPackage('extension.admob'))
{
if (cls == null) continue;
var className:String = Type.getClassName(cls);
Polymod.blacklistImport(className);
}
// Disable access to AndroidTools Extension
for (cls in ClassMacro.listClassesInPackage('extension.androidtools'))
{
if (cls == null) continue;
var className:String = Type.getClassName(cls);
Polymod.blacklistImport(className);
}
// Disable access to IAPCore Extension
for (cls in ClassMacro.listClassesInPackage('extension.iapcore'))
{
if (cls == null) continue;
var className:String = Type.getClassName(cls);
Polymod.blacklistImport(className);
}
// Disable access to Haptics Extension
for (cls in ClassMacro.listClassesInPackage('extension.haptics'))
{
if (cls == null) continue;
var className:String = Type.getClassName(cls);
Polymod.blacklistImport(className);
}
// `lime.system.CFFI`
// Can load and execute compiled binaries.
Polymod.blacklistImport('lime.system.CFFI');
@ -374,9 +325,6 @@ class PolymodHandler
// Can load native processes on the host operating system.
Polymod.blacklistImport('openfl.desktop.NativeProcess');
// Contains critical private environment variables.
Polymod.blacklistImport('funkin.util.macro.EnvironmentConfigMacro');
// `funkin.api.*`
// Contains functions which may allow for cheating and such.
for (cls in ClassMacro.listClassesInPackage('funkin.api'))
@ -396,24 +344,6 @@ class PolymodHandler
Polymod.blacklistImport(className);
}
// `hscript.*
// Contains functions which may allow for interpreting unsanitized strings.
for (cls in ClassMacro.listClassesInPackage('hscript'))
{
if (cls == null) continue;
var className:String = Type.getClassName(cls);
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'))

View file

@ -3,13 +3,11 @@ package funkin.modding.base;
/**
* An empty base class meant to be extended by scripts.
*/
@:nullSafety
class Object
{
class Object {
public function new() {}
public function toString():String
{
public function toString():String {
return "(Object)";
}
}

View file

@ -15,7 +15,6 @@ import openfl.events.KeyboardEvent;
* This is a base class for all events that are issued to scripted classes.
* It can be used to identify the type of event called, store data, and cancel event propagation.
*/
@:nullSafety
class ScriptEvent
{
/**
@ -256,8 +255,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,
cancelable:Bool = false):Void
public function new(type:ScriptEventType, holdNote:SustainTrail, healthChange:Float, score:Int, isComboBreak:Bool, comboCount:Int = 0, cancelable:Bool = false):Void
{
super(type, null, healthChange, comboCount, true);
this.holdNote = holdNote;

View file

@ -6,7 +6,6 @@ import funkin.modding.IScriptedClass;
/**
* Utility functions to assist with handling scripted classes.
*/
@:nullSafety
class ScriptEventDispatcher
{
/**

View file

@ -1,6 +1,5 @@
package funkin.modding.events;
@:nullSafety
enum abstract ScriptEventType(String) from String to String
{
/**

View file

@ -8,7 +8,6 @@ import funkin.modding.events.ScriptEvent;
* A module is a scripted class which receives all events without requiring a specific context.
* You may have the module active at all times, or only when another script enables it.
*/
@:nullSafety
class Module implements IPlayStateScriptedClass implements IStateChangingScriptedClass
{
/**
@ -29,7 +28,7 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte
*
* Priority 1 is processed before Priority 1000, etc.
*/
public var priority(default, set):Int = 1000;
public var priority(default, set):Int;
function set_priority(value:Int):Int
{

View file

@ -10,7 +10,6 @@ import funkin.modding.module.ScriptedModule;
/**
* Utility functions for loading and manipulating active modules.
*/
@:nullSafety
class ModuleHandler
{
static final moduleCache:Map<String, Module> = new Map<String, Module>();
@ -75,15 +74,11 @@ class ModuleHandler
* Given two module IDs, sort them by priority.
* @return 1 or -1 depending on which module has a higher priority.
*/
static function sortByPriority(a:String, b:String):Int
static function sortByPriority(a:String, b:String)
{
var aModule:Null<Module> = getModule(a);
var bModule:Null<Module> = getModule(b);
var aModule:Module = moduleCache.get(a);
var bModule:Module = moduleCache.get(b);
if (aModule == null || bModule == null)
{
return 0;
}
if (aModule.priority != bModule.priority)
{
return aModule.priority - bModule.priority;
@ -94,14 +89,14 @@ class ModuleHandler
}
}
public static function getModule(moduleId:String):Null<Module>
public static function getModule(moduleId:String):Module
{
return moduleCache.get(moduleId);
}
public static function activateModule(moduleId:String):Void
{
var module:Null<Module> = getModule(moduleId);
var module:Module = getModule(moduleId);
if (module != null)
{
module.active = true;
@ -110,7 +105,7 @@ class ModuleHandler
public static function deactivateModule(moduleId:String):Void
{
var module:Null<Module> = getModule(moduleId);
var module:Module = getModule(moduleId);
if (module != null)
{
module.active = false;
@ -141,7 +136,7 @@ class ModuleHandler
{
for (moduleId in modulePriorityOrder)
{
var module:Null<Module> = moduleCache.get(moduleId);
var module:Module = moduleCache.get(moduleId);
// The module needs to be active to receive events.
if (module != null && module.active)
{

View file

@ -1,14 +1,13 @@
package funkin.play;
import funkin.ui.freeplay.charselect.PlayableCharacter;
import flixel.FlxState;
import funkin.data.freeplay.player.PlayerRegistry;
import flixel.FlxG;
import flixel.FlxObject;
import flixel.FlxSprite;
import flixel.input.touch.FlxTouch;
import flixel.util.FlxColor;
import flixel.util.FlxTimer;
import funkin.util.HapticUtil;
import funkin.audio.FunkinSound;
import funkin.graphics.FunkinSprite;
import funkin.modding.events.ScriptEvent;
@ -20,10 +19,6 @@ import funkin.ui.story.StoryMenuState;
import funkin.util.MathUtil;
import funkin.effects.RetroCameraFade;
import flixel.math.FlxPoint;
import funkin.util.TouchUtil;
#if FEATURE_MOBILE_ADVERTISEMENTS
import funkin.mobile.util.AdMobUtil;
#end
/**
* A substate which renders over the PlayState when the player dies.
@ -99,8 +94,6 @@ class GameOverSubState extends MusicBeatSubState
var targetCameraZoom:Float = 1.0;
var canInput:Bool = false;
public function new(params:GameOverParams)
{
super();
@ -108,19 +101,9 @@ class GameOverSubState extends MusicBeatSubState
this.isChartingMode = params?.isChartingMode ?? false;
transparent = params.transparent;
cameraFollowPoint = new FlxObject(0, 0, 1, 1);
if (parentPlayState != null)
{
cameraFollowPoint.x = parentPlayState.cameraFollowPoint.x;
cameraFollowPoint.y = parentPlayState.cameraFollowPoint.y;
}
cameraFollowPoint = new FlxObject(PlayState.instance.cameraFollowPoint.x, PlayState.instance.cameraFollowPoint.y, 1, 1);
}
/**
* The PlayState that this GameOverSubState is displaying on top of.
*/
public var parentPlayState:Null<PlayState>;
/**
* Reset the game over configuration to the default.
*/
@ -136,18 +119,19 @@ class GameOverSubState extends MusicBeatSubState
{
if (instance != null)
{
// TODO: Do something in this case? IDK.
FlxG.log.warn('WARNING: GameOverSubState instance already exists. This should not happen.');
}
instance = this;
super.create();
parentPlayState = cast _parentState;
//
// Set up the visuals
//
var playState = PlayState.instance;
// Add a black background to the screen.
var bg:FunkinSprite = new FunkinSprite().makeSolidColor(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK);
// We make this transparent so that we can see the stage underneath during debugging,
@ -159,10 +143,10 @@ class GameOverSubState extends MusicBeatSubState
// Pluck Boyfriend from the PlayState and place him (in the same position) in the GameOverSubState.
// We can then play the character's `firstDeath` animation.
if ((parentPlayState?.isMinimalMode ?? true)) {}
if (PlayState.instance.isMinimalMode) {}
else
{
boyfriend = parentPlayState?.currentStage.getBoyfriend(true);
boyfriend = PlayState.instance.currentStage.getBoyfriend(true);
if (boyfriend != null)
{
boyfriend.canPlayOtherAnims = true;
@ -180,26 +164,15 @@ class GameOverSubState extends MusicBeatSubState
// The conductor now represents the BPM of the game over music.
Conductor.instance.update(0);
#if mobile
addBackButton(FlxG.width - 230, FlxG.height - 200, FlxColor.WHITE, goBack);
#end
HapticUtil.vibrate(0, Constants.DEFAULT_VIBRATION_DURATION);
// Allow input a second later to prevent accidental gameover skips.
new FlxTimer().start(1, function(tmr:FlxTimer) {
canInput = true;
});
}
@:nullSafety(Off)
function setCameraTarget():Void
{
if ((parentPlayState?.isMinimalMode ?? true) || boyfriend == null) return;
if (PlayState.instance.isMinimalMode || boyfriend == null) return;
// Assign a camera follow point to the boyfriend's position.
cameraFollowPoint = new FlxObject(parentPlayState.cameraFollowPoint.x, parentPlayState.cameraFollowPoint.y, 1, 1);
cameraFollowPoint = new FlxObject(PlayState.instance.cameraFollowPoint.x, PlayState.instance.cameraFollowPoint.y, 1, 1);
cameraFollowPoint.x = getMidPointOld(boyfriend).x;
cameraFollowPoint.y = getMidPointOld(boyfriend).y;
var offsets:Array<Float> = boyfriend.getDeathCameraOffsets();
@ -209,7 +182,7 @@ class GameOverSubState extends MusicBeatSubState
FlxG.camera.target = null;
FlxG.camera.follow(cameraFollowPoint, LOCKON, Constants.DEFAULT_CAMERA_FOLLOW_RATE / 2);
targetCameraZoom = (parentPlayState?.currentStage?.camZoom ?? 1.0) * boyfriend.getDeathCameraZoom();
targetCameraZoom = (PlayState?.instance?.currentStage?.camZoom ?? 1.0) * boyfriend.getDeathCameraZoom();
}
/**
@ -234,7 +207,7 @@ class GameOverSubState extends MusicBeatSubState
public function resetCameraZoom():Void
{
// Apply camera zoom level from stage data.
FlxG.camera.zoom = parentPlayState?.currentStage?.camZoom ?? 1.0;
FlxG.camera.zoom = PlayState?.instance?.currentStage?.camZoom ?? 1.0;
}
var hasStartedAnimation:Bool = false;
@ -245,7 +218,7 @@ class GameOverSubState extends MusicBeatSubState
{
hasStartedAnimation = true;
if (boyfriend == null || (parentPlayState?.isMinimalMode ?? true))
if (boyfriend == null || PlayState.instance.isMinimalMode)
{
// Play the "blue balled" sound. May play a variant if one has been assigned.
playBlueBalledSFX();
@ -272,16 +245,76 @@ class GameOverSubState extends MusicBeatSubState
// Handle user inputs.
//
// Restart the level when pressing the assigned key.
if ((controls.ACCEPT #if mobile || (TouchUtil.pressAction() && !TouchUtil.overlaps(backButton) && canInput) #end)
&& blueballed
&& !mustNotExit)
// MOBILE ONLY: Restart the level when tapping Boyfriend.
if (FlxG.onMobile)
{
var touch:FlxTouch = FlxG.touches.getFirst();
if (touch != null)
{
if (boyfriend == null || touch.overlaps(boyfriend))
{
confirmDeath();
}
}
}
// KEYBOARD ONLY: Restart the level when pressing the assigned key.
if (controls.ACCEPT && blueballed && !mustNotExit)
{
blueballed = false;
confirmDeath();
}
if (controls.BACK && !mustNotExit && !isEnding) goBack();
// KEYBOARD ONLY: Return to the menu when pressing the assigned key.
if (controls.BACK && !mustNotExit && !isEnding)
{
isEnding = true;
blueballed = false;
PlayState.instance.deathCounter = 0;
// PlayState.seenCutscene = false; // old thing...
if (gameOverMusic != null) gameOverMusic.stop();
// Stop death quotes immediately.
hasPlayedDeathQuote = true;
if (deathQuoteSound != null)
{
deathQuoteSound.stop();
deathQuoteSound = null;
}
if (isChartingMode)
{
this.close();
if (FlxG.sound.music != null) FlxG.sound.music.pause(); // Don't reset song position!
PlayState.instance.close(); // This only works because PlayState is a substate!
return;
}
else
{
var targetState:funkin.ui.transition.stickers.StickerSubState->FlxState = (PlayStatePlaylist.isStoryMode) ? (sticker) ->
new StoryMenuState(sticker) : (sticker) -> FreeplayState.build(sticker);
if (PlayStatePlaylist.isStoryMode)
{
PlayStatePlaylist.reset();
}
var stickerPackId:Null<String> = PlayState.instance.currentChart.stickerPack;
if (stickerPackId == null)
{
var playerCharacterId = PlayerRegistry.instance.getCharacterOwnerId(PlayState.instance.currentChart.characters.player);
var playerCharacter = PlayerRegistry.instance.fetchEntry(playerCharacterId ?? Constants.DEFAULT_CHARACTER);
if (playerCharacter != null)
{
stickerPackId = playerCharacter.getStickerPackID();
}
}
openSubState(new funkin.ui.transition.stickers.StickerSubState({targetState: targetState, stickerPack: stickerPackId}));
}
}
if (gameOverMusic != null && gameOverMusic.playing)
{
@ -291,9 +324,9 @@ class GameOverSubState extends MusicBeatSubState
}
else if (boyfriend != null)
{
if ((parentPlayState?.isMinimalMode ?? true))
if (PlayState.instance.isMinimalMode)
{
// Do nothing?
// startDeathMusic(1.0, false);
}
else
{
@ -327,11 +360,9 @@ class GameOverSubState extends MusicBeatSubState
function playDeathQuote():Void
{
if (isEnding) return;
if (boyfriend == null) return;
if (parentPlayState == null) return;
var deathQuote:Null<String> = boyfriend.getDeathQuote();
var deathQuote = boyfriend.getDeathQuote();
if (deathQuote == null) return;
if (deathQuoteSound != null)
@ -371,7 +402,7 @@ class GameOverSubState extends MusicBeatSubState
startDeathMusic(1.0, true); // isEnding changes this function's behavior.
if ((parentPlayState?.isMinimalMode ?? true) || boyfriend == null) {}
if (PlayState.instance.isMinimalMode || boyfriend == null) {}
else
{
boyfriend.playAnimation('deathConfirm' + animationSuffix, true);
@ -386,15 +417,15 @@ class GameOverSubState extends MusicBeatSubState
if (pixel) RetroCameraFade.fadeBlack(FlxG.camera, 10, 1);
else
FlxG.camera.fade(FlxColor.BLACK, 1, true, null, true);
if (parentPlayState != null) parentPlayState.needsReset = true;
PlayState.instance.needsReset = true;
if ((parentPlayState?.isMinimalMode ?? true) || boyfriend == null) {}
if (PlayState.instance.isMinimalMode || boyfriend == null) {}
else
{
// Readd Boyfriend to the stage.
boyfriend.isDead = false;
remove(boyfriend);
parentPlayState?.currentStage.addCharacter(boyfriend, BF);
PlayState.instance.currentStage.addCharacter(boyfriend, BF);
}
// Snap reset the camera which may have changed because of the player character data.
@ -409,37 +440,13 @@ class GameOverSubState extends MusicBeatSubState
RetroCameraFade.fadeToBlack(FlxG.camera, 10, 2);
new FlxTimer().start(2, _ -> {
FlxG.camera.filters = [];
#if FEATURE_MOBILE_ADVERTISEMENTS
if (AdMobUtil.PLAYING_COUNTER >= AdMobUtil.MAX_BEFORE_AD)
{
AdMobUtil.loadInterstitial(function():Void {
AdMobUtil.PLAYING_COUNTER = 0;
resetPlaying(true);
});
}
else
resetPlaying(true);
#else
resetPlaying(true);
#end
});
}
else
{
FlxG.camera.fade(FlxColor.BLACK, 2, false, function() {
#if FEATURE_MOBILE_ADVERTISEMENTS
if (AdMobUtil.PLAYING_COUNTER >= AdMobUtil.MAX_BEFORE_AD)
{
AdMobUtil.loadInterstitial(function():Void {
AdMobUtil.PLAYING_COUNTER = 0;
resetPlaying();
});
}
else
resetPlaying();
#else
resetPlaying();
#end
});
}
});
@ -482,7 +489,7 @@ class GameOverSubState extends MusicBeatSubState
public function startDeathMusic(startingVolume:Float = 1, force:Bool = false):Void
{
var musicPath:Null<String> = resolveMusicPath(musicSuffix, isStarting, isEnding);
var onComplete:Void->Void = () -> {};
var onComplete:() -> Void = () -> {};
if (isStarting)
{
@ -497,7 +504,7 @@ class GameOverSubState extends MusicBeatSubState
onComplete = function() {
isStarting = true;
// We need to force to ensure that the non-starting music plays.
startDeathMusic(0.0, true);
startDeathMusic(1.0, true);
};
}
}
@ -526,61 +533,6 @@ class GameOverSubState extends MusicBeatSubState
}
}
/**
* Pressing BACK from the Game Over screen should return the player to the Story/Freeplay menu as appropriate.
*/
public function goBack():Void
{
if (blueballed == false) return;
isEnding = true;
blueballed = false;
if (parentPlayState != null) parentPlayState.deathCounter = 0;
// PlayState.seenCutscene = false; // old thing...
if (gameOverMusic != null) gameOverMusic.stop();
// Stop death quotes immediately.
hasPlayedDeathQuote = true;
if (deathQuoteSound != null)
{
deathQuoteSound.stop();
deathQuoteSound = null;
}
if (isChartingMode)
{
this.close();
if (FlxG.sound.music != null) FlxG.sound.music.pause(); // Don't reset song position!
if (parentPlayState != null) parentPlayState.close(); // This only works because PlayState is a substate!
parentPlayState = null;
return;
}
else
{
var targetState:funkin.ui.transition.stickers.StickerSubState->FlxState = (PlayStatePlaylist.isStoryMode) ? (sticker) ->
new StoryMenuState(sticker) : (sticker) -> FreeplayState.build(sticker);
if (PlayStatePlaylist.isStoryMode)
{
PlayStatePlaylist.reset();
}
var stickerPackId:Null<String> = parentPlayState?.currentChart.stickerPack;
if (stickerPackId == null)
{
var playerCharacterId:Null<String> = PlayerRegistry.instance.getCharacterOwnerId(parentPlayState?.currentChart.characters.player);
var playerCharacter:Null<PlayableCharacter> = PlayerRegistry.instance.fetchEntry(playerCharacterId ?? Constants.DEFAULT_CHARACTER);
if (playerCharacter != null)
{
stickerPackId = playerCharacter.getStickerPackID();
}
}
openSubState(new funkin.ui.transition.stickers.StickerSubState({targetState: targetState, stickerPack: stickerPackId}));
}
}
/**
* Play the sound effect that occurs when
* boyfriend's testicles get utterly annihilated.
@ -588,7 +540,6 @@ class GameOverSubState extends MusicBeatSubState
public static function playBlueBalledSFX():Void
{
blueballed = true;
if (Assets.exists(Paths.sound('gameplay/gameover/fnf_loss_sfx' + blueBallSuffix)))
{
FunkinSound.playOnce(Paths.sound('gameplay/gameover/fnf_loss_sfx' + blueBallSuffix));

View file

@ -6,10 +6,6 @@ import funkin.graphics.FunkinSprite;
import funkin.ui.MusicBeatState;
import flixel.addons.transition.FlxTransitionableState;
import funkin.ui.mainmenu.MainMenuState;
#if mobile
import funkin.util.TouchUtil;
import funkin.util.SwipeUtil;
#end
class GitarooPause extends MusicBeatState
{
@ -36,18 +32,15 @@ class GitarooPause extends MusicBeatState
}
var bg:FunkinSprite = FunkinSprite.create('pauseAlt/pauseBG');
bg.setGraphicSize(Std.int(FlxG.width));
bg.updateHitbox();
bg.screenCenter();
add(bg);
var bf:FunkinSprite = FunkinSprite.createSparrow(0, 30, 'pauseAlt/bfLol');
bf.animation.addByPrefix('lol', "funnyThing", 13);
bf.animation.play('lol');
bf.screenCenter(X);
add(bf);
bf.screenCenter(X);
replayButton = FunkinSprite.createSparrow(FlxG.width * 0.25, FlxG.height * 0.7, 'pauseAlt/pauseUI');
replayButton = FunkinSprite.createSparrow(FlxG.width * 0.28, FlxG.height * 0.7, 'pauseAlt/pauseUI');
replayButton.animation.addByPrefix('selected', 'bluereplay', 0, false);
replayButton.animation.appendByPrefix('selected', 'yellowreplay');
replayButton.animation.play('selected');
@ -64,19 +57,11 @@ class GitarooPause extends MusicBeatState
super.create();
}
#if mobile
function checkSelectionPress():Bool
{
var buttonAcceptCheck:Bool = replaySelect ? TouchUtil.pressAction(replayButton) : TouchUtil.pressAction(cancelButton);
return buttonAcceptCheck && !SwipeUtil.swipeAny;
}
#end
override function update(elapsed:Float):Void
{
if (controls.UI_LEFT_P || controls.UI_RIGHT_P #if mobile || SwipeUtil.justSwipedLeft || SwipeUtil.justSwipedRight #end) changeThing();
if (controls.UI_LEFT_P || controls.UI_RIGHT_P) changeThing();
if (controls.ACCEPT #if mobile || checkSelectionPress() #end)
if (controls.ACCEPT)
{
if (replaySelect)
{

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