1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2025-08-31 02:45:13 +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
219 changed files with 5388 additions and 3249 deletions

10
.vscode/settings.json vendored
View file

@ -150,6 +150,16 @@
"target": "windows",
"args": ["-debug", "-DRESULTS"]
},
{
"label": "Windows / Debug (Straight to Stage Editor)",
"target": "windows",
"args": ["-debug", "-DSTAGING", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "Windows / Debug (Straight to Stage Builder)",
"target": "windows",
"args": ["-debug", "-DSTAGEBUILD", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "Windows / Debug (Straight to Animation Editor)",
"target": "windows",

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 8402339e2e63ae99c441941a46d54bf3f0c0d5fa
Subproject commit 78dc310219370144719b4eeef9b3b511c5a44532

2
assets

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

View file

@ -77,7 +77,7 @@
"name": "hscript",
"type": "git",
"dir": null,
"ref": "27c86f9a761c1d16d4433c4cf252eccb7b2e18de",
"ref": "d60bb2947fa609fdc875ccfae89666a6984eeaf2",
"url": "https://github.com/FunkinCrew/hscript"
},
{
@ -192,7 +192,7 @@
"name": "polymod",
"type": "git",
"dir": null,
"ref": "0fbdf27fe124549730accd540cec8a183f8652c0",
"ref": "3e030c81de99ca84acde681431f806d8103bcf6e",
"url": "https://github.com/larsiusprime/polymod"
},
{

File diff suppressed because it is too large Load diff

View file

@ -6,6 +6,7 @@ import flixel.math.FlxMath;
import funkin.data.song.SongData.SongTimeChange;
import funkin.data.song.SongDataUtils;
import funkin.save.Save;
import funkin.util.TimerUtil.SongSequence;
import haxe.Timer;
import flixel.sound.FlxSound;
@ -92,6 +93,12 @@ class Conductor
*/
public var songPosition(default, null):Float = 0;
/**
* The offset between frame time and music time.
* Used in `getTimeWithDelta()` to get a more accurate music time when on higher framerates.
*/
var songPositionDelta(default, null):Float = 0;
var prevTimestamp:Float = 0;
var prevTime:Float = 0;
@ -422,7 +429,8 @@ class Conductor
// If the song is playing, limit the song position to the length of the song or beginning of the song.
if (FlxG.sound.music != null && FlxG.sound.music.playing)
{
this.songPosition = Math.min(currentLength, Math.max(0, songPos));
this.songPosition = FlxMath.bound(Math.min(this.combinedOffset, 0), songPos, currentLength);
this.songPositionDelta += FlxG.elapsed * 1000 * FlxG.sound.music.pitch;
}
else
{
@ -488,10 +496,23 @@ class Conductor
// which it doesn't do every frame!
if (prevTime != this.songPosition)
{
this.songPositionDelta = 0;
// Update the timestamp for use in-between frames
prevTime = this.songPosition;
prevTimestamp = Std.int(Timer.stamp() * 1000);
}
if (this == Conductor.instance) @:privateAccess SongSequence.update.dispatch();
}
/**
* Returns a more accurate music time for higher framerates.
* @return Float
*/
public function getTimeWithDelta():Float
{
return this.songPosition + this.songPositionDelta;
}
/**

View file

@ -3,6 +3,7 @@ package funkin;
/**
* A core class which handles tracking score and combo for the current song.
*/
@:nullSafety
class Highscore
{
/**

View file

@ -27,6 +27,7 @@ import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.notes.notekind.NoteKindManager;
import funkin.play.PlayStatePlaylist;
import funkin.ui.debug.charting.ChartEditorState;
import funkin.ui.debug.stageeditor.StageEditorState;
import funkin.ui.title.TitleState;
import funkin.ui.transition.LoadingState;
import funkin.util.CLIUtil;
@ -51,6 +52,7 @@ import funkin.api.newgrounds.NewgroundsClient;
*
* It should not contain any sprites or rendering.
*/
@:nullSafety
class InitState extends FlxState
{
/**
@ -241,6 +243,9 @@ class InitState extends FlxState
#elseif CHARTING
// -DCHARTING
FlxG.switchState(() -> new funkin.ui.debug.charting.ChartEditorState());
#elseif STAGING
// -DSTAGING
FlxG.switchState(() -> new funkin.ui.debug.stageeditor.StageEditorState());
#elseif STAGEBUILD
// -DSTAGEBUILD
FlxG.switchState(() -> new funkin.ui.debug.stage.StageBuilderState());
@ -303,6 +308,13 @@ class InitState extends FlxState
fnfcTargetPath: params.chart.chartPath,
}));
}
else if (params.stage.shouldLoadStage)
{
FlxG.switchState(() -> new StageEditorState(
{
fnfsTargetPath: params.stage.stagePath,
}));
}
else
{
FlxG.sound.cache(Paths.music('freakyMenu/freakyMenu'));
@ -317,7 +329,7 @@ class InitState extends FlxState
*/
function startSong(songId:String, difficultyId:String = 'normal'):Void
{
var songData:funkin.play.song.Song = funkin.data.song.SongRegistry.instance.fetchEntry(songId);
var songData:Null<funkin.play.song.Song> = funkin.data.song.SongRegistry.instance.fetchEntry(songId);
if (songData == null)
{
@ -354,6 +366,7 @@ class InitState extends FlxState
PlayStatePlaylist.campaignId = 'weekend1';
}
@:nullSafety(Off) // Cannot unify?
LoadingState.loadPlayState(
{
targetSong: songData,
@ -368,7 +381,7 @@ class InitState extends FlxState
*/
function startLevel(levelId:String, difficultyId:String = 'normal'):Void
{
var currentLevel:funkin.ui.story.Level = funkin.data.story.level.LevelRegistry.instance.fetchEntry(levelId);
var currentLevel:Null<funkin.ui.story.Level> = funkin.data.story.level.LevelRegistry.instance.fetchEntry(levelId);
if (currentLevel == null)
{
@ -384,10 +397,19 @@ class InitState extends FlxState
PlayStatePlaylist.isStoryMode = true;
PlayStatePlaylist.campaignScore = 0;
var targetSongId:String = PlayStatePlaylist.playlistSongIds.shift();
var targetSongId:Null<String> = PlayStatePlaylist.playlistSongIds.shift();
var targetSong:funkin.play.song.Song = SongRegistry.instance.fetchEntry(targetSongId);
var targetSong:Null<funkin.play.song.Song> = null;
if (targetSongId != null) targetSong = SongRegistry.instance.fetchEntry(targetSongId);
if (targetSongId == null)
{
startGameNormally();
return;
}
@:nullSafety(Off)
LoadingState.loadPlayState(
{
targetSong: targetSong,
@ -395,6 +417,7 @@ class InitState extends FlxState
});
}
@:nullSafety(Off) // Meh, remove when flixel.system.debug.log.LogStyle is null safe
function setupFlixelDebug():Void
{
//
@ -474,17 +497,17 @@ class InitState extends FlxState
#end
}
function defineSong():String
function defineSong():Null<String>
{
return MacroUtil.getDefine('SONG');
}
function defineLevel():String
function defineLevel():Null<String>
{
return MacroUtil.getDefine('LEVEL');
}
function defineDifficulty():String
function defineDifficulty():Null<String>
{
return MacroUtil.getDefine('DIFFICULTY');
}

View file

@ -6,6 +6,7 @@ import openfl.utils.AssetType;
/**
* A core class which handles determining asset paths.
*/
@:nullSafety
class Paths
{
static var currentLevel:Null<String> = null;
@ -136,7 +137,7 @@ class Paths
* @param withExtension if it should return with the audio file extension `.mp3` or `.ogg`.
* @return String
*/
public static function inst(song:String, ?suffix:String = '', ?withExtension:Bool = true):String
public static function inst(song:String, ?suffix:String = '', withExtension:Bool = true):String
{
var ext:String = withExtension ? '.${Constants.EXT_SOUND}' : '';
return 'songs:assets/songs/${song.toLowerCase()}/Inst$suffix$ext';

View file

@ -9,12 +9,17 @@ import flixel.util.FlxSignal.FlxTypedSignal;
/**
* A core class which represents the current player(s) and their controls and other configuration.
*/
@:nullSafety
class PlayerSettings
{
// TODO: Finish implementation of second player.
public static var numPlayers(default, null) = 0;
public static var numAvatars(default, null) = 0;
// TODO: Making both of these null makes a lot of errors with the controls.
// That'd explain why unplugging input devices can cause the game to crash?
@:nullSafety(Off)
public static var player1(default, null):PlayerSettings;
@:nullSafety(Off)
public static var player2(default, null):PlayerSettings;
public static var onAvatarAdd(default, null) = new FlxTypedSignal<PlayerSettings->Void>();
@ -70,6 +75,7 @@ class PlayerSettings
/**
* Forcibly destroy the PlayerSettings singletons for each player.
*/
@:nullSafety(Off)
public static function reset():Void
{
player1 = null;

View file

@ -6,6 +6,7 @@ import funkin.util.WindowUtil;
/**
* A core class which provides a store of user-configurable, globally relevant values.
*/
@:nullSafety
class Preferences
{
/**
@ -45,7 +46,7 @@ class Preferences
static function get_naughtyness():Bool
{
return Save?.instance?.options?.naughtyness;
return Save?.instance?.options?.naughtyness ?? true;
}
static function set_naughtyness(value:Bool):Bool
@ -64,7 +65,7 @@ class Preferences
static function get_downscroll():Bool
{
return Save?.instance?.options?.downscroll;
return Save?.instance?.options?.downscroll ?? false;
}
static function set_downscroll(value:Bool):Bool
@ -102,7 +103,7 @@ class Preferences
static function get_zoomCamera():Bool
{
return Save?.instance?.options?.zoomCamera;
return Save?.instance?.options?.zoomCamera ?? true;
}
static function set_zoomCamera(value:Bool):Bool
@ -121,7 +122,7 @@ class Preferences
static function get_debugDisplay():Bool
{
return Save?.instance?.options?.debugDisplay;
return Save?.instance?.options?.debugDisplay ?? false;
}
static function set_debugDisplay(value:Bool):Bool
@ -228,7 +229,7 @@ class Preferences
static function get_unlockedFramerate():Bool
{
return Save?.instance?.options?.unlockedFramerate;
return Save?.instance?.options?.unlockedFramerate ?? false;
}
static function set_unlockedFramerate(value:Bool):Bool
@ -343,44 +344,6 @@ class Preferences
return value;
}
/**
* The game will save any screenshots taken to this format.
* @default `PNG`
*/
public static var saveFormat(get, set):Any;
static function get_saveFormat():Any
{
return Save?.instance?.options?.screenshot?.saveFormat ?? 'PNG';
}
static function set_saveFormat(value):Any
{
var save:Save = Save.instance;
save.options.screenshot.saveFormat = value;
save.flush();
return value;
}
/**
* The game will save JPEG screenshots with this quality percentage.
* @default `80`
*/
public static var jpegQuality(get, set):Int;
static function get_jpegQuality():Int
{
return Save?.instance?.options?.screenshot?.jpegQuality ?? 80;
}
static function set_jpegQuality(value:Int):Int
{
var save:Save = Save.instance;
save.options.screenshot.jpegQuality = value;
save.flush();
return value;
}
/**
* Loads the user's preferences from the save data and apply them.
*/

View file

@ -8,6 +8,7 @@ import hxdiscord_rpc.Types.DiscordRichPresence;
import hxdiscord_rpc.Types.DiscordUser;
import sys.thread.Thread;
@:nullSafety
class DiscordClient
{
static final CLIENT_ID:String = "816168432860790794";
@ -40,12 +41,12 @@ class DiscordClient
trace('[DISCORD] Initializing connection...');
// Discord.initialize(CLIENT_ID, handlers, true, null);
Discord.Initialize(CLIENT_ID, cpp.RawPointer.addressOf(handlers), 1, null);
Discord.Initialize(CLIENT_ID, cpp.RawPointer.addressOf(handlers), 1, "");
createDaemon();
}
var daemon:Thread = null;
var daemon:Null<Thread> = null;
function createDaemon():Void
{
@ -56,8 +57,6 @@ class DiscordClient
{
while (true)
{
trace('[DISCORD] Performing client update...');
#if DISCORD_DISABLE_IO_THREAD
Discord.updateConnection();
#end
@ -76,8 +75,6 @@ class DiscordClient
public function setPresence(params:DiscordClientPresenceParams):Void
{
trace('[DISCORD] Updating presence... (${params})');
Discord.updatePresence(buildPresence(params));
}
@ -92,17 +89,15 @@ class DiscordClient
presence.largeImageText = "Friday Night Funkin'";
// State should be generally what the person is doing, like "In the Menus" or "Pico (Pico Mix) [Freeplay Hard]"
presence.state = cast(params.state, Null<String>);
presence.state = cast(params.state, Null<String>) ?? "";
// Details should be what the person is specifically doing, including stuff like timestamps (maybe something like "03:24 elapsed").
presence.details = cast(params.details, Null<String>);
presence.details = cast(params.details, Null<String>) ?? "";
// The large image displaying what the user is doing.
// This should probably be album art.
// IMPORTANT NOTE: This can be an asset key uploaded to Discord's developer panel OR any URL you like.
presence.largeImageKey = cast(params.largeImageKey, Null<String>) ?? "album-volume1";
trace('[DISCORD] largeImageKey: ${presence.largeImageKey}');
// TODO: Make this use the song's album art.
// presence.largeImageKey = "icon";
// presence.largeImageKey = "https://f4.bcbits.com/img/a0746694746_16.jpg";
@ -110,7 +105,7 @@ class DiscordClient
// The small inset image for what the user is doing.
// This can be the opponent's health icon?
// NOTE: Like largeImageKey, this can be a URL, or an asset key.
presence.smallImageKey = cast(params.smallImageKey, Null<String>);
presence.smallImageKey = cast(params.smallImageKey, Null<String>) ?? "";
// NOTE: In previous versions, this showed as "Elapsed", but now shows as playtime and doesn't look good
// presence.startTimestamp = time - 10;
@ -136,9 +131,9 @@ class DiscordClient
final username:String = request[0].username;
final globalName:String = request[0].username;
final discriminator:Int = Std.parseInt(request[0].discriminator);
final discriminator:Null<Int> = Std.parseInt(request[0].discriminator);
if (discriminator != 0)
if (discriminator != null && discriminator != 0)
{
trace('[DISCORD] User: ${username}#${discriminator} (${globalName})');
}
@ -204,4 +199,17 @@ typedef DiscordClientPresenceParams =
*/
var ?smallImageKey:String;
}
class DiscordClientSandboxed
{
public static function setPresence(params:DiscordClientPresenceParams)
{
return DiscordClient.instance.setPresence(params);
}
public static function shutdown()
{
DiscordClient.instance.shutdown();
}
}
#end

View file

@ -2,10 +2,14 @@ package funkin.api.newgrounds;
#if FEATURE_NEWGROUNDS
import io.newgrounds.Call.CallError;
import io.newgrounds.components.ScoreBoardComponent;
import io.newgrounds.objects.Score;
import io.newgrounds.objects.ScoreBoard as LeaderboardData;
import io.newgrounds.objects.User;
import io.newgrounds.objects.events.Outcome;
import io.newgrounds.utils.ScoreBoardList;
@:nullSafety
class Leaderboards
{
public static function listLeaderboardData():Map<Leaderboard, LeaderboardData>
@ -16,21 +20,8 @@ class Leaderboards
trace('[NEWGROUNDS] Not logged in, cannot fetch medal data!');
return [];
}
else
{
var result:Map<Leaderboard, LeaderboardData> = [];
for (leaderboardId in leaderboardList.keys())
{
var leaderboardData = leaderboardList.get(leaderboardId);
if (leaderboardData == null) continue;
// A little hacky, but it works.
result.set(cast leaderboardId, leaderboardData);
}
return result;
}
return @:privateAccess leaderboardList._map?.copy() ?? [];
}
/**
@ -66,6 +57,41 @@ class Leaderboards
}
}
/**
* Request to receive scores from Newgrounds.
* @param leaderboard The leaderboard to fetch scores from.
* @param params Additional parameters for fetching the score.
*/
public static function requestScores(leaderboard:Leaderboard, ?params:RequestScoresParams)
{
// Silently reject retrieving scores from unknown leaderboards.
if (leaderboard == Leaderboard.Unknown) return;
var leaderboardList = NewgroundsClient.instance.leaderboards;
if (leaderboardList == null) return;
var leaderboardData:Null<LeaderboardData> = leaderboardList.get(leaderboard.getId());
if (leaderboardData == null) return;
var user:Null<User> = null;
if ((params?.useCurrentUser ?? false) && NewgroundsClient.instance.isLoggedIn()) user = NewgroundsClient.instance.user;
leaderboardData.requestScores(params?.limit ?? 10, params?.skip ?? 0, params?.period ?? ALL, params?.social ?? false, params?.tag, user,
function(outcome:Outcome<CallError>):Void {
switch (outcome)
{
case SUCCESS:
trace('[NEWGROUNDS] Fetched scores!');
if (params != null && params.onComplete != null) params.onComplete(leaderboardData.scores);
case FAIL(error):
trace('[NEWGROUNDS] Failed to fetch scores!');
trace(error);
if (params != null && params.onFail != null) params.onFail();
}
});
}
/**
* Submit a score for a Story Level to Newgrounds.
*/
@ -84,9 +110,77 @@ class Leaderboards
Leaderboards.submitScore(Leaderboard.getLeaderboardBySong(songId, difficultyId), score, tag);
}
}
/**
* Wrapper for `Leaderboards` that prevents submitting scores.
*/
@:nullSafety
class LeaderboardsSandboxed
{
public static function getLeaderboardBySong(songId:String, difficultyId:String)
{
return Leaderboard.getLeaderboardBySong(songId, difficultyId);
}
public static function getLeaderboardByLevel(levelId:String)
{
return Leaderboard.getLeaderboardByLevel(levelId);
}
public function requestScores(leaderboard:Leaderboard, params:RequestScoresParams)
{
Leaderboards.requestScores(leaderboard, params);
}
}
/**
* Additional parameters for `Leaderboards.requestScores()`
*/
typedef RequestScoresParams =
{
/**
* How many scores to include in a list.
* @default `10`
*/
var ?limit:Int;
/**
* How many scores to skip before starting the list.
* @default `0`
*/
var ?skip:Int;
/**
* The time-frame to pull the scores from.
* @default `Period.ALL`
*/
var ?period:Period;
/**
* If true, only scores by the user and their friends will be loaded. Ignored if no user is set.
* @default `false`
*/
var ?social:Bool;
/**
* An optional tag to filter the results by.
* @default `null`
*/
var ?tag:String;
/**
* If true, only the scores from the currently logged in user will be loaded.
* Additionally, if `social` is set to true, the scores of the user's friend will be loaded.
* @default `false`
*/
var ?useCurrentUser:Bool;
var ?onComplete:Array<Score>->Void;
var ?onFail:Void->Void;
}
#end
enum abstract Leaderboard(Int)
enum abstract Leaderboard(Int) from Int to Int
{
/**
* Represents an undefined or invalid leaderboard.
@ -285,7 +379,7 @@ enum abstract Leaderboard(Int)
{
case "darnell":
return DarnellBFMix;
case "litup":
case "lit-up":
return LitUpBFMix;
default:
return Unknown;
@ -379,7 +473,7 @@ enum abstract Leaderboard(Int)
return Stress;
case "darnell":
return Darnell;
case "litup":
case "lit-up":
return LitUp;
case "2hot":
return TwoHot;

View file

@ -8,6 +8,7 @@ import openfl.display.BitmapData;
import io.newgrounds.utils.MedalList;
import haxe.Json;
@:nullSafety
class Medals
{
public static var medalJSON:Array<MedalJSON> = [];
@ -21,22 +22,8 @@ class Medals
trace('[NEWGROUNDS] Not logged in, cannot fetch medal data!');
return [];
}
else
{
// TODO: Why do I have to do this, @:nullSafety is fucked up
var result:Map<Medal, MedalData> = [];
for (medalId in medalList.keys())
{
var medalData = medalList.get(medalId);
if (medalData == null) continue;
// A little hacky, but it works.
result.set(cast medalId, medalData);
}
return result;
}
return @:privateAccess medalList._map?.copy() ?? [];
}
public static function award(medal:Medal):Void
@ -131,32 +118,78 @@ class Medals
}
}
public static function awardStoryLevel(id:String):Void
public static function fetchMedalData(medal:Medal):Null<FetchedMedalData>
{
switch (id)
var medalList = NewgroundsClient.instance.medals;
@:privateAccess
if (medalList == null || medalList._map == null) return null;
var medalData:Null<MedalData> = medalList.get(medal.getId());
@:privateAccess
if (medalData == null || medalData._data == null)
{
case 'tutorial':
Medals.award(Medal.StoryTutorial);
case 'week1':
Medals.award(Medal.StoryWeek1);
case 'week2':
Medals.award(Medal.StoryWeek2);
case 'week3':
Medals.award(Medal.StoryWeek3);
case 'week4':
Medals.award(Medal.StoryWeek4);
case 'week5':
Medals.award(Medal.StoryWeek5);
case 'week6':
Medals.award(Medal.StoryWeek6);
case 'week7':
Medals.award(Medal.StoryWeek7);
case 'weekend1':
Medals.award(Medal.StoryWeekend1);
default:
trace('[NEWGROUNDS] Story level does not have a medal! (${id}).');
trace('[NEWGROUNDS] Could not retrieve data for medal: ${medal}');
return null;
}
return {
id: medalData.id,
name: medalData.name,
description: medalData.description,
icon: medalData.icon,
value: medalData.value,
difficulty: medalData.difficulty,
secret: medalData.secret,
unlocked: medalData.unlocked
}
}
public static function awardStoryLevel(id:String):Void
{
var medal:Medal = Medal.getMedalByStoryLevel(id);
if (medal == Medal.Unknown)
{
trace('[NEWGROUNDS] Story level does not have a medal! (${id}).');
return;
}
Medals.award(medal);
}
}
/**
* Wrapper for `Medals` that prevents awarding medals.
*/
class MedalsSandboxed
{
public static function fetchMedalData(medal:Medal):Null<FetchedMedalData>
{
return Medals.fetchMedalData(medal);
}
public static function getMedalByStoryLevel(id:String):Medal
{
return Medal.getMedalByStoryLevel(id);
}
public static function getAllMedals():Array<Medal>
{
return Medal.getAllMedals();
}
}
/**
* Contains data for a Medal, but excludes functions like `sendUnlock()`.
*/
typedef FetchedMedalData =
{
var id:Int;
var name:String;
var description:String;
var icon:String;
var value:Int;
var difficulty:Int;
var secret:Bool;
var unlocked:Bool;
}
#end
@ -324,6 +357,8 @@ enum abstract Medal(Int) from Int to Int
{
switch (levelId)
{
case "tutorial":
return StoryTutorial;
case "week1":
return StoryWeek1;
case "week2":
@ -344,4 +379,33 @@ enum abstract Medal(Int) from Int to Int
return Unknown;
}
}
/**
* Lists all medals aside from the `Unknown` one.
*/
public static function getAllMedals()
{
return [
StartGame,
StoryTutorial,
StoryWeek1,
StoryWeek2,
StoryWeek3,
StoryWeek4,
StoryWeek5,
StoryWeek6,
StoryWeek7,
StoryWeekend1,
CharSelect,
FreeplayPicoMix,
FreeplayStressPico,
LossRating,
PerfectRatingHard,
GoldPerfectRatingHard,
ErectDifficulty,
GoldPerfectRatingNightmare,
FridayNight,
Nice
];
}
}

View file

@ -331,4 +331,22 @@ class NewgroundsClient
return Save.instance.ngSessionId;
}
}
/**
* Wrapper for `NewgroundsClient` that prevents submitting cheated data.
*/
class NewgroundsClientSandboxed
{
public static var user(get, never):Null<User>;
static function get_user()
{
return NewgroundsClient.instance.user;
}
public static function isLoggedIn()
{
return NewgroundsClient.instance.isLoggedIn();
}
}
#end

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

@ -11,6 +11,7 @@ import openfl.utils.AssetType;
/**
* a FlxSound that just overrides loadEmbedded to allow for "streamed" sounds to load with better performance!
*/
@:nullSafety
class FlxStreamSound extends FlxSound
{
public function new()
@ -18,7 +19,7 @@ class FlxStreamSound extends FlxSound
super();
}
override public function loadEmbedded(EmbeddedSound:FlxSoundAsset, Looped:Bool = false, AutoDestroy:Bool = false, ?OnComplete:Void->Void):FlxSound
override public function loadEmbedded(EmbeddedSound:Null<FlxSoundAsset>, Looped:Bool = false, AutoDestroy:Bool = false, ?OnComplete:Void->Void):FlxSound
{
if (EmbeddedSound == null) return this;

View file

@ -551,6 +551,7 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
}
FlxTween.cancelTweensOf(this);
this._label = 'unknown';
this._waveformData = null;
}
@:access(openfl.media.Sound)

View file

@ -7,6 +7,7 @@ import flixel.tweens.FlxTween;
* A group of FunkinSounds that are all synced together.
* Unlike FlxSoundGroup, you can also control their time and pitch.
*/
@:nullSafety
class SoundGroup extends FlxTypedGroup<FunkinSound>
{
public var time(get, set):Float;
@ -36,6 +37,7 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
return result;
}
@:nullSafety(Off)
for (sndFile in files)
{
var snd:FunkinSound = FunkinSound.load(Paths.voices(song, '$sndFile'));
@ -70,7 +72,7 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
/**
* Add a sound to the group.
*/
public override function add(sound:FunkinSound):FunkinSound
public override function add(sound:FunkinSound):Null<FunkinSound>
{
var result:FunkinSound = super.add(sound);
@ -134,6 +136,7 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
/**
* Fade in all the sounds in the group.
*/
@:nullSafety(Off)
public function fadeIn(duration:Float, ?from:Float = 0.0, ?to:Float = 1.0, ?onComplete:FlxTween->Void):Void
{
forEachAlive(function(sound:FunkinSound) {
@ -144,6 +147,7 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
/**
* Fade out all the sounds in the group.
*/
@:nullSafety(Off)
public function fadeOut(duration:Float, ?to:Float = 0.0, ?onComplete:FlxTween->Void):Void
{
forEachAlive(function(sound:FunkinSound) {
@ -238,7 +242,7 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
function get_muted():Bool
{
if (getFirstAlive() != null) return getFirstAlive().muted;
if (getFirstAlive() != null) return getFirstAlive()?.muted ?? false;
else
return false;
}

View file

@ -3,10 +3,11 @@ package funkin.audio;
import flixel.group.FlxGroup.FlxTypedGroup;
import funkin.audio.waveform.WaveformData;
@:nullSafety
class VoicesGroup extends SoundGroup
{
var playerVoices:FlxTypedGroup<FunkinSound>;
var opponentVoices:FlxTypedGroup<FunkinSound>;
var playerVoices:Null<FlxTypedGroup<FunkinSound>>;
var opponentVoices:Null<FlxTypedGroup<FunkinSound>>;
/**
* Control the volume of only the sounds in the player group.
@ -41,12 +42,12 @@ class VoicesGroup extends SoundGroup
public function addPlayerVoice(sound:FunkinSound):Void
{
super.add(sound);
playerVoices.add(sound);
playerVoices?.add(sound);
}
function set_playerVolume(volume:Float):Float
{
playerVoices.forEachAlive(function(voice:FunkinSound) {
playerVoices?.forEachAlive(function(voice:FunkinSound) {
voice.volume = volume;
});
return playerVolume = volume;
@ -59,10 +60,10 @@ class VoicesGroup extends SoundGroup
snd.time = time;
});
playerVoices.forEachAlive(function(voice:FunkinSound) {
playerVoices?.forEachAlive(function(voice:FunkinSound) {
voice.time -= playerVoicesOffset;
});
opponentVoices.forEachAlive(function(voice:FunkinSound) {
opponentVoices?.forEachAlive(function(voice:FunkinSound) {
voice.time -= opponentVoicesOffset;
});
@ -71,7 +72,7 @@ class VoicesGroup extends SoundGroup
function set_playerVoicesOffset(offset:Float):Float
{
playerVoices.forEachAlive(function(voice:FunkinSound) {
playerVoices?.forEachAlive(function(voice:FunkinSound) {
voice.time += playerVoicesOffset;
voice.time -= offset;
});
@ -80,7 +81,7 @@ class VoicesGroup extends SoundGroup
function set_opponentVoicesOffset(offset:Float):Float
{
opponentVoices.forEachAlive(function(voice:FunkinSound) {
opponentVoices?.forEachAlive(function(voice:FunkinSound) {
voice.time += opponentVoicesOffset;
voice.time -= offset;
});
@ -93,12 +94,12 @@ class VoicesGroup extends SoundGroup
public function addOpponentVoice(sound:FunkinSound):Void
{
super.add(sound);
opponentVoices.add(sound);
opponentVoices?.add(sound);
}
function set_opponentVolume(volume:Float):Float
{
opponentVoices.forEachAlive(function(voice:FunkinSound) {
opponentVoices?.forEachAlive(function(voice:FunkinSound) {
voice.volume = volume;
});
return opponentVolume = volume;
@ -106,26 +107,26 @@ class VoicesGroup extends SoundGroup
public function getPlayerVoice(index:Int = 0):Null<FunkinSound>
{
return playerVoices.members[index];
return playerVoices?.members[index];
}
public function getOpponentVoice(index:Int = 0):Null<FunkinSound>
{
return opponentVoices.members[index];
return opponentVoices?.members[index];
}
public function getPlayerVoiceWaveform():Null<WaveformData>
{
if (playerVoices.members.length == 0) return null;
if (playerVoices?.members.length == 0) return null;
return playerVoices.members[0].waveformData;
return playerVoices?.members[0].waveformData;
}
public function getOpponentVoiceWaveform():Null<WaveformData>
{
if (opponentVoices.members.length == 0) return null;
if (opponentVoices?.members.length == 0) return null;
return opponentVoices.members[0].waveformData;
return opponentVoices?.members[0].waveformData;
}
/**
@ -133,9 +134,9 @@ class VoicesGroup extends SoundGroup
*/
public function getPlayerVoiceLength():Float
{
if (playerVoices.members.length == 0) return 0.0;
if (playerVoices?.members.length == 0) return 0.0;
return playerVoices.members[0].length;
return playerVoices?.members[0]?.length ?? 0.0;
}
/**
@ -143,15 +144,15 @@ class VoicesGroup extends SoundGroup
*/
public function getOpponentVoiceLength():Float
{
if (opponentVoices.members.length == 0) return 0.0;
if (opponentVoices?.members.length == 0) return 0.0;
return opponentVoices.members[0].length;
return opponentVoices?.members[0]?.length ?? 0.0;
}
public override function clear():Void
{
playerVoices.clear();
opponentVoices.clear();
playerVoices?.clear();
opponentVoices?.clear();
super.clear();
}
@ -159,13 +160,13 @@ class VoicesGroup extends SoundGroup
{
if (playerVoices != null)
{
playerVoices.destroy();
playerVoices?.destroy();
playerVoices = null;
}
if (opponentVoices != null)
{
opponentVoices.destroy();
opponentVoices?.destroy();
opponentVoices = null;
}

View file

@ -3,6 +3,7 @@ package funkin.audio.visualize;
import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
@:nullSafety
class ABot extends FlxTypedSpriteGroup<FlxSprite>
{
public function new()

View file

@ -116,7 +116,7 @@ class ABotVis extends FlxTypedSpriteGroup<FlxSprite>
for (i in 0...min(group.members.length, levels.length))
{
var animFrame:Int = Math.round(levels[i].value * 6);
var animFrame:Int = (FlxG.sound.volume == 0 || FlxG.sound.muted) ? 0 : Math.round(levels[i].value * 6);
// don't display if we're at 0 volume from the level
group.members[i].visible = animFrame > 0;

View file

@ -3,11 +3,12 @@ package funkin.audio.visualize;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.sound.FlxSound;
@:nullSafety
class PolygonVisGroup extends FlxTypedGroup<PolygonSpectogram>
{
public var playerVis:PolygonSpectogram;
public var opponentVis:PolygonSpectogram;
public var instVis:PolygonSpectogram;
public var playerVis:Null<PolygonSpectogram>;
public var opponentVis:Null<PolygonSpectogram>;
public var instVis:Null<PolygonSpectogram>;
public function new()
{
@ -99,8 +100,14 @@ class PolygonVisGroup extends FlxTypedGroup<PolygonSpectogram>
public override function destroy():Void
{
playerVis.destroy();
opponentVis.destroy();
if (playerVis != null)
{
playerVis.destroy();
}
if (opponentVis != null)
{
opponentVis.destroy();
}
super.destroy();
}
}

View file

@ -4,6 +4,7 @@ package funkin.audio.visualize.dsp;
Complex number representation.
**/
@:forward(real, imag) @:notNull @:pure
@:nullSafety
abstract Complex({
final real:Float;
final imag:Float;

View file

@ -8,6 +8,7 @@ using funkin.audio.visualize.dsp.Signal;
/**
Fast/Finite Fourier Transforms.
**/
@:nullSafety
class FFT
{
/**

View file

@ -6,6 +6,7 @@ package funkin.audio.visualize.dsp;
Usages include 1-indexed sequences or zero-centered buffers with negative indexing.
**/
@:forward(array, offset)
@:nullSafety
abstract OffsetArray<T>({
final array:Array<T>;
final offset:Int;

View file

@ -5,12 +5,13 @@ using Lambda;
/**
Signal processing miscellaneous utilities.
**/
@:nullSafety
class Signal
{
/**
Returns a smoothed version of the input array using a moving average.
**/
public static function smooth(y:Array<Float>, n:Int):Array<Float>
public static function smooth(y:Array<Float>, n:Int):Null<Array<Float>>
{
if (n <= 0)
{

View file

@ -2,6 +2,7 @@ package funkin.audio.waveform;
import funkin.util.TimerUtil;
@:nullSafety
class WaveformDataParser
{
static final INT16_MAX:Int = 32767;
@ -10,7 +11,7 @@ class WaveformDataParser
static final INT8_MAX:Int = 127;
static final INT8_MIN:Int = -128;
public static function interpretFlxSound(sound:flixel.sound.FlxSound):Null<WaveformData>
public static function interpretFlxSound(sound:Null<flixel.sound.FlxSound>):Null<WaveformData>
{
if (sound == null) return null;

View file

@ -15,6 +15,7 @@ typedef EntryConstructorFunction = String->Void;
* @param T The type to construct. Must implement `IRegistryEntry`.
* @param J The type of the JSON data used when constructing.
*/
@:nullSafety
@:generic
@:autoBuild(funkin.util.macro.DataRegistryMacro.buildRegistry())
abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructorFunction>), J>
@ -115,7 +116,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
{
try
{
var entry:T = createEntry(entryId);
var entry:Null<T> = createEntry(entryId);
if (entry != null)
{
trace(' Loaded entry data: ${entry}');
@ -165,7 +166,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
* @param id The ID of the entry.
* @return The class name, or `null` if it does not exist.
*/
public function getScriptedEntryClassName(id:String):String
public function getScriptedEntryClassName(id:String):Null<String>
{
return scriptedEntryIds.get(id);
}
@ -216,7 +217,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
public function fetchEntryVersion(id:String):Null<thx.semver.Version>
{
var entryStr:String = loadEntryFile(id).contents;
var entryVersion:thx.semver.Version = VersionUtil.getVersionFromJSON(entryStr);
var entryVersion:Null<thx.semver.Version> = VersionUtil.getVersionFromJSON(entryStr);
return entryVersion;
}

View file

@ -4,6 +4,7 @@ import json2object.Position;
import json2object.Position.Line;
import json2object.Error;
@:nullSafety
class DataError
{
public static function printError(error:Error):Void

View file

@ -19,6 +19,7 @@ import thx.semver.VersionRule;
*
* Functions must be of the signature `(hxjsonast.Json, String) -> T`, where the String is the property name and `T` is the type of the property.
*/
@:nullSafety
class DataParse
{
/**
@ -146,7 +147,6 @@ class DataParse
throw 'Expected Backdrop property $name to be specify a valid "type", but it was "${backdropType}".';
}
return null;
default:
throw 'Expected property $name to be an object, but it was ${json.value}.';
}
@ -310,6 +310,7 @@ class DataParse
var length:Null<Float> = values[2] == null ? null : Tools.getValue(values[2]);
var alt:Null<Bool> = values[3] == null ? null : Tools.getValue(values[3]);
if (time == null || data == null) throw 'Property $name note is missing time and/or data values.';
return new LegacyNote(time, data, length, alt);
// return null;
default:

View file

@ -12,6 +12,7 @@ import haxe.ds.Either;
*
* NOTE: Result must include quotation marks if the value is a string! json2object will not add them for you!
*/
@:nullSafety
class DataWrite
{
/**

View file

@ -1,8 +1,9 @@
package funkin.data.animation;
@:nullSafety
class AnimationDataUtil
{
public static function toNamed(data:UnnamedAnimationData, ?name:String = ""):AnimationData
public static function toNamed(data:UnnamedAnimationData, name:String = ""):AnimationData
{
return {
name: name,
@ -22,7 +23,7 @@ class AnimationDataUtil
* @param name (adds index to name)
* @return Array<AnimationData>
*/
public static function toNamedArray(data:Array<UnnamedAnimationData>, ?name:String = ""):Array<AnimationData>
public static function toNamedArray(data:Array<UnnamedAnimationData>, name:String = ""):Array<AnimationData>
{
return data.mapi(function(animItem, ind) return toNamed(animItem, '$name$ind'));
}

View file

@ -5,6 +5,7 @@ import funkin.play.cutscene.dialogue.ScriptedConversation;
import funkin.util.tools.ISingleton;
import funkin.data.DefaultRegistryImpl;
@:nullSafety
class ConversationRegistry extends BaseRegistry<Conversation, ConversationData> implements ISingleton implements DefaultRegistryImpl
{
/**

View file

@ -6,6 +6,7 @@ import funkin.play.cutscene.dialogue.ScriptedDialogueBox;
import funkin.util.tools.ISingleton;
import funkin.data.DefaultRegistryImpl;
@:nullSafety
class DialogueBoxRegistry extends BaseRegistry<DialogueBox, DialogueBoxData> implements ISingleton implements DefaultRegistryImpl
{
/**

View file

@ -5,6 +5,7 @@ import funkin.play.cutscene.dialogue.ScriptedSpeaker;
import funkin.util.tools.ISingleton;
import funkin.data.DefaultRegistryImpl;
@:nullSafety
class SpeakerRegistry extends BaseRegistry<Speaker, SpeakerData> implements ISingleton implements DefaultRegistryImpl
{
/**

View file

@ -8,6 +8,7 @@ import funkin.play.event.ScriptedSongEvent;
/**
* This class statically handles the parsing of internal and scripted song event handlers.
*/
@:nullSafety
class SongEventRegistry
{
/**
@ -87,14 +88,14 @@ class SongEventRegistry
return eventCache.values();
}
public static function getEvent(id:String):SongEvent
public static function getEvent(id:String):Null<SongEvent>
{
return eventCache.get(id);
}
public static function getEventSchema(id:String):SongEventSchema
public static function getEventSchema(id:String):Null<SongEventSchema>
{
var event:SongEvent = getEvent(id);
var event:Null<SongEvent> = getEvent(id);
if (event == null) return null;
return event.getEventSchema();
@ -108,7 +109,7 @@ class SongEventRegistry
public static function handleEvent(data:SongEventData):Void
{
var eventKind:String = data.eventKind;
var eventHandler:SongEvent = eventCache.get(eventKind);
var eventHandler:Null<SongEvent> = eventCache.get(eventKind);
if (eventHandler != null)
{

View file

@ -6,6 +6,7 @@ import funkin.ui.freeplay.ScriptedAlbum;
import funkin.util.tools.ISingleton;
import funkin.data.DefaultRegistryImpl;
@:nullSafety
class AlbumRegistry extends BaseRegistry<Album, AlbumData> implements ISingleton implements DefaultRegistryImpl
{
/**

View file

@ -7,6 +7,7 @@ import funkin.save.Save;
import funkin.util.tools.ISingleton;
import funkin.data.DefaultRegistryImpl;
@:nullSafety
class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData> implements ISingleton implements DefaultRegistryImpl
{
/**

View file

@ -6,6 +6,7 @@ import funkin.ui.freeplay.ScriptedFreeplayStyle;
import funkin.util.tools.ISingleton;
import funkin.data.DefaultRegistryImpl;
@:nullSafety
class FreeplayStyleRegistry extends BaseRegistry<FreeplayStyle, FreeplayStyleData> implements ISingleton implements DefaultRegistryImpl
{
/**

View file

@ -6,6 +6,7 @@ import funkin.data.notestyle.NoteStyleData;
import funkin.util.tools.ISingleton;
import funkin.data.DefaultRegistryImpl;
@:nullSafety
class NoteStyleRegistry extends BaseRegistry<NoteStyle, NoteStyleData> implements ISingleton implements DefaultRegistryImpl
{
/**
@ -24,6 +25,8 @@ class NoteStyleRegistry extends BaseRegistry<NoteStyle, NoteStyleData> implement
public function fetchDefault():NoteStyle
{
return fetchEntry(Constants.DEFAULT_NOTE_STYLE);
var notestyle:Null<NoteStyle> = fetchEntry(Constants.DEFAULT_NOTE_STYLE);
if (notestyle == null) throw 'Default notestyle was null! This should not happen!';
return notestyle;
}
}

View file

@ -68,11 +68,12 @@ class SongMetadata implements ICloneable<SongMetadata>
@:jignored
public var variation:String;
public function new(songName:String, artist:String, ?variation:String)
public function new(songName:String, artist:String, ?charter:String, ?variation:String)
{
this.version = SongRegistry.SONG_METADATA_VERSION;
this.songName = songName;
this.artist = artist;
this.charter = (charter == null) ? null : charter;
this.timeFormat = 'ms';
this.divisions = null;
this.offsets = new SongOffsets();
@ -96,7 +97,7 @@ class SongMetadata implements ICloneable<SongMetadata>
*/
public function clone():SongMetadata
{
var result:SongMetadata = new SongMetadata(this.songName, this.artist, this.variation);
var result:SongMetadata = new SongMetadata(this.songName, this.artist, this.charter, this.variation);
result.version = this.version;
result.timeFormat = this.timeFormat;
result.divisions = this.divisions;
@ -139,7 +140,7 @@ class SongMetadata implements ICloneable<SongMetadata>
*/
public function toString():String
{
return 'SongMetadata(${this.songName} by ${this.artist}, variation ${this.variation})';
return 'SongMetadata(${this.songName} by ${this.artist} and ${this.charter}, variation ${this.variation})';
}
}

View file

@ -5,13 +5,13 @@ import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongTimeChange;
import funkin.util.ClipboardUtil;
import funkin.util.SerializerUtil;
using Lambda;
/**
* Utility functions for working with song data, including note data, event data, metadata, etc.
*/
@:nullSafety
class SongDataUtils
{
/**
@ -28,7 +28,7 @@ class SongDataUtils
var time:Float = note.time + offset;
var data:Int = note.data;
var length:Float = note.length;
var kind:String = note.kind;
var kind:Null<String> = note.kind;
return new SongNoteData(time, data, length, kind);
};
@ -132,7 +132,7 @@ class SongDataUtils
* Create an array of notes whose note data is flipped (player becomes opponent and vice versa)
* Does not mutate the original array.
*/
public static function flipNotes(notes:Array<SongNoteData>, ?strumlineSize:Int = 4):Array<SongNoteData>
public static function flipNotes(notes:Array<SongNoteData>, strumlineSize:Int = 4):Array<SongNoteData>
{
return notes.map(function(note:SongNoteData):SongNoteData {
var newData = note.data;
@ -150,7 +150,7 @@ class SongDataUtils
*
* Offset the provided array of notes such that the first note is at 0 milliseconds.
*/
public static function buildNoteClipboard(notes:Array<SongNoteData>, ?timeOffset:Int = null):Array<SongNoteData>
public static function buildNoteClipboard(notes:Array<SongNoteData>, ?timeOffset:Int):Array<SongNoteData>
{
if (notes.length == 0) return notes;
if (timeOffset == null) timeOffset = Std.int(notes[0].time);
@ -162,7 +162,7 @@ class SongDataUtils
*
* Offset the provided array of events such that the first event is at 0 milliseconds.
*/
public static function buildEventClipboard(events:Array<SongEventData>, ?timeOffset:Int = null):Array<SongEventData>
public static function buildEventClipboard(events:Array<SongEventData>, ?timeOffset:Int):Array<SongEventData>
{
if (events.length == 0) return events;
if (timeOffset == null) timeOffset = Std.int(events[0].time);

View file

@ -0,0 +1,138 @@
package funkin.data.song;
using SongData.SongNoteData;
/**
* Utility class for extra handling of song notes
*/
@:nullSafety
class SongNoteDataUtils
{
static final CHUNK_INTERVAL_MS:Float = 2500;
/**
* Retrieves all stacked notes. It does this by cycling through "chunks" of notes within a certain interval.
*
* @param notes Sorted notes by time.
* @param threshold The note stack threshold. Refer to `doNotesStack` for more details.
* @param includeOverlapped (Optional) If overlapped notes should be included.
* @param overlapped (Optional) An array that gets populated with overlapped notes.
* Note that it's only guaranteed to work properly if the provided notes are sorted.
* @return Stacked notes.
*/
public static function listStackedNotes(notes:Array<SongNoteData>, threshold:Float, includeOverlapped:Bool = true,
?overlapped:Array<SongNoteData>):Array<SongNoteData>
{
var stackedNotes:Array<SongNoteData> = [];
var chunkTime:Float = 0;
var chunks:Array<Array<SongNoteData>> = [[]];
for (note in notes)
{
if (note == null)
{
continue;
}
while (note.time >= chunkTime + CHUNK_INTERVAL_MS)
{
chunkTime += CHUNK_INTERVAL_MS;
chunks.push([]);
}
chunks[chunks.length - 1].push(note);
}
for (chunk in chunks)
{
for (i in 0...(chunk.length - 1))
{
for (j in (i + 1)...chunk.length)
{
var noteI:SongNoteData = chunk[i];
var noteJ:SongNoteData = chunk[j];
if (doNotesStack(noteI, noteJ, threshold))
{
if (!stackedNotes.fastContains(noteI))
{
if (includeOverlapped) stackedNotes.push(noteI);
if (overlapped != null && !overlapped.contains(noteI)) overlapped.push(noteI);
}
if (!stackedNotes.fastContains(noteJ))
{
stackedNotes.push(noteJ);
}
}
}
}
}
return stackedNotes;
}
/**
* Concatenates two arrays of notes but overwrites notes in `lhs` that are overlapped by notes in `rhs`.
* Hold notes are only overwritten by longer hold notes.
* This operation only modifies the second array and `overwrittenNotes`.
*
* @param lhs An array of notes
* @param rhs An array of notes to concatenate into `lhs`
* @param overwrittenNotes An optional array that is modified in-place with the notes in `lhs` that were overwritten.
* @param threshold The note stack threshold. Refer to `doNotesStack` for more details.
* @return The unsorted resulting array.
*/
public static function concatOverwrite(lhs:Array<SongNoteData>, rhs:Array<SongNoteData>, ?overwrittenNotes:Array<SongNoteData>,
threshold:Float = 0):Array<SongNoteData>
{
if (lhs == null || rhs == null || rhs.length == 0) return lhs;
if (lhs.length == 0) return rhs;
var result = lhs.copy();
for (i in 0...rhs.length)
{
var noteB:SongNoteData = rhs[i];
var hasOverlap:Bool = false;
for (j in 0...lhs.length)
{
var noteA:SongNoteData = lhs[j];
if (doNotesStack(noteA, noteB, threshold))
{
// Long hold notes should have priority over shorter hold notes
if (noteA.length <= noteB.length)
{
overwrittenNotes?.push(result[j].clone());
result[j] = noteB;
}
hasOverlap = true;
break;
}
}
if (!hasOverlap) result.push(noteB);
}
return result;
}
/**
* @param noteA First note.
* @param noteB Second note.
* @param threshold The note stack threshold, in steps.
* @return Returns `true` if both notes are on the same strumline, have the same direction
* and their time difference in steps is less than the step-based threshold.
* A threshold of 0 will return `true` if notes are nearly perfectly aligned.
*/
public static function doNotesStack(noteA:SongNoteData, noteB:SongNoteData, threshold:Float = 0):Bool
{
if (noteA.data != noteB.data) return false;
else if (threshold == 0) return Math.ffloor(Math.abs(noteA.time - noteB.time)) < 1;
final stepDiff:Float = Math.abs(noteA.getStepTime() - noteB.getStepTime());
return stepDiff <= threshold + 0.001;
}
}

View file

@ -324,7 +324,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata> implements ISingleto
}
else
{
throw '[${registryId}] Chart entry ${id}:${variation} does not support migration to version ${SONG_CHART_DATA_VERSION_RULE}.';
throw '[${registryId}] Chart entry ${id}:${variation} does not support migration to version ${SONG_MUSIC_DATA_VERSION_RULE}.';
}
}
@ -337,7 +337,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata> implements ISingleto
}
else
{
throw '[${registryId}] Chart entry "$fileName" does not support migration to version ${SONG_CHART_DATA_VERSION_RULE}.';
throw '[${registryId}] Chart entry "$fileName" does not support migration to version ${SONG_MUSIC_DATA_VERSION_RULE}.';
}
}

View file

@ -9,9 +9,10 @@ import funkin.data.song.SongData.SongTimeChange;
import funkin.data.song.importer.FNFLegacyData;
import funkin.data.song.importer.FNFLegacyData.LegacyNoteSection;
@:nullSafety
class FNFLegacyImporter
{
public static function parseLegacyDataRaw(input:String, fileName:String = 'raw'):FNFLegacyData
public static function parseLegacyDataRaw(input:String, fileName:String = 'raw'):Null<FNFLegacyData>
{
var parser = new json2object.JsonParser<FNFLegacyData>();
parser.ignoreUnknownVariables = true; // Set to true to ignore extra variables that might be included in the JSON.
@ -38,16 +39,14 @@ class FNFLegacyImporter
var songMetadata:SongMetadata = new SongMetadata('Import', Constants.DEFAULT_ARTIST, 'default');
var hadError:Bool = false;
// Set generatedBy string for debugging.
songMetadata.generatedBy = 'Chart Editor Import (FNF Legacy)';
songMetadata.playData.stage = songData?.song?.stageDefault ?? 'mainStage';
songMetadata.songName = songData?.song?.song ?? 'Import';
songMetadata.playData.stage = songData.song?.stageDefault ?? 'mainStage';
songMetadata.songName = songData.song?.song ?? 'Import';
songMetadata.playData.difficulties = [];
if (songData?.song?.notes != null)
if (songData.song?.notes != null)
{
switch (songData.song.notes)
{
@ -65,7 +64,7 @@ class FNFLegacyImporter
songMetadata.timeChanges = rebuildTimeChanges(songData);
songMetadata.playData.characters = new SongCharacterData(songData?.song?.player1 ?? 'bf', 'gf', songData?.song?.player2 ?? 'dad');
songMetadata.playData.characters = new SongCharacterData(songData.song?.player1 ?? 'bf', 'gf', songData.song?.player2 ?? 'dad');
return songMetadata;
}
@ -76,7 +75,7 @@ class FNFLegacyImporter
var songChartData:SongChartData = new SongChartData([difficulty => 1.0], [], [difficulty => []]);
if (songData?.song?.notes != null)
if (songData.song?.notes != null)
{
switch (songData.song.notes)
{
@ -84,7 +83,6 @@ class FNFLegacyImporter
// One difficulty of notes.
songChartData.notes.set(difficulty, migrateNoteSections(notes));
case Right(difficulties):
var baseDifficulty = null;
if (difficulties.easy != null) songChartData.notes.set('easy', migrateNoteSections(difficulties.easy));
if (difficulties.normal != null) songChartData.notes.set('normal', migrateNoteSections(difficulties.normal));
if (difficulties.hard != null) songChartData.notes.set('hard', migrateNoteSections(difficulties.hard));
@ -124,8 +122,8 @@ class FNFLegacyImporter
noteSections = notes;
case Right(difficulties):
if (difficulties.normal != null) noteSections = difficulties.normal;
if (difficulties.hard != null) noteSections = difficulties.normal;
if (difficulties.easy != null) noteSections = difficulties.normal;
if (difficulties.hard != null) noteSections = difficulties.hard;
if (difficulties.easy != null) noteSections = difficulties.easy;
}
if (noteSections == null || noteSections.length == 0) return result;
@ -158,7 +156,7 @@ class FNFLegacyImporter
{
var result:Array<SongTimeChange> = [];
result.push(new SongTimeChange(0, songData?.song?.bpm ?? Constants.DEFAULT_BPM));
result.push(new SongTimeChange(0, songData.song?.bpm ?? Constants.DEFAULT_BPM));
var noteSections = [];
switch (songData.song.notes)
@ -168,8 +166,8 @@ class FNFLegacyImporter
noteSections = notes;
case Right(difficulties):
if (difficulties.normal != null) noteSections = difficulties.normal;
if (difficulties.hard != null) noteSections = difficulties.normal;
if (difficulties.easy != null) noteSections = difficulties.normal;
if (difficulties.hard != null) noteSections = difficulties.hard;
if (difficulties.easy != null) noteSections = difficulties.easy;
}
if (noteSections == null || noteSections.length == 0) return result;
@ -179,7 +177,7 @@ class FNFLegacyImporter
if (noteSection.changeBPM ?? false)
{
var firstNote:LegacyNote = noteSection.sectionNotes[0];
if (firstNote != null) result.push(new SongTimeChange(firstNote.time, noteSection.bpm));
if (firstNote != null) result.push(new SongTimeChange(firstNote.time, noteSection.bpm ?? Constants.DEFAULT_BPM));
}
}

View file

@ -205,7 +205,7 @@ typedef StageDataProp =
/**
* The angle of the prop, as a float.
* @default 1.0
* @default 0.0
*/
@:optional
@:default(0.0)
@ -284,7 +284,7 @@ typedef StageDataCharacter =
/**
* The angle of the character, as a float.
* @default 1.0
* @default 0.0
*/
@:optional
@:default(0.0)

View file

@ -5,6 +5,7 @@ import funkin.play.stage.ScriptedStage;
import funkin.util.tools.ISingleton;
import funkin.data.DefaultRegistryImpl;
@:nullSafety
class StageRegistry extends BaseRegistry<Stage, StageData> implements ISingleton implements DefaultRegistryImpl
{
/**

View file

@ -4,6 +4,7 @@ import funkin.data.stickers.StickerData;
import funkin.ui.transition.stickers.StickerPack;
import funkin.ui.transition.stickers.ScriptedStickerPack;
@:nullSafety
class StickerRegistry extends BaseRegistry<StickerPack, StickerData>
{
/**
@ -24,7 +25,9 @@ class StickerRegistry extends BaseRegistry<StickerPack, StickerData>
public function fetchDefault():StickerPack
{
return fetchEntry(Constants.DEFAULT_STICKER_PACK);
var stickerPack:Null<StickerPack> = fetchEntry(Constants.DEFAULT_STICKER_PACK);
if (stickerPack == null) throw 'Default sticker pack was null! This should not happen!';
return stickerPack;
}
/**

View file

@ -6,6 +6,7 @@ import funkin.ui.story.ScriptedLevel;
import funkin.util.tools.ISingleton;
import funkin.data.DefaultRegistryImpl;
@:nullSafety
class LevelRegistry extends BaseRegistry<Level, LevelData> implements ISingleton implements DefaultRegistryImpl
{
/**

View file

@ -32,6 +32,7 @@ import openfl.display._internal.CairoGraphics as GfxRenderer;
* A modified `FlxSprite` that supports filters.
* The name's pretty much self-explanatory.
*/
@:nullSafety
@:access(openfl.geom.Rectangle)
@:access(openfl.filters.BitmapFilter)
@:access(flixel.graphics.frames.FlxFrame)
@ -39,12 +40,12 @@ class FlxFilteredSprite extends FlxSprite
{
@:noCompletion var _renderer:FlxAnimateFilterRenderer = new FlxAnimateFilterRenderer();
@:noCompletion var _filterMatrix:FlxMatrix;
@:noCompletion var _filterMatrix:FlxMatrix = new FlxMatrix();
/**
* An `Array` of shader filters (aka `BitmapFilter`).
*/
public var filters(default, set):Array<BitmapFilter>;
public var filters(default, set):Null<Array<BitmapFilter>>;
/**
* a flag to update the image with the filters.
@ -52,11 +53,15 @@ class FlxFilteredSprite extends FlxSprite
*/
public var filterDirty:Bool = false;
@:noCompletion var filtered:Bool;
@:noCompletion var filtered:Bool = false;
// These appear to be a little troublesome to null safe.
@:nullSafety(Off)
@:noCompletion var _blankFrame:FlxFrame;
@:nullSafety(Off)
var _filterBmp1:BitmapData;
@:nullSafety(Off)
var _filterBmp2:BitmapData;
override public function update(elapsed:Float)
@ -162,6 +167,7 @@ class FlxFilteredSprite extends FlxSprite
}
_flashRect.width += frameWidth;
_flashRect.height += frameHeight;
@:nullSafety(Off)
if (_blankFrame == null) _blankFrame = new FlxFrame(null);
if (_blankFrame.parent == null || _flashRect.width > _blankFrame.parent.width || _flashRect.height > _blankFrame.parent.height)
@ -178,6 +184,7 @@ class FlxFilteredSprite extends FlxSprite
_filterBmp2 = new BitmapData(_blankFrame.parent.width, _blankFrame.parent.height, 0);
}
_blankFrame.offset.copyFrom(_frame.offset);
@:nullSafety(Off)
_blankFrame.parent.bitmap = _renderer.applyFilter(_blankFrame.parent.bitmap, _filterBmp1, _filterBmp2, frame.parent.bitmap, filters, _flashRect,
frame.frame.copyToFlash());
_blankFrame.frame = FlxRect.get(0, 0, _blankFrame.parent.bitmap.width, _blankFrame.parent.bitmap.height);
@ -193,7 +200,7 @@ class FlxFilteredSprite extends FlxSprite
}
@:noCompletion
function set_filters(value:Array<BitmapFilter>)
function set_filters(value:Null<Array<BitmapFilter>>)
{
if (filters != value) filterDirty = true;

View file

@ -24,6 +24,7 @@ import openfl.filters.ShaderFilter;
* - NOTE: Several other blend modes work without FunkinCamera. Some still do not work.
* - NOTE: Framerate-independent camera tweening is fixed in Flixel 6.x. Rest in peace, SwagCamera.
*/
@:nullSafety
@:access(openfl.display.DisplayObject)
@:access(openfl.display.BitmapData)
@:access(openfl.display3D.Context3D)
@ -50,11 +51,12 @@ class FunkinCamera extends FlxCamera
// Used to identify the camera during debugging.
final id:String = 'unknown';
@:nullSafety(Off)
public function new(id:String = 'unknown', x:Int = 0, y:Int = 0, width:Int = 0, height:Int = 0, zoom:Float = 0)
{
super(x, y, width, height, zoom);
this.id = id;
bgTexture = pickTexture(width, height);
bgTexture = @:nullSafety(Off) pickTexture(width, height);
bgBitmap = FixedBitmapData.fromTexture(bgTexture);
bgFrame = new FlxFrame(new FlxGraphic('', null));
bgFrame.parent.bitmap = bgBitmap;
@ -74,12 +76,15 @@ class FunkinCamera extends FlxCamera
* and the grabbed bitmap will not include any previously rendered sprites
* @return the grabbed bitmap data
*/
public function grabScreen(applyFilters:Bool, isolate:Bool = false):BitmapData
public function grabScreen(applyFilters:Bool, isolate:Bool = false):Null<BitmapData>
{
final texture = pickTexture(width, height);
final bitmap = FixedBitmapData.fromTexture(texture);
squashTo(bitmap, applyFilters, isolate);
grabbed.push(bitmap);
if (bitmap != null)
{
squashTo(bitmap, applyFilters, isolate);
grabbed.push(bitmap);
}
return bitmap;
}
@ -126,12 +131,14 @@ class FunkinCamera extends FlxCamera
if (applyFilters)
{
bitmap.draw(flashSprite, matrix);
@:nullSafety(Off) // TODO: Remove this once openfl.display.Sprite has been null safed.
flashSprite.filters = null;
filtersApplied = true;
}
else
{
final tmp = flashSprite.filters;
@:nullSafety(Off)
flashSprite.filters = null;
bitmap.draw(flashSprite, matrix);
flashSprite.filters = tmp;
@ -197,6 +204,7 @@ class FunkinCamera extends FlxCamera
final isolated = grabScreen(false, true);
// apply fullscreen blend
customBlendShader.blendSwag = blend;
@:nullSafety(Off) // I hope this doesn't cause issues
customBlendShader.sourceSwag = isolated;
customBlendShader.updateViewInfo(FlxG.width, FlxG.height, this);
applyFilter(customBlendFilter);
@ -228,7 +236,7 @@ class FunkinCamera extends FlxCamera
bgItemCount = 0;
}
function pickTexture(width:Int, height:Int):TextureBase
function pickTexture(width:Int, height:Int):Null<TextureBase>
{
// zero-sized textures will be problematic
width = width < 1 ? 1 : width;
@ -236,7 +244,9 @@ class FunkinCamera extends FlxCamera
if (texturePool.length > 0)
{
final res = texturePool.pop();
BitmapDataUtil.resizeTexture(res, width, height);
if (res != null) BitmapDataUtil.resizeTexture(res, width, height);
else
trace('huh? why is this null? $texturePool');
return res;
}
return Lib.current.stage.context3D.createTexture(width, height, BGRA, true);

View file

@ -17,6 +17,7 @@ import flixel.FlxCamera;
* - A more efficient method for creating solid color sprites.
* - TODO: Better cache handling for textures.
*/
@:nullSafety
class FunkinSprite extends FlxSprite
{
/**
@ -158,9 +159,14 @@ class FunkinSprite extends FlxSprite
* @param input The OpenFL `TextureBase` to apply
* @return This sprite, for chaining
*/
public function loadTextureBase(input:TextureBase):FunkinSprite
public function loadTextureBase(input:TextureBase):Null<FunkinSprite>
{
var inputBitmap:FixedBitmapData = FixedBitmapData.fromTexture(input);
var inputBitmap:Null<FixedBitmapData> = FixedBitmapData.fromTexture(input);
if (inputBitmap == null)
{
FlxG.log.warn('loadTextureBase - input resulted in null bitmap! $input');
return null;
}
return loadBitmapData(inputBitmap);
}
@ -219,7 +225,7 @@ class FunkinSprite extends FlxSprite
// Move the graphic from the previous cache to the current cache.
var graphic = previousCachedTextures.get(key);
previousCachedTextures.remove(key);
currentCachedTextures.set(key, graphic);
if (graphic != null) currentCachedTextures.set(key, graphic);
return;
}
@ -271,8 +277,9 @@ class FunkinSprite extends FlxSprite
static function isGraphicCached(graphic:FlxGraphic):Bool
{
var result = null;
if (graphic == null) return false;
var result = FlxG.bitmap.get(graphic.key);
result = FlxG.bitmap.get(graphic.key);
if (result == null) return false;
if (result != graphic)
{
@ -288,8 +295,9 @@ class FunkinSprite extends FlxSprite
*/
public function isAnimationDynamic(id:String):Bool
{
var animData = null;
if (this.animation == null) return false;
var animData = this.animation.getByName(id);
animData = this.animation.getByName(id);
if (animData == null) return false;
return animData.numFrames > 1;
}
@ -428,6 +436,7 @@ class FunkinSprite extends FlxSprite
public override function destroy():Void
{
@:nullSafety(Off) // TODO: Remove when flixel.FlxSprite is null safed.
frames = null;
// Cancel all tweens so they don't continue to run on a destroyed sprite.
// This prevents crashes.

View file

@ -1,8 +1,8 @@
package funkin.modding.base;
package funkin.graphics;
/**
* A script that can be tied to a FunkinSprite.
* Create a scripted class that extends FunkinSprite to use this.
*/
@:hscriptClass
class ScriptedFunkinSprite extends funkin.graphics.FunkinSprite implements HScriptedClass {}
class ScriptedFunkinSprite extends funkin.graphics.FunkinSprite implements polymod.hscript.HScriptedClass {}

View file

@ -11,6 +11,7 @@ import flxanimate.animate.FlxKeyFrame;
/**
* A sprite which provides convenience functions for rendering a texture atlas with animations.
*/
@:nullSafety
class FlxAtlasSprite extends FlxAnimate
{
static final SETTINGS:Settings =
@ -40,10 +41,11 @@ class FlxAtlasSprite extends FlxAnimate
*/
public var onAnimationLoop:FlxTypedSignal<String->Void> = new FlxTypedSignal();
var currentAnimation:String;
var currentAnimation:String = '';
var canPlayOtherAnims:Bool = true;
@:nullSafety(Off) // null safety HATES new classes atm, it'll be fixed in haxe 4.0.0?
public function new(x:Float, y:Float, ?path:String, ?settings:Settings)
{
if (settings == null) settings = SETTINGS;
@ -110,7 +112,7 @@ class FlxAtlasSprite extends FlxAnimate
var _completeAnim:Bool = false;
var fr:FlxKeyFrame = null;
var fr:Null<FlxKeyFrame> = null;
var looping:Bool = false;
@ -195,8 +197,9 @@ class FlxAtlasSprite extends FlxAnimate
fr = null;
}
var frameLabelNames = getFrameLabelNames();
// Only call goToFrameLabel if there is a frame label with that name. This prevents annoying warnings!
if (getFrameLabelNames().indexOf(id) != -1)
if (frameLabelNames != null && frameLabelNames.indexOf(id) != -1)
{
goToFrameLabel(id);
fr = anim.getFrameLabel(id);
@ -266,7 +269,7 @@ class FlxAtlasSprite extends FlxAnimate
this.anim.goToFrameLabel(label);
}
function getFrameLabelNames(?layer:haxe.extern.EitherType<Int, String>):Array<String>
function getFrameLabelNames(?layer:haxe.extern.EitherType<Int, String>):Null<Array<String>>
{
var labels = this.anim.getFrameLabels(layer);
var array = [];
@ -374,6 +377,7 @@ class FlxAtlasSprite extends FlxAnimate
var prevFrame:FlxFrame = prevFrames.get(index) ?? frames.getByIndex(index).copyTo();
prevFrames.set(index, prevFrame);
@:nullSafety(Off) // TODO: Remove this once flixel.system.frontEnds.BitmapFrontEnd has been null safed
var frame = FlxG.bitmap.add(graphic).imageFrame.frame;
frame.copyTo(frames.getByIndex(index));

View file

@ -12,6 +12,7 @@ import openfl.filters.BitmapFilter;
/**
* Provides cool stuff for `BitmapData`s that have a hardware texture internally.
*/
@:nullSafety
@:access(openfl.display.BitmapData)
@:access(openfl.display3D.textures.TextureBase)
@:access(openfl.display3D.Context3D)
@ -19,7 +20,7 @@ class BitmapDataUtil
{
static function getCache():{sprite:Sprite, bitmap:Bitmap}
{
static var cache:{sprite:Sprite, bitmap:Bitmap} = null;
static var cache:Null<{sprite:Sprite, bitmap:Bitmap}> = null;
if (cache == null)
{
final sprite = new Sprite();
@ -56,7 +57,7 @@ class BitmapDataUtil
* @param format the format if the internal texture
* @return the bitmap
*/
public static function create(width:Int, height:Int, format:Context3DTextureFormat = BGRA):FixedBitmapData
public static function create(width:Int, height:Int, format:Context3DTextureFormat = BGRA):Null<FixedBitmapData>
{
final texture = Lib.current.stage.context3D.createTexture(width, height, format, true);
return FixedBitmapData.fromTexture(texture);
@ -83,6 +84,7 @@ class BitmapDataUtil
* @param width the width
* @param height the height
*/
@:nullSafety(Off) // the final context there is causing an error, idk how to fix it
public static function resizeTexture(texture:TextureBase, width:Int, height:Int):Void
{
if (texture.__width == width && texture.__height == height) return;
@ -101,6 +103,7 @@ class BitmapDataUtil
* @param dst the destination bitmap
* @param src the source bitmap
*/
@:nullSafety(Off) // TODO: Remove this once openfl.display.Sprite has been null safed.
public static function copy(dst:BitmapData, src:BitmapData):Void
{
hardwareCheck(dst);

View file

@ -10,6 +10,7 @@ import openfl.display3D.textures.TextureBase;
/**
* `BitmapData` is kinda broken so I fixed it.
*/
@:nullSafety
@:access(openfl.display3D.textures.TextureBase)
@:access(openfl.display.OpenGLRenderer)
class FixedBitmapData extends BitmapData
@ -29,7 +30,7 @@ class FixedBitmapData extends BitmapData
* @param texture the texture
* @return the bitmap data
*/
public static function fromTexture(texture:TextureBase):FixedBitmapData
public static function fromTexture(texture:Null<TextureBase>):Null<FixedBitmapData>
{
if (texture == null) return null;
final bitmapData:FixedBitmapData = new FixedBitmapData(texture.__width, texture.__height, true, 0);

View file

@ -7,6 +7,7 @@ import flixel.util.FlxColor;
* Yoinked from AustinEast, thanks hopefully u dont mind me using some of ur good code
* instead of my dumbass ugly code bro
*/
@:nullSafety
class MeshRender extends FlxStrip
{
public var vertex_count(default, null):Int = 0;

View file

@ -2,12 +2,13 @@ package funkin.graphics.shaders;
import flixel.addons.display.FlxRuntimeShader;
@:nullSafety
class AdjustColorShader extends FlxRuntimeShader
{
public var hue(default, set):Float;
public var saturation(default, set):Float;
public var brightness(default, set):Float;
public var contrast(default, set):Float;
public var hue(default, set):Float = 0;
public var saturation(default, set):Float = 0;
public var brightness(default, set):Float = 0;
public var contrast(default, set):Float = 0;
public function new()
{

View file

@ -8,12 +8,13 @@ typedef BlendModeShader =
var uBlendColor:ShaderParameter<Float>;
}
@:nullSafety
class BlendModeEffect
{
public var shader(default, null):BlendModeShader;
@:isVar
public var color(default, set):FlxColor;
public var color(default, set):FlxColor = new FlxColor();
public function new(shader:BlendModeShader, color:FlxColor):Void
{

View file

@ -4,10 +4,11 @@ import flixel.addons.display.FlxRuntimeShader;
import openfl.display.BitmapData;
import openfl.display.ShaderInput;
@:nullSafety
class BlendModesShader extends FlxRuntimeShader
{
public var camera:ShaderInput<BitmapData>;
public var cameraData:BitmapData;
public var camera:Null<ShaderInput<BitmapData>>;
public var cameraData:Null<BitmapData>;
public function new()
{

View file

@ -5,7 +5,7 @@ import flixel.tweens.FlxTween;
class BlueFade extends FlxShader
{
public var fadeVal(default, set):Float;
public var fadeVal(default, set):Float = 1;
function set_fadeVal(val:Float):Float
{

View file

@ -5,9 +5,10 @@ import flixel.addons.display.FlxRuntimeShader;
/**
* Note... not actually gaussian!
*/
@:nullSafety
class GaussianBlurShader extends FlxRuntimeShader
{
public var amount:Float;
public var amount:Float = 1;
public function new(amount:Float = 1.0)
{

View file

@ -2,6 +2,7 @@ package funkin.graphics.shaders;
import flixel.addons.display.FlxRuntimeShader;
@:nullSafety
class Grayscale extends FlxRuntimeShader
{
public var amount:Float = 1;

View file

@ -2,11 +2,12 @@ package funkin.graphics.shaders;
import flixel.addons.display.FlxRuntimeShader;
@:nullSafety
class HSVShader extends FlxRuntimeShader
{
public var hue(default, set):Float;
public var saturation(default, set):Float;
public var value(default, set):Float;
public var hue(default, set):Float = 1;
public var saturation(default, set):Float = 1;
public var value(default, set):Float = 1;
public function new(h:Float = 1, s:Float = 1, v:Float = 1)
{

View file

@ -5,9 +5,10 @@ import flixel.addons.display.FlxRuntimeShader;
/**
* Create a little dotting effect.
*/
@:nullSafety
class InverseDotsShader extends FlxRuntimeShader
{
public var amount:Float;
public var amount:Float = 0;
public function new(amount:Float = 1.0)
{

View file

@ -3,6 +3,7 @@ package funkin.graphics.shaders;
import flixel.addons.display.FlxRuntimeShader;
import flixel.math.FlxPoint;
@:nullSafety
class MosaicEffect extends FlxRuntimeShader
{
public var blockSize:FlxPoint = FlxPoint.get(1.0, 1.0);

View file

@ -3,6 +3,7 @@ package funkin.graphics.shaders;
import flixel.addons.display.FlxRuntimeShader;
import openfl.Assets;
@:nullSafety
class PuddleShader extends FlxRuntimeShader
{
public function new()

View file

@ -2,6 +2,7 @@ package funkin.graphics.shaders;
import flixel.system.FlxAssets.FlxShader;
@:nullSafety
class WaveShader extends FlxShader
{
@:glFragmentSource('

View file

@ -18,16 +18,17 @@ enum WiggleEffectType
* 2. Call `sprite.shader = wiggleEffect` on the target sprite.
* 3. Call the update() method on the instance every frame.
*/
@:nullSafety
class WiggleEffectRuntime extends FlxRuntimeShader
{
public static function getEffectTypeId(v:WiggleEffectType):Int
public static function getEffectTypeId(v:Null<WiggleEffectType>):Int
{
return WiggleEffectType.getConstructors().indexOf(Std.string(v));
}
public var effectType(default, set):WiggleEffectType = DREAMY;
public var effectType(default, set):Null<WiggleEffectType> = DREAMY;
function set_effectType(v:WiggleEffectType):WiggleEffectType
function set_effectType(v:Null<WiggleEffectType>):Null<WiggleEffectType>
{
this.setInt('effectType', getEffectTypeId(v));
return effectType = v;

View file

@ -13,6 +13,7 @@ import openfl.net.NetStream;
* This does NOT replace hxvlc, nor does hxvlc replace this.
* hxvlc only works on desktop and does not work on HTML5!
*/
@:nullSafety
class FlxVideo extends FunkinSprite
{
var video:Video;
@ -22,14 +23,16 @@ class FlxVideo extends FunkinSprite
/**
* A callback to execute when the video finishes.
*/
public var finishCallback:Void->Void;
public var finishCallback:Null<Void->Void> = null;
@:nullSafety(Off)
public function new(videoPath:String)
{
super();
this.videoPath = videoPath;
@:nullSafety(Off) // Why do I to do this here as well for this to build?
makeGraphic(2, 2, FlxColor.TRANSPARENT);
video = new Video();
@ -72,7 +75,7 @@ class FlxVideo extends FunkinSprite
}
var videoAvailable:Bool = false;
var frameTimer:Float;
var frameTimer:Float = 0;
static final FRAME_RATE:Float = 60;

View file

@ -7,6 +7,7 @@ import hxvlc.flixel.FlxVideoSprite;
* Not to be confused with FlxVideo, this is a hxvlc based video class
* We override it simply to correct/control our volume easier.
*/
@:nullSafety
class FunkinVideoSprite extends FlxVideoSprite
{
public function new(x:Float = 0, y:Float = 0)

View file

@ -1067,9 +1067,9 @@ class Controls extends FlxActionSet
case Control.FREEPLAY_CHAR_SELECT:
return [X];
case Control.FREEPLAY_JUMP_TO_TOP:
return [];
return [RIGHT_STICK_DIGITAL_UP];
case Control.FREEPLAY_JUMP_TO_BOTTOM:
return [];
return [RIGHT_STICK_DIGITAL_DOWN];
case Control.VOLUME_UP:
[];
case Control.VOLUME_DOWN:

View file

@ -10,6 +10,7 @@ import funkin.data.notestyle.NoteStyleRegistry;
import funkin.play.notes.notekind.NoteKindManager;
import funkin.data.song.SongRegistry;
import funkin.data.freeplay.player.PlayerRegistry;
import funkin.data.freeplay.style.FreeplayStyleRegistry;
import funkin.data.stage.StageRegistry;
import funkin.data.stickers.StickerRegistry;
import funkin.data.freeplay.album.AlbumRegistry;
@ -255,9 +256,29 @@ class PolymodHandler
Polymod.addImportAlias('lime.utils.Assets', funkin.Assets);
Polymod.addImportAlias('openfl.utils.Assets', funkin.Assets);
// Backward compatibility for certain scripted classes outside `funkin.modding.base`.
Polymod.addImportAlias('funkin.modding.base.ScriptedFunkinSprite', funkin.graphics.ScriptedFunkinSprite);
Polymod.addImportAlias('funkin.modding.base.ScriptedMusicBeatState', funkin.ui.ScriptedMusicBeatState);
Polymod.addImportAlias('funkin.modding.base.ScriptedMusicBeatSubState', funkin.ui.ScriptedMusicBeatSubState);
// `funkin.util.FileUtil` has unrestricted access to the file system.
Polymod.addImportAlias('funkin.util.FileUtil', funkin.util.FileUtilSandboxed);
#if FEATURE_NEWGROUNDS
// `funkin.api.newgrounds.Leaderboards` allows for submitting cheated scores.
Polymod.addImportAlias('funkin.api.newgrounds.Leaderboards', funkin.api.newgrounds.Leaderboards.LeaderboardsSandboxed);
// `funkin.api.newgrounds.Medals` allows for unfair granting of medals.
Polymod.addImportAlias('funkin.api.newgrounds.Medals', funkin.api.newgrounds.Medals.MedalsSandboxed);
// `funkin.api.newgrounds.NewgroundsClientSandboxed` allows for submitting cheated data.
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.
// `Sys`
@ -276,9 +297,9 @@ class PolymodHandler
// Lib.load() can load malicious DLLs
Polymod.blacklistImport('cpp.Lib');
// `Unserializer`
// `haxe.Unserializer`
// Unserializer.DEFAULT_RESOLVER.resolveClass() can access blacklisted packages
Polymod.blacklistImport('Unserializer');
Polymod.blacklistImport('haxe.Unserializer');
// `lime.system.CFFI`
// Can load and execute compiled binaries.
@ -310,6 +331,7 @@ class PolymodHandler
{
if (cls == null) continue;
var className:String = Type.getClassName(cls);
if (polymod.hscript._internal.PolymodScriptClass.importOverrides.exists(className)) continue;
Polymod.blacklistImport(className);
}
@ -322,15 +344,6 @@ class PolymodHandler
Polymod.blacklistImport(className);
}
// `funkin.api.newgrounds.*`
// Contains functions which allow for cheating medals and leaderboards.
for (cls in ClassMacro.listClassesInPackage('funkin.api.newgrounds'))
{
if (cls == null) continue;
var className:String = Type.getClassName(cls);
Polymod.blacklistImport(className);
}
// `io.newgrounds.*`
// Contains functions which allow for cheating medals and leaderboards.
for (cls in ClassMacro.listClassesInPackage('io.newgrounds'))
@ -348,6 +361,16 @@ class PolymodHandler
var className:String = Type.getClassName(cls);
Polymod.blacklistImport(className);
}
// `funkin.util.macro.*`
// CompiledClassList's get function allows access to sys and Newgrounds classes
// None of the classes are suitable for mods anyway
for (cls in ClassMacro.listClassesInPackage('funkin.util.macro'))
{
if (cls == null) continue;
var className:String = Type.getClassName(cls);
Polymod.blacklistImport(className);
}
}
/**
@ -489,6 +512,7 @@ class PolymodHandler
AlbumRegistry.instance.loadEntries();
StageRegistry.instance.loadEntries();
StickerRegistry.instance.loadEntries();
FreeplayStyleRegistry.instance.loadEntries();
CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry.
NoteKindManager.loadScripts();

View file

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

View file

@ -0,0 +1,8 @@
package funkin.modding.base;
/**
* A script that can be tied to an FlxBasic.
* Create a scripted class that extends FlxBasic to use this.
*/
@:hscriptClass
class ScriptedFlxBasic extends flixel.FlxBasic implements HScriptedClass {}

View file

@ -0,0 +1,8 @@
package funkin.modding.base;
/**
* A script that can be tied to an FlxObject.
* Create a scripted class that extends FlxObject to use this.
*/
@:hscriptClass
class ScriptedFlxObject extends flixel.FlxObject implements HScriptedClass {}

View file

@ -0,0 +1,8 @@
package funkin.modding.base;
/**
* A script that can be tied to an Object (empty base class).
* Create a scripted class that extends Object to use this.
*/
@:hscriptClass
class ScriptedObject extends funkin.modding.base.Object implements HScriptedClass {}

View file

@ -255,7 +255,7 @@ class HoldNoteScriptEvent extends NoteScriptEvent
*/
public var doesNotesplash:Bool = false;
public function new(type:ScriptEventType, holdNote:SustainTrail, healthChange:Float, score:Int, isComboBreak:Bool, 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

@ -176,13 +176,13 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte
/**
* Called when the game regains focus.
* This does not get called if "Auto Pause" is disabled.
* This does not get called if "Pause on Unfocus" is disabled.
*/
public function onFocusGained(event:FocusScriptEvent) {}
/**
* Called when the game loses focus.
* This does not get called if "Auto Pause" is disabled.
* This does not get called if "Pause on Unfocus" is disabled.
*/
public function onFocusLost(event:FocusScriptEvent) {}

View file

@ -239,7 +239,7 @@ class GameOverSubState extends MusicBeatSubState
}
// Smoothly lerp the camera
FlxG.camera.zoom = MathUtil.smoothLerp(FlxG.camera.zoom, targetCameraZoom, elapsed, CAMERA_ZOOM_DURATION);
FlxG.camera.zoom = MathUtil.smoothLerpPrecision(FlxG.camera.zoom, targetCameraZoom, elapsed, CAMERA_ZOOM_DURATION);
//
// Handle user inputs.
@ -391,7 +391,6 @@ class GameOverSubState extends MusicBeatSubState
if (!isEnding)
{
isEnding = true;
startDeathMusic(1.0, true); // isEnding changes this function's behavior.
// Stop death quotes immediately.
hasPlayedDeathQuote = true;
@ -401,6 +400,8 @@ class GameOverSubState extends MusicBeatSubState
deathQuoteSound = null;
}
startDeathMusic(1.0, true); // isEnding changes this function's behavior.
if (PlayState.instance.isMinimalMode || boyfriend == null) {}
else
{

View file

@ -68,22 +68,13 @@ class PauseSubState extends MusicBeatSubState
];
/**
* Pause menu entries for when the game is paused during a video cutscene.
* Pause menu entries for when the game is paused during a cutscene.
* `[CUTSCENE]` is replaced with the name of the cutscene type.
*/
static final PAUSE_MENU_ENTRIES_VIDEO_CUTSCENE:Array<PauseMenuEntry> = [
static final PAUSE_MENU_ENTRIES_CUTSCENE:Array<PauseMenuEntry> = [
{text: 'Resume', callback: resume},
{text: 'Skip Cutscene', callback: skipVideoCutscene},
{text: 'Restart Cutscene', callback: restartVideoCutscene},
{text: 'Exit to Menu', callback: quitToMenu},
];
/**
* Pause menu entries for when the game is paused during a conversation.
*/
static final PAUSE_MENU_ENTRIES_CONVERSATION:Array<PauseMenuEntry> = [
{text: 'Resume', callback: resume},
{text: 'Skip Dialogue', callback: skipConversation},
{text: 'Restart Dialogue', callback: restartConversation},
{text: 'Skip [CUTSCENE]', callback: skipCutscene},
{text: 'Restart [CUTSCENE]', callback: restartCutscene},
{text: 'Exit to Menu', callback: quitToMenu},
];
@ -533,10 +524,15 @@ class PauseSubState extends MusicBeatSubState
// Add the back button.
currentMenuEntries = entries.concat(PAUSE_MENU_ENTRIES_DIFFICULTY.clone());
case PauseMode.Conversation:
currentMenuEntries = PAUSE_MENU_ENTRIES_CONVERSATION.clone();
case PauseMode.Cutscene:
currentMenuEntries = PAUSE_MENU_ENTRIES_VIDEO_CUTSCENE.clone();
case Cutscene(entryName, pauseName, onResume, onSkip, onRestart):
var entries:Array<PauseMenuEntry> = [];
for (entry in PAUSE_MENU_ENTRIES_CUTSCENE)
{
entries.push({text: StringTools.replace(entry.text, '[CUTSCENE]', entryName), callback: entry.callback});
}
currentMenuEntries = entries;
}
}
@ -601,10 +597,8 @@ class PauseSubState extends MusicBeatSubState
metadataDeaths.text = '${PlayState.instance?.deathCounter} Blue Balls';
case Charting:
metadataDeaths.text = 'Chart Editor Preview';
case Conversation:
metadataDeaths.text = 'Dialogue Paused';
case Cutscene:
metadataDeaths.text = 'Video Paused';
case Cutscene(entryName, pauseName, onResume, onSkip, onRestart):
metadataDeaths.text = '$pauseName Paused';
}
}
@ -618,8 +612,15 @@ class PauseSubState extends MusicBeatSubState
*/
static function resume(state:PauseSubState):Void
{
// Resume a paused video if it exists.
VideoCutscene.resumeVideo();
switch (state.currentMode)
{
case Cutscene(entryName, pauseName, onResume, onSkip, onRestart):
if (onResume != null)
{
onResume();
}
default:
}
state.close();
}
@ -681,46 +682,40 @@ class PauseSubState extends MusicBeatSubState
}
/**
* Restart the paused video cutscene, then resume the game.
* Restart the paused cutscene, then resume the game.
* @param state The current PauseSubState.
*/
static function restartVideoCutscene(state:PauseSubState):Void
static function restartCutscene(state:PauseSubState):Void
{
VideoCutscene.restartVideo();
switch (state.currentMode)
{
case Cutscene(entryName, pauseName, onResume, onSkip, onRestart):
if (onRestart != null)
{
onRestart(); // VideoCutscene.restartVideo(); on videos
}
default:
}
state.close();
}
/**
* Skip the paused video cutscene, then resume the game.
* Skip the paused cutscene, then resume the game.
* @param state The current PauseSubState.
*/
static function skipVideoCutscene(state:PauseSubState):Void
static function skipCutscene(state:PauseSubState):Void
{
VideoCutscene.finishVideo();
state.close();
}
switch (state.currentMode)
{
case Cutscene(entryName, pauseName, onResume, onSkip, onRestart):
if (onSkip != null)
{
onSkip(); // VideoCutscene.finishVideo(); on videos
}
default:
}
/**
* Restart the paused conversation, then resume the game.
* @param state The current PauseSubState.
*/
static function restartConversation(state:PauseSubState):Void
{
if (PlayState.instance?.currentConversation == null) return;
PlayState.instance.currentConversation.resetConversation();
state.close();
}
/**
* Skip the paused conversation, then resume the game.
* @param state The current PauseSubState.
*/
static function skipConversation(state:PauseSubState):Void
{
if (PlayState.instance?.currentConversation == null) return;
PlayState.instance.currentConversation.skipConversation();
state.close();
}
@ -766,11 +761,15 @@ class PauseSubState extends MusicBeatSubState
* Quit the game and return to the chart editor.
* @param state The current PauseSubState.
*/
@:access(funkin.play.PlayState)
static function quitToChartEditor(state:PauseSubState):Void
{
// This should come first because the sounds list gets cleared!
PlayState.instance?.forEachPausedSound(s -> s.destroy());
state.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!
FlxG.sound.music?.pause(); // Don't reset song position!
PlayState.instance?.vocals?.pause();
PlayState.instance?.close(); // This only works because PlayState is a substate!
}
}
@ -795,14 +794,14 @@ enum PauseMode
Difficulty;
/**
* The menu displayed when the player pauses the game during a conversation.
* The menu displayed when the player pauses the game during a cutscene.
* @param entryName The name to show in entries.
* @param pauseName The name to show on the "Cutscene Paused" text.
* @param onResume Gets called when `Resume` is selected.
* @param onSkip Gets called when `Skip [entryName]` is selected.
* @param onRestart Gets called when `Restart [entryName]` is selected.
*/
Conversation;
/**
* The menu displayed when the player pauses the game during a video cutscene.
*/
Cutscene;
Cutscene(entryName:String, pauseName:String, onResume:Void->Void, onSkip:Void->Void, onRestart:Void->Void);
}
/**

View file

@ -7,6 +7,7 @@ import flixel.FlxObject;
import flixel.FlxSubState;
import flixel.math.FlxMath;
import flixel.math.FlxPoint;
import flixel.sound.FlxSound;
import flixel.text.FlxText;
import flixel.tweens.FlxTween;
import flixel.ui.FlxBar;
@ -185,6 +186,11 @@ class PlayState extends MusicBeatSubState
*/
public var needsReset:Bool = false;
/**
* A timer that gets active once resetting happens. Used to vwoosh in notes.
*/
public var vwooshTimer:FlxTimer = new FlxTimer();
/**
* The current 'Blueball Counter' to display in the pause menu.
* Resets when you beat a song or go back to the main menu.
@ -304,13 +310,13 @@ class PlayState extends MusicBeatSubState
/**
* Whether the game is currently in Practice Mode.
* If true, player will not lose gain or lose score from notes.
* If true, player will not gain or lose score from notes.
*/
public var isPracticeMode:Bool = false;
/**
* Whether the game is currently in Bot Play Mode.
* If true, player will not lose gain or lose score from notes.
* If true, player will not gain or lose score from notes.
*/
public var isBotPlayMode:Bool = false;
@ -432,6 +438,11 @@ class PlayState extends MusicBeatSubState
*/
var cameraTweensPausedBySubState:List<FlxTween> = new List<FlxTween>();
/**
* Track any sounds we've paused for a Pause substate, so we can unpause them when we return.
*/
var soundsPausedBySubState:List<FlxSound> = new List<FlxSound>();
/**
* False until `create()` has completed.
*/
@ -807,7 +818,6 @@ class PlayState extends MusicBeatSubState
super.update(elapsed);
var list = FlxG.sound.list;
updateHealthBar();
updateScoreText();
@ -837,9 +847,9 @@ class PlayState extends MusicBeatSubState
// Reset music properly.
if (FlxG.sound.music != null)
{
FlxG.sound.music.time = startTimestamp - Conductor.instance.combinedOffset;
FlxG.sound.music.pitch = playbackRate;
FlxG.sound.music.pause();
FlxG.sound.music.time = startTimestamp;
FlxG.sound.music.pitch = playbackRate;
}
if (!overrideMusic)
@ -854,7 +864,7 @@ class PlayState extends MusicBeatSubState
}
}
vocals.pause();
vocals.time = 0 - Conductor.instance.combinedOffset;
vocals.time = startTimestamp - Conductor.instance.instrumentalOffset;
if (FlxG.sound.music != null) FlxG.sound.music.volume = 1;
vocals.volume = 1;
@ -875,9 +885,6 @@ class PlayState extends MusicBeatSubState
// Delete all notes and reset the arrays.
regenNoteData();
// so the song doesn't start too early :D
Conductor.instance.update(-5000, false);
// Reset camera zooming
cameraBopIntensity = Constants.DEFAULT_BOP_INTENSITY;
hudCameraZoomIntensity = (cameraBopIntensity - 1.0) * 2.0;
@ -886,10 +893,13 @@ class PlayState extends MusicBeatSubState
health = Constants.HEALTH_STARTING;
songScore = 0;
Highscore.tallies.combo = 0;
// so the song doesn't start too early :D
var vwooshDelay:Float = 0.5;
Conductor.instance.update(-vwooshDelay * 1000 + startTimestamp + Conductor.instance.beatLengthMs * -5);
// timer for vwoosh
var vwooshTimer = new FlxTimer();
vwooshTimer.start(0.5, function(t:FlxTimer) {
Conductor.instance.update(startTimestamp - Conductor.instance.combinedOffset, false);
vwooshTimer.start(vwooshDelay, function(_) {
if (playerStrumline.notes.length == 0) playerStrumline.updateNotes();
if (opponentStrumline.notes.length == 0) opponentStrumline.updateNotes();
playerStrumline.vwooshInNotes();
@ -897,6 +907,9 @@ class PlayState extends MusicBeatSubState
Countdown.performCountdown();
});
// Stops any existing countdown.
Countdown.stopCountdown();
// Reset the health icons.
currentStage?.getBoyfriend()?.initHealthIcon(false);
currentStage?.getDad()?.initHealthIcon(true);
@ -959,18 +972,16 @@ class PlayState extends MusicBeatSubState
// Enable drawing while the substate is open, allowing the game state to be shown behind the pause menu.
persistentDraw = true;
// There is a 1/1000 change to use a special pause menu.
// Prevent vwoosh timer from starting countdown in pause menu
vwooshTimer.active = false;
// There is a 1/1000 chance to use a special pause menu.
// This prevents the player from resuming, but that's the point.
// It's a reference to Gitaroo Man, which doesn't let you pause the game.
if (!isSubState && event.gitaroo)
{
this.remove(currentStage);
FlxG.switchState(() -> new GitarooPause(
{
targetSong: currentSong,
targetDifficulty: currentDifficulty,
targetVariation: currentVariation,
}));
FlxG.switchState(() -> new GitarooPause(lastParams));
}
else
{
@ -1127,6 +1138,8 @@ class PlayState extends MusicBeatSubState
playerStrumline.clean();
opponentStrumline.clean();
vwooshTimer.cancel();
songScore = 0;
updateScoreText();
@ -1230,9 +1243,29 @@ class PlayState extends MusicBeatSubState
musicPausedBySubState = true;
}
// Pause vocals.
// Not tracking that we've done this via a bool because vocal re-syncing involves pausing the vocals anyway.
if (vocals != null) vocals.pause();
// Pause any sounds that are playing and keep track of them.
// Vocals are also paused here but are not included as they are handled separately.
if (Std.isOfType(subState, PauseSubState))
{
FlxG.sound.list.forEachAlive(function(sound:FlxSound) {
if (!sound.active || sound == FlxG.sound.music) return;
// In case it's a scheduled sound
var funkinSound:FunkinSound = cast sound;
if (funkinSound != null && !funkinSound.isPlaying) return;
if (!sound.playing && sound.time >= 0) return;
sound.pause();
soundsPausedBySubState.add(sound);
});
vocals?.forEach(function(voice:FunkinSound) {
soundsPausedBySubState.remove(voice);
});
}
else
{
vocals?.pause();
}
}
// Pause camera tweening, and keep track of which tweens we pause.
@ -1281,6 +1314,9 @@ class PlayState extends MusicBeatSubState
if (event.eventCanceled) return;
// Resume vwooshTimer
if (!vwooshTimer.finished) vwooshTimer.active = true;
// Resume music if we paused it.
if (musicPausedBySubState)
{
@ -1288,6 +1324,8 @@ class PlayState extends MusicBeatSubState
musicPausedBySubState = false;
}
forEachPausedSound((s) -> needsReset ? s.destroy() : s.resume());
// Resume camera tweens if we paused any.
for (camTween in cameraTweensPausedBySubState)
{
@ -1423,6 +1461,10 @@ class PlayState extends MusicBeatSubState
{
performCleanup();
// `performCleanup()` clears the static reference to this state
// scripts might still need it, so we set it back to `this`
instance = this;
funkin.modding.PolymodHandler.forceReloadAssets();
lastParams.targetSong = SongRegistry.instance.fetchEntry(currentSong.id);
LoadingState.loadPlayState(lastParams);
@ -1510,13 +1552,6 @@ class PlayState extends MusicBeatSubState
}
// trace('Not bopping camera: ${FlxG.camera.zoom} < ${(1.35 * defaultCameraZoom)} && ${cameraZoomRate} > 0 && ${Conductor.instance.currentBeat} % ${cameraZoomRate} == ${Conductor.instance.currentBeat % cameraZoomRate}}');
// That combo milestones that got spoiled that one time.
// Comes with NEAT visual and audio effects.
// bruh this var is bonkers i thot it was a function lmfaooo
// Break up into individual lines to aid debugging.
if (playerStrumline != null) playerStrumline.onBeatHit();
if (opponentStrumline != null) opponentStrumline.onBeatHit();
@ -2056,9 +2091,9 @@ class PlayState extends MusicBeatSubState
FlxG.sound.music.onComplete = function() {
if (mayPauseGame) endSong(skipEndingTransition);
};
// A negative instrumental offset means the song skips the first few milliseconds of the track.
// This just gets added into the startTimestamp behavior so we don't need to do anything extra.
FlxG.sound.music.play(true, Math.max(0, startTimestamp - Conductor.instance.combinedOffset));
FlxG.sound.music.pause();
FlxG.sound.music.time = startTimestamp;
FlxG.sound.music.pitch = playbackRate;
// Prevent the volume from being wrong.
@ -2067,13 +2102,17 @@ class PlayState extends MusicBeatSubState
trace('Playing vocals...');
add(vocals);
vocals.play();
vocals.volume = 1.0;
vocals.time = startTimestamp - Conductor.instance.instrumentalOffset;
vocals.pitch = playbackRate;
vocals.time = FlxG.sound.music.time;
vocals.volume = 1.0;
// trace('STARTING SONG AT:');
// trace('${FlxG.sound.music.time}');
// trace('${vocals.time}');
resyncVocals();
FlxG.sound.music.play();
vocals.play();
#if FEATURE_DISCORD_RPC
// Updating Discord Rich Presence (with Time Left)
@ -2102,7 +2141,7 @@ class PlayState extends MusicBeatSubState
}
/**
* Resyncronize the vocal tracks if they have become offset from the instrumental.
* Resynchronize the vocal tracks if they have become offset from the instrumental.
*/
function resyncVocals():Void
{
@ -2111,8 +2150,10 @@ class PlayState extends MusicBeatSubState
// Skip this if the music is paused (GameOver, Pause menu, start-of-song offset, etc.)
if (!(FlxG.sound.music?.playing ?? false)) return;
var timeToPlayAt:Float = Math.min(FlxG.sound.music.length, Math.max(0, Conductor.instance.songPosition - Conductor.instance.combinedOffset));
var timeToPlayAt:Float = Math.min(FlxG.sound.music.length,
Math.max(Math.min(Conductor.instance.combinedOffset, 0), Conductor.instance.songPosition) - Conductor.instance.combinedOffset);
trace('Resyncing vocals to ${timeToPlayAt}');
FlxG.sound.music.pause();
vocals.pause();
@ -2347,7 +2388,7 @@ class PlayState extends MusicBeatSubState
{
// Call an event to allow canceling the note miss.
// NOTE: This is what handles the character animations!
var event:NoteScriptEvent = new NoteScriptEvent(NOTE_MISS, note, Constants.HEALTH_MISS_PENALTY, 0, true);
var event:NoteScriptEvent = new NoteScriptEvent(NOTE_MISS, note, Constants.HEALTH_MISS_PENALTY, Highscore.tallies.combo, true);
dispatchEvent(event);
// Calling event.cancelEvent() skips all the other logic! Neat!
@ -2414,7 +2455,7 @@ class PlayState extends MusicBeatSubState
var healthChange = healthChangeUncapped.clamp(healthChangeMax, 0);
var scoreChange = Std.int(Constants.SCORE_HOLD_DROP_PENALTY_PER_SECOND * remainingLengthSec);
var event:HoldNoteScriptEvent = new HoldNoteScriptEvent(NOTE_HOLD_DROP, holdNote, healthChange, scoreChange, true);
var event:HoldNoteScriptEvent = new HoldNoteScriptEvent(NOTE_HOLD_DROP, holdNote, healthChange, scoreChange, true, Highscore.tallies.combo);
dispatchEvent(event);
trace('Penalizing score by ${event.score} and health by ${event.healthChange} for dropping hold note (is combo break: ${event.isComboBreak})!');
@ -2448,7 +2489,7 @@ class PlayState extends MusicBeatSubState
}
}
// Respawns notes that were b
// Respawns notes that were between the previous time and the current time when skipping backward, or destroy notes between the previous time and the current time when skipping forward.
playerStrumline.handleSkippedNotes();
opponentStrumline.handleSkippedNotes();
}
@ -2709,6 +2750,8 @@ class PlayState extends MusicBeatSubState
FlxG.switchState(() -> new ChartEditorState(
{
targetSongId: currentSong.id,
targetSongDifficulty: currentDifficulty,
targetSongVariation: currentVariation,
}));
}
}
@ -2833,7 +2876,10 @@ class PlayState extends MusicBeatSubState
{
currentConversation.pauseMusic();
var pauseSubState:FlxSubState = new PauseSubState({mode: Conversation});
var pauseSubState:FlxSubState = new PauseSubState(
{
mode: Cutscene('Conversation', 'Conversation', null, currentConversation?.skipConversation, currentConversation?.resetConversation)
});
persistentUpdate = false;
FlxTransitionableState.skipNextTransIn = true;
@ -2849,7 +2895,10 @@ class PlayState extends MusicBeatSubState
{
VideoCutscene.pauseVideo();
var pauseSubState:FlxSubState = new PauseSubState({mode: Cutscene});
var pauseSubState:FlxSubState = new PauseSubState(
{
mode: Cutscene('Cutscene', 'Video', VideoCutscene.resumeVideo, VideoCutscene.finishVideo.bind(null), VideoCutscene.restartVideo.bind(true))
});
persistentUpdate = false;
FlxTransitionableState.skipNextTransIn = true;
@ -3154,6 +3203,9 @@ class PlayState extends MusicBeatSubState
// TODO: Uncache the song.
}
// Prevent vwoosh timer from running outside PlayState (e.g Chart Editor)
vwooshTimer.cancel();
if (overrideMusic)
{
// Stop the music. Do NOT destroy it, something still references it!
@ -3175,6 +3227,11 @@ class PlayState extends MusicBeatSubState
}
}
forEachPausedSound((s) -> s.destroy());
FlxTween.globalManager.clear();
FlxTimer.globalManager.clear();
// Remove reference to stage and remove sprites from it to save memory.
if (currentStage != null)
{
@ -3429,7 +3486,7 @@ class PlayState extends MusicBeatSubState
cancelCameraZoomTween();
}
var prevScrollTargets:Array<Dynamic> = []; // used to snap scroll speed when things go unruely
var prevScrollTargets:Array<Dynamic> = []; // used to snap scroll speed when things go unruly
/**
* The magical function that shall tween the scroll speed.
@ -3483,6 +3540,15 @@ class PlayState extends MusicBeatSubState
scrollSpeedTweens = [];
}
function forEachPausedSound(f:FlxSound->Void):Void
{
for (sound in soundsPausedBySubState)
{
f(sound);
}
soundsPausedBySubState.clear();
}
#if FEATURE_DEBUG_FUNCTIONS
/**
* Jumps forward or backward a number of sections in the song.

View file

@ -32,6 +32,7 @@ import funkin.ui.freeplay.FreeplayState;
import funkin.ui.MusicBeatSubState;
import funkin.ui.story.StoryMenuState;
import funkin.modding.base.ScriptedFlxAtlasSprite;
import funkin.graphics.ScriptedFunkinSprite;
#if FEATURE_NEWGROUNDS
import funkin.api.newgrounds.Medals;
#end
@ -258,7 +259,15 @@ class ResultState extends MusicBeatSubState
// Add to the scene.
add(animation);
case 'sparrow':
var animation:FunkinSprite = FunkinSprite.createSparrow(offsets[0], offsets[1], animPath);
@:nullSafety(Off)
var animation:FunkinSprite = null;
if (animData.scriptClass != null) animation = ScriptedFunkinSprite.init(animData.scriptClass, offsets[0], offsets[1]);
else
animation = FunkinSprite.createSparrow(offsets[0], offsets[1], animPath);
if (animation == null) continue;
animation.animation.addByPrefix('idle', '', 24, false, false, false);
if (animData.loopFrame != null)
@ -487,8 +496,7 @@ class ResultState extends MusicBeatSubState
bgFlash.visible = true;
FlxTween.tween(bgFlash, {alpha: 0}, 5 / 24);
// NOTE: Only divide if totalNotes > 0 to prevent divide-by-zero errors.
var clearPercentFloat = params.scoreData.tallies.totalNotes == 0 ? 0.0 : (params.scoreData.tallies.sick +
params.scoreData.tallies.good
var clearPercentFloat = params.scoreData.tallies.totalNotes == 0 ? 0.0 : (params.scoreData.tallies.sick + params.scoreData.tallies.good
- params.scoreData.tallies.missed) / params.scoreData.tallies.totalNotes * 100;
clearPercentTarget = Math.floor(clearPercentFloat);
// Prevent off-by-one errors.
@ -563,16 +571,6 @@ class ResultState extends MusicBeatSubState
// scorePopin.animation.play("score");
// scorePopin.visible = true;
if (params.isNewHighscore ?? false)
{
highscoreNew.visible = true;
highscoreNew.animation.play("new");
}
else
{
highscoreNew.visible = false;
}
};
}

View file

@ -13,6 +13,7 @@ import funkin.util.VersionUtil;
import haxe.Json;
import flixel.graphics.frames.FlxFrame;
@:nullSafety
class CharacterDataParser
{
/**
@ -57,7 +58,7 @@ class CharacterDataParser
{
try
{
var charData:CharacterData = parseCharacterData(charId);
var charData:Null<CharacterData> = parseCharacterData(charId);
if (charData != null)
{
trace(' Loaded character data: ${charId}');
@ -203,14 +204,14 @@ class CharacterDataParser
return null;
}
var charData:CharacterData = characterCache.get(charId);
var charScriptClass:String = characterScriptedClass.get(charId);
var charData:Null<CharacterData> = characterCache.get(charId);
var charScriptClass:Null<String> = characterScriptedClass.get(charId);
var char:BaseCharacter;
var char:Null<BaseCharacter> = null;
if (charScriptClass != null)
{
switch (charData.renderType)
if (charData != null) switch (charData.renderType)
{
case CharacterRenderType.AnimateAtlas:
char = ScriptedAnimateAtlasCharacter.init(charScriptClass, charId);
@ -227,7 +228,7 @@ class CharacterDataParser
}
else
{
switch (charData.renderType)
if (charData != null) switch (charData.renderType)
{
case CharacterRenderType.AnimateAtlas:
char = new AnimateAtlasCharacter(charId);
@ -283,7 +284,7 @@ class CharacterDataParser
/**
* Returns the idle frame of a character.
*/
public static function getCharPixelIconAsset(char:String):FlxFrame
public static function getCharPixelIconAsset(char:String):Null<FlxFrame>
{
var charPath:String = "freeplay/icons/";
@ -323,13 +324,13 @@ class CharacterDataParser
}
var isAnimated = Assets.exists(Paths.file('images/$charPath.xml'));
var frame:FlxFrame = null;
var frame:Null<FlxFrame> = null;
if (isAnimated)
{
var frames = Paths.getSparrowAtlas(charPath);
var idleFrame:FlxFrame = frames.frames.find(function(frame:FlxFrame):Bool {
var idleFrame:Null<FlxFrame> = frames.frames.find(function(frame:FlxFrame):Bool {
return frame.name.startsWith('idle');
});
@ -380,7 +381,7 @@ class CharacterDataParser
{
var rawJson:String = loadCharacterFile(charId);
var charData:CharacterData = migrateCharacterData(rawJson, charId);
var charData:Null<CharacterData> = migrateCharacterData(rawJson, charId);
return validateCharacterData(charId, charData);
}
@ -445,7 +446,7 @@ class CharacterDataParser
* @param input
* @return The validated character data
*/
static function validateCharacterData(id:String, input:CharacterData):Null<CharacterData>
static function validateCharacterData(id:String, input:Null<CharacterData>):Null<CharacterData>
{
if (input == null)
{

View file

@ -207,10 +207,10 @@ class HealthIcon extends FunkinSprite
if (bopEvery != 0)
{
lerpIconSize();
lerpIconSize(false, elapsed);
// Lerp the health icon back to its normal angle.
this.angle = MathUtil.coolLerp(this.angle, 0, 0.15);
this.angle = MathUtil.smoothLerpPrecision(this.angle, 0, elapsed, 0.511);
}
this.updatePosition();
@ -221,14 +221,14 @@ class HealthIcon extends FunkinSprite
* Mainly forced when changing to old icon to not have a weird lerp related to changing from pixel icon to non-pixel old icon
* @param force Force the icon immedialtely to be the target size. Defaults to false.
*/
function lerpIconSize(force:Bool = false):Void
function lerpIconSize(force:Bool = false, ?elapsed:Float):Void
{
// Lerp the health icon back to its normal size,
// while maintaining aspect ratio.
if (this.width > this.height)
{
// Apply linear interpolation while accounting for frame rate.
var targetSize:Int = Std.int(MathUtil.coolLerp(this.width, HEALTH_ICON_SIZE * this.size.x, 0.15));
var targetSize:Int = Std.int(MathUtil.smoothLerpPrecision(this.width, HEALTH_ICON_SIZE * this.size.x, elapsed ?? FlxG.elapsed, 0.511));
if (force) targetSize = Std.int(HEALTH_ICON_SIZE * this.size.x);
@ -236,7 +236,7 @@ class HealthIcon extends FunkinSprite
}
else
{
var targetSize:Int = Std.int(MathUtil.coolLerp(this.height, HEALTH_ICON_SIZE * this.size.y, 0.15));
var targetSize:Int = Std.int(MathUtil.smoothLerpPrecision(this.height, HEALTH_ICON_SIZE * this.size.y, elapsed ?? FlxG.elapsed, 0.511));
if (force) targetSize = Std.int(HEALTH_ICON_SIZE * this.size.y);

View file

@ -5,7 +5,6 @@ import funkin.data.IRegistryEntry;
import flixel.group.FlxSpriteGroup;
import flixel.graphics.frames.FlxFramesCollection;
import funkin.graphics.FunkinSprite;
import flixel.addons.text.FlxTypeText;
import funkin.util.assets.FlxAnimationUtil;
import funkin.modding.events.ScriptEvent;
import funkin.audio.FunkinSound;
@ -66,7 +65,7 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass imple
}
var boxSprite:FlxSprite;
var textDisplay:FlxTypeText;
var textDisplay:FunkinTypeText;
var text(default, set):String;
@ -273,7 +272,7 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass imple
function loadText():Void
{
textDisplay = new FlxTypeText(0, 0, 300, '', 32);
textDisplay = new FunkinTypeText(0, 0, 300, '', 32);
textDisplay.fieldWidth = _data.text.width;
textDisplay.setFormat(_data.text.fontFamily, _data.text.size, FlxColor.fromString(_data.text.color), LEFT, SHADOW,
FlxColor.fromString(_data.text.shadowColor ?? '#00000000'), false);

View file

@ -0,0 +1,104 @@
package funkin.play.cutscene.dialogue;
import flixel.addons.text.FlxTypeText;
import flixel.input.keyboard.FlxKey;
/**
* An FlxTypeText that better accounts for text-wrapping,
* by overriding the functions of insertBreakLines() to check the finished state.
* Also fixes a bug where empty strings would make the typing never 'finish'.
*/
class FunkinTypeText extends FlxTypeText
{
public var preWrapping:Bool = true;
public function new(X:Float, Y:Float, Width:Int, Text:String, Size:Int = 8, EmbeddedFont:Bool = true, CheckWrapping:Bool = true)
{
super(X, Y, Width, "", Size, EmbeddedFont);
_finalText = Text;
preWrapping = CheckWrapping;
}
override public function start(?Delay:Float, ForceRestart:Bool = false, AutoErase:Bool = false, ?SkipKeys:Array<FlxKey>, ?Callback:Void->Void):Void
{
if (Delay != null)
{
delay = Delay;
}
_typing = true;
_erasing = false;
paused = false;
_waiting = false;
if (ForceRestart)
{
text = "";
_length = 0;
}
autoErase = AutoErase;
if (SkipKeys != null)
{
skipKeys = SkipKeys;
}
if (Callback != null)
{
completeCallback = Callback;
}
if (useDefaultSound)
{
loadDefaultSound();
}
// Autocomplete if the text is empty anyway. Why bother?
if (_finalText.length == 0)
{
onComplete();
return;
}
if (preWrapping)
{
insertBreakLines();
}
}
override function insertBreakLines()
{
var saveText = text;
// See what it looks like when it's finished typing.
text = prefix + _finalText;
var prefixLength:Null<Int> = prefix.length;
var split:String = '';
// trace('Breaking apart text lines...');
for (i in 0...textField.numLines)
{
var curLine = textField.getLineText(i);
// trace('now at line $i, curLine: $curLine');
if (prefixLength >= curLine.length)
{
prefixLength -= curLine.length;
}
else if (prefixLength != null)
{
split += curLine.substr(prefixLength);
prefixLength = null;
}
else
{
split += '\n' + curLine;
}
// trace('now at line $i, split: $split');
}
_finalText = split;
text = saveText;
}
}

View file

@ -218,9 +218,21 @@ class FocusCameraSongEvent extends SongEvent
'Smooth Step In' => 'smoothStepIn',
'Smooth Step Out' => 'smoothStepOut',
'Smooth Step In/Out' => 'smoothStepInOut',
'Smoother Step In' => 'smootherStepIn',
'Smoother Step Out' => 'smootherStepOut',
'Smoother Step In/Out' => 'smootherStepInOut',
'Elastic In' => 'elasticIn',
'Elastic Out' => 'elasticOut',
'Elastic In/Out' => 'elasticInOut',
'Back In' => 'backIn',
'Back Out' => 'backOut',
'Back In/Out' => 'backInOut',
'Bounce In' => 'bounceIn',
'Bounce Out' => 'bounceOut',
'Bounce In/Out' => 'bounceInOut',
'Circ In' => 'circIn',
'Circ Out' => 'circOut',
'Circ In/Out' => 'circInOut',
'Instant (Ignores duration)' => 'INSTANT',
'Classic (Ignores duration)' => 'CLASSIC'
]

View file

@ -149,9 +149,21 @@ class ScrollSpeedEvent extends SongEvent
'Smooth Step In' => 'smoothStepIn',
'Smooth Step Out' => 'smoothStepOut',
'Smooth Step In/Out' => 'smoothStepInOut',
'Smoother Step In' => 'smootherStepIn',
'Smoother Step Out' => 'smootherStepOut',
'Smoother Step In/Out' => 'smootherStepInOut',
'Elastic In' => 'elasticIn',
'Elastic Out' => 'elasticOut',
'Elastic In/Out' => 'elasticInOut'
'Elastic In/Out' => 'elasticInOut',
'Back In' => 'backIn',
'Back Out' => 'backOut',
'Back In/Out' => 'backInOut',
'Bounce In' => 'bounceIn',
'Bounce Out' => 'bounceOut',
'Bounce In/Out' => 'bounceInOut',
'Circ In' => 'circIn',
'Circ Out' => 'circOut',
'Circ In/Out' => 'circInOut'
]
},
{

View file

@ -158,9 +158,21 @@ class ZoomCameraSongEvent extends SongEvent
'Smooth Step In' => 'smoothStepIn',
'Smooth Step Out' => 'smoothStepOut',
'Smooth Step In/Out' => 'smoothStepInOut',
'Smoother Step In' => 'smootherStepIn',
'Smoother Step Out' => 'smootherStepOut',
'Smoother Step In/Out' => 'smootherStepInOut',
'Elastic In' => 'elasticIn',
'Elastic Out' => 'elasticOut',
'Elastic In/Out' => 'elasticInOut'
'Elastic In/Out' => 'elasticInOut',
'Back In' => 'backIn',
'Back Out' => 'backOut',
'Back In/Out' => 'backInOut',
'Bounce In' => 'bounceIn',
'Bounce Out' => 'bounceOut',
'Bounce In/Out' => 'bounceInOut',
'Circ In' => 'circIn',
'Circ Out' => 'circOut',
'Circ In/Out' => 'circInOut'
]
}
]);

View file

@ -71,6 +71,8 @@ class NoteHoldCover extends FlxTypedSpriteGroup<FlxSprite>
this.visible = false;
holdNote.cover = null;
if (glow != null) glow.visible = false;
if (sparks != null) sparks.visible = false;
}

View file

@ -448,7 +448,6 @@ class Strumline extends FlxSpriteGroup
}
}
/**
* For a note's strumTime, calculate its Y position relative to the strumline.
* NOTE: Assumes Conductor and PlayState are both initialized.
@ -458,7 +457,7 @@ class Strumline extends FlxSpriteGroup
public function calculateNoteYPos(strumTime:Float):Float
{
return
Constants.PIXELS_PER_MS * (conductorInUse.songPosition - strumTime - Conductor.instance.inputOffset) * scrollSpeed * (Preferences.downscroll ? 1 : -1);
Constants.PIXELS_PER_MS * (conductorInUse.getTimeWithDelta() - strumTime - Conductor.instance.inputOffset) * scrollSpeed * (Preferences.downscroll ? 1 : -1);
}
public function updateNotes():Void

View file

@ -194,14 +194,14 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
includeScript:Bool = true, validScore:Bool = false):Song
{
@:privateAccess
var result:Null<Song>;
var result:Null<Song> = null;
if (includeScript && SongRegistry.instance.isScriptedEntry(songId))
{
var songClassName:String = SongRegistry.instance.getScriptedEntryClassName(songId);
var songClassName:Null<String> = SongRegistry.instance.getScriptedEntryClassName(songId);
@:privateAccess
result = SongRegistry.instance.createScriptedEntry(songClassName);
if (songClassName != null) result = SongRegistry.instance.createScriptedEntry(songClassName);
}
else
{

View file

@ -1,76 +0,0 @@
package funkin.play.song;
import funkin.data.song.SongData.SongChartData;
import funkin.data.song.SongData.SongMetadata;
import funkin.util.FileUtil;
import openfl.net.FileReference;
/**
* TODO: Refactor and remove this.
*/
class SongSerializer
{
/**
* Access a SongChartData JSON file from a specific path, then load it.
* @param path The file path to read from.
*/
public static function importSongChartDataSync(path:String):SongChartData
{
var fileData = FileUtil.readStringFromPath(path);
if (fileData == null) return null;
var songChartData:SongChartData = fileData.parseJSON();
return songChartData;
}
/**
* Access a SongMetadata JSON file from a specific path, then load it.
* @param path The file path to read from.
*/
public static function importSongMetadataSync(path:String):SongMetadata
{
var fileData = FileUtil.readStringFromPath(path);
if (fileData == null) return null;
var songMetadata:SongMetadata = fileData.parseJSON();
return songMetadata;
}
/**
* Prompt the user to browse for a SongChartData JSON file path, then load it.
* @param callback The function to call when the file is loaded.
*/
public static function importSongChartDataAsync(callback:SongChartData->Void):Void
{
FileUtil.browseFileReference(function(fileReference:FileReference) {
var data = fileReference.data.toString();
if (data == null) return;
var songChartData:SongChartData = data.parseJSON();
if (songChartData != null) callback(songChartData);
});
}
/**
* Prompt the user to browse for a SongMetadata JSON file path, then load it.
* @param callback The function to call when the file is loaded.
*/
public static function importSongMetadataAsync(callback:SongMetadata->Void):Void
{
FileUtil.browseFileReference(function(fileReference:FileReference) {
var data = fileReference.data.toString();
if (data == null) return;
var songMetadata:SongMetadata = data.parseJSON();
if (songMetadata != null) callback(songMetadata);
});
}
}

View file

@ -248,8 +248,8 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
propSprite.scrollFactor.y = dataProp.scroll[1];
propSprite.angle = dataProp.angle;
propSprite.color = FlxColor.fromString(dataProp.color);
@:privateAccess if (!isSolidColor) propSprite.blend = BlendMode.fromString(dataProp.blend);
if (!isSolidColor) propSprite.color = FlxColor.fromString(dataProp.color);
@:privateAccess propSprite.blend = BlendMode.fromString(dataProp.blend);
propSprite.zIndex = dataProp.zIndex;

View file

@ -2,6 +2,7 @@ package funkin.save;
import flixel.util.FlxSave;
import funkin.input.Controls.Device;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.scoring.Scoring;
import funkin.play.scoring.Scoring.ScoringRank;
import funkin.save.migrator.RawSaveData_v1_0_0;
@ -127,8 +128,6 @@ class Save
shouldHideMouse: true,
fancyPreview: true,
previewOnSave: true,
saveFormat: 'PNG',
jpegQuality: 80,
},
controls:
@ -173,6 +172,10 @@ class Save
metronomeVolume: 1.0,
hitsoundVolumePlayer: 1.0,
hitsoundVolumeOpponent: 1.0,
instVolume: 1.0,
playerVoiceVolume: 1.0,
opponentVoiceVolume: 1.0,
playbackSpeed: 0.5,
themeMusic: true
},
@ -181,7 +184,10 @@ class Save
previousFiles: [],
moveStep: "1px",
angleStep: 5,
theme: StageEditorTheme.Light
theme: StageEditorTheme.Light,
bfChar: "bf",
gfChar: "gf",
dadChar: "dad"
}
};
}
@ -407,6 +413,57 @@ class Save
return data.optionsChartEditor.hitsoundVolumeOpponent;
}
public var chartEditorInstVolume(get, set):Float;
function get_chartEditorInstVolume():Float
{
if (data.optionsChartEditor.instVolume == null) data.optionsChartEditor.instVolume = 1.0;
return data.optionsChartEditor.instVolume;
}
function set_chartEditorInstVolume(value:Float):Float
{
// Set and apply.
data.optionsChartEditor.instVolume = value;
flush();
return data.optionsChartEditor.instVolume;
}
public var chartEditorPlayerVoiceVolume(get, set):Float;
function get_chartEditorPlayerVoiceVolume():Float
{
if (data.optionsChartEditor.playerVoiceVolume == null) data.optionsChartEditor.playerVoiceVolume = 1.0;
return data.optionsChartEditor.playerVoiceVolume;
}
function set_chartEditorPlayerVoiceVolume(value:Float):Float
{
// Set and apply.
data.optionsChartEditor.playerVoiceVolume = value;
flush();
return data.optionsChartEditor.playerVoiceVolume;
}
public var chartEditorOpponentVoiceVolume(get, set):Float;
function get_chartEditorOpponentVoiceVolume():Float
{
if (data.optionsChartEditor.opponentVoiceVolume == null) data.optionsChartEditor.opponentVoiceVolume = 1.0;
return data.optionsChartEditor.opponentVoiceVolume;
}
function set_chartEditorOpponentVoiceVolume(value:Float):Float
{
// Set and apply.
data.optionsChartEditor.opponentVoiceVolume = value;
flush();
return data.optionsChartEditor.opponentVoiceVolume;
}
public var chartEditorThemeMusic(get, set):Bool;
function get_chartEditorThemeMusic():Bool
@ -428,7 +485,7 @@ class Save
function get_chartEditorPlaybackSpeed():Float
{
if (data.optionsChartEditor.playbackSpeed == null) data.optionsChartEditor.playbackSpeed = 1.0;
if (data.optionsChartEditor.playbackSpeed == null) data.optionsChartEditor.playbackSpeed = 0.5;
return data.optionsChartEditor.playbackSpeed;
}
@ -550,6 +607,60 @@ class Save
return data.optionsStageEditor.theme;
}
public var stageBoyfriendChar(get, set):String;
function get_stageBoyfriendChar():String
{
if (data.optionsStageEditor.bfChar == null
|| CharacterDataParser.fetchCharacterData(data.optionsStageEditor.bfChar) == null) data.optionsStageEditor.bfChar = "bf";
return data.optionsStageEditor.bfChar;
}
function set_stageBoyfriendChar(value:String):String
{
// Set and apply.
data.optionsStageEditor.bfChar = value;
flush();
return data.optionsStageEditor.bfChar;
}
public var stageGirlfriendChar(get, set):String;
function get_stageGirlfriendChar():String
{
if (data.optionsStageEditor.gfChar == null
|| CharacterDataParser.fetchCharacterData(data.optionsStageEditor.gfChar ?? "") == null) data.optionsStageEditor.gfChar = "gf";
return data.optionsStageEditor.gfChar;
}
function set_stageGirlfriendChar(value:String):String
{
// Set and apply.
data.optionsStageEditor.gfChar = value;
flush();
return data.optionsStageEditor.gfChar;
}
public var stageDadChar(get, set):String;
function get_stageDadChar():String
{
if (data.optionsStageEditor.dadChar == null
|| CharacterDataParser.fetchCharacterData(data.optionsStageEditor.dadChar ?? "") == null) data.optionsStageEditor.dadChar = "dad";
return data.optionsStageEditor.dadChar;
}
function set_stageDadChar(value:String):String
{
// Set and apply.
data.optionsStageEditor.dadChar = value;
flush();
return data.optionsStageEditor.dadChar;
}
/**
* When we've seen a character unlock, add it to the list of characters seen.
* @param character
@ -1441,16 +1552,12 @@ typedef SaveDataOptions =
* @param shouldHideMouse Should the mouse be hidden when taking a screenshot? Default: `true`
* @param fancyPreview Show a fancy preview? Default: `true`
* @param previewOnSave Only show the fancy preview after a screenshot is saved? Default: `true`
* @param saveFormat The save format of the screenshot, PNG or JPEG. Default: `PNG`
* @param jpegQuality The JPEG Quality, if we're saving to the format. Default: `80`
*/
var screenshot:
{
var shouldHideMouse:Bool;
var fancyPreview:Bool;
var previewOnSave:Bool;
var saveFormat:String;
var jpegQuality:Int;
};
var controls:
@ -1653,10 +1760,16 @@ typedef SaveDataChartEditorOptions =
var ?instVolume:Float;
/**
* Voices volume in the Chart Editor.
* Player voice volume in the Chart Editor.
* @default `1.0`
*/
var ?voicesVolume:Float;
var ?playerVoiceVolume:Float;
/**
* Opponent voice volume in the Chart Editor.
* @default `1.0`
*/
var ?opponentVoiceVolume:Float;
/**
* Playback speed in the Chart Editor.
@ -1699,4 +1812,22 @@ typedef SaveDataStageEditorOptions =
* @default `StageEditorTheme.Light`
*/
var ?theme:StageEditorTheme;
/**
* The BF character ID used in testing stages.
* @default bf
*/
var ?bfChar:String;
/**
* The GF character ID used in testing stages.
* @default gf
*/
var ?gfChar:String;
/**
* The Dad character ID used in testing stages.
* @default dad
*/
var ?dadChar:String;
};

View file

@ -8,9 +8,10 @@ typedef AtlasAsset = flixel.util.typeLimit.OneOfTwo<String, FlxAtlasFrames>;
/**
* A menulist whose items share a single texture atlas.
*/
@:nullSafety
class AtlasMenuList extends MenuTypedList<AtlasMenuItem>
{
public var atlas:FlxAtlasFrames;
public var atlas:Null<FlxAtlasFrames>;
public function new(atlas, navControls:NavControls = Vertical, ?wrapMode)
{
@ -38,9 +39,10 @@ class AtlasMenuList extends MenuTypedList<AtlasMenuItem>
/**
* A menu list item which uses single texture atlas.
*/
@:nullSafety
class AtlasMenuItem extends MenuListItem
{
var atlas:FlxAtlasFrames;
var atlas:Null<FlxAtlasFrames>;
public var centered:Bool = false;
@ -52,7 +54,7 @@ class AtlasMenuItem extends MenuListItem
override function setData(name:String, ?callback:Void->Void)
{
frames = atlas;
if (atlas != null) frames = atlas;
animation.addByPrefix('idle', '$name idle', 24);
animation.addByPrefix('selected', '$name selected', 24);

View file

@ -9,6 +9,7 @@ import flixel.util.FlxStringUtil;
* AtlasText is an improved version of Alphabet and FlxBitmapText.
* It supports animations on the letters, and is less buggy than Alphabet.
*/
@:nullSafety
class AtlasText extends FlxTypedSpriteGroup<AtlasChar>
{
static var fonts = new Map<AtlasFont, AtlasFontData>();
@ -16,7 +17,7 @@ class AtlasText extends FlxTypedSpriteGroup<AtlasChar>
public var text(default, set):String = "";
var font:AtlasFontData;
var font:AtlasFontData = new AtlasFontData(AtlasFont.DEFAULT);
public var atlas(get, never):FlxAtlasFrames;
@ -33,10 +34,10 @@ class AtlasText extends FlxTypedSpriteGroup<AtlasChar>
inline function get_maxHeight()
return font.maxHeight;
public function new(x = 0.0, y = 0.0, text:String, fontName:AtlasFont = AtlasFont.DEFAULT)
public function new(x = 0.0, y = 0.0, text:String = "", fontName:AtlasFont = AtlasFont.DEFAULT)
{
if (!fonts.exists(fontName)) fonts[fontName] = new AtlasFontData(fontName);
font = fonts[fontName];
font = fonts[fontName] ?? new AtlasFontData(fontName);
super(x, y);
@ -251,6 +252,7 @@ class AtlasChar extends FlxSprite
}
}
@:nullSafety
private class AtlasFontData
{
static public var upperChar = ~/^[A-Z]\d+$/;

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