Compare commits

...
708 Commits
Author SHA1 Message Date
Hundrec 188870dbe6 Clean assets submodule
Build and Upload nightly game builds / gather-changes (push) Canceled after 0s
Build and Upload nightly game builds / gather-tags (push) Canceled after 0s
Build and Upload nightly game builds / build-game-on-host (macos, macos) (push) Canceled after 0s
Build and Upload nightly game builds / build-game-on-host (windows, windows) (push) Canceled after 0s
Build and Upload nightly game builds / docker-image (push) Canceled after 0s
Build and Upload nightly game builds / build-game-in-container (html5) (push) Canceled after 0s
Build and Upload nightly game builds / build-game-in-container (linux) (push) Canceled after 0s
2026-01-10 00:42:34 -07:00
Hundrec e2c90dced3 Bump docs and template versions to 0.8.1 2026-01-10 00:42:34 -07:00
Hundrec 9fe64c0ec3 Changelog for 0.8.1 and version bump 2026-01-04 03:33:27 -05:00
EliteMasterEricandHundrec 75873d2b92 "scoreable = false" should stop hold notes from GRANTING points too. 2026-01-04 03:15:31 -05:00
EliteMasterEricandHundrec cd2cc9b6a2 Fix "scoreable = false" not working on hold notes. 2026-01-04 03:15:31 -05:00
EliteMasterEricandHundrec 3bf345247e Play the note animation on hit for Non-scoreable notes. 2026-01-04 03:15:31 -05:00
ComedyLostandHundrec 16f3e038d8 Change the script event order in PlayState 2026-01-04 03:15:31 -05:00
2cd7be00af Non scoreable notes for Spaghetti
Co-Authored-By: Eric <4635334+EliteMasterEric@users.noreply.github.com>
2026-01-04 03:15:31 -05:00
Hundrec 92df70c864 Changelog for 0.8.0 - LE SSERAFIM Collab Update 2025-12-30 11:47:49 -05:00
Karim AkraandHundrec 10816add64 Fix sserafim special backing card text cuttinf off on wide res 2025-12-30 07:42:54 -05:00
MAJigsaw77andKarim Akra cd58eebf96 Fix some weird crash on Android Webview from firelab. 2025-12-30 13:43:50 +02:00
Cameron Taylor b04ad86537 assets submod: fabs freeplay scroll work 2025-12-30 02:10:00 -05:00
fabsandCameron Taylor 6d2caca39c allow BGScrollingText text to be edited 2025-12-30 02:09:04 -05:00
EliteMasterEricandHundrec 9472fac99d Add release leaderboard and medal ID 2025-12-29 22:34:08 -05:00
sector-aandCameron Taylor 0c5b5900a5 Prevent updateSelected from being repeatedly called 2025-12-29 20:55:21 -05:00
EliteMasterEricandKade c63935a34a Update polymod to latest 2025-12-29 09:50:58 -08:00
EliteMasterEricandHundrec f4f94eafce Leaderboard fix (plus additional logging cleanup) 2025-12-29 11:07:58 -05:00
EliteMasterEricandHundrec c546e8a709 Implement Eat It Up medal. 2025-12-29 11:07:58 -05:00
MoonDroidandKarim Akra bd533da585 Add assets fixes for Mobile's Spaghetti 2025-12-29 15:06:56 +02:00
sector-aandKarim Akra 609d56613a Add haptics to spaghetti 2025-12-29 14:52:02 +02:00
Karim AkraandKarim Akra 3675df76f5 Exclude sakra spritemap from astc due to rendering artifacts 2025-12-29 14:47:20 +02:00
MoonDroidandKarim Akra 26dd6eac3f Update astc exclusions for new LE-SSERAFIM changes 2025-12-29 14:47:20 +02:00
AbnormalPoofandHundrec eb5eb6b1eb Fix the health icon showing up briefly on restart + Fix BF and GF repeating their anims on restart 2025-12-29 06:56:49 -05:00
AbnormalPoofandHundrec bc79de3c4d Add the Rift of the Necrodancer collab trailer to Attract Mode 2025-12-29 05:47:41 -05:00
Hundrec 2a57c57c11 Change NEW song markings in scripts 2025-12-29 04:13:55 -05:00
AbnormalPoofandHundrec 9e0b51efc6 Don't play the intro cutscene while playtesting in the Chart Editor 2025-12-29 04:13:55 -05:00
AbnormalPoofandHundrec 43abb25114 Remove some unused reference images and rename the stage to Diner 2025-12-29 04:13:55 -05:00
AbnormalPoofandHundrec 8feac720ac Reapply the shader to BF on song restart 2025-12-29 04:13:55 -05:00
AbnormalPoofandHundrec 8ac9209c3b Implement the game over animation for BF in Spaghetti 2025-12-29 04:13:55 -05:00
EliteMasterEricandHundrec 18a1f51397 Fix the default ease direction not working for some events. 2025-12-29 03:29:47 -05:00
EliteMasterEricandHundrec a12ea5d020 Cleaned up documentation for ScriptedSongEvent 2025-12-29 03:23:37 -05:00
EliteMasterEricandHundrec 413b9503a5 Code cleanup for ZoomCamera song event 2025-12-29 03:23:37 -05:00
EliteMasterEricandHundrec 8fe9297a37 Code cleanup for SetHealthIcon song event 2025-12-29 03:23:37 -05:00
EliteMasterEricandHundrec bfb4ad5743 Code cleanup for SetCameraBop song event 2025-12-29 03:23:37 -05:00
EliteMasterEricandHundrec f400ad99b8 Code cleanup for ScrollSpeed song event 2025-12-29 03:23:37 -05:00
EliteMasterEricandHundrec 029c3614a6 Code cleanup for PlayAnimation song event 2025-12-29 03:23:37 -05:00
EliteMasterEricandHundrec 5c5b8f5e0c Code cleanup for FocusCamera song event 2025-12-29 03:23:37 -05:00
EliteMasterEricandHundrec 9354895f73 Rewrite song event tooltip logic to include default values, plus cleaned up a bunch of adjacent code. 2025-12-29 03:23:37 -05:00
EliteMasterEricandHundrec b502d5dcc6 Reworked logic to make sure measure numbers don't display if the last measure is really short. 2025-12-29 02:31:50 -05:00
EliteMasterEricandHundrec dffd6eee25 Remove "early" cropping code to guarantee Measure 1 always renders. 2025-12-29 02:31:50 -05:00
Cameron Taylor f2bf74b4d1 assets submod: fabs work final stretch 2025-12-29 01:55:23 -05:00
Cameron Taylor 38e7874b42 assets submod: feat j-hope of bts 2025-12-29 00:51:20 -05:00
Cameron Taylor bc200f3677 story menu tracklist spacing tweak 2025-12-29 00:50:28 -05:00
Cameron Taylor f7cc0ffd27 asset submod: mp3 files 2025-12-27 20:15:55 -05:00
Cameron Taylor 91429de35d assets submod 2025-12-27 20:10:50 -05:00
AbnormalPoofandHundrec 7bf3830c62 Fix a script error thrown on HTML5 if any song with Pico (Playable) outside of Weekend 1 was played 2025-12-27 03:10:55 -05:00
AbnormalPoofandHundrec 0bdc79715e Fix blood blend mode on Nene's knife 2025-12-26 21:11:06 -05:00
AbnormalPoofandHundrec cef369110b Fix sserafim-chaewon disappearing if the camera zoomed in too much 2025-12-26 20:57:41 -05:00
AbnormalPoofandHundrec 63f42de828 Fix Otis script error when pressing F5 2025-12-26 20:42:05 -05:00
AbnormalPoofandHundrec f862274a41 Only initialize the timemap for pico-speaker and otis-speaker if debug if off 2025-12-26 20:42:05 -05:00
Cameron Taylor 7197c28f62 update difficulty ratings for spaghetti (fixes FUNK-4119) 2025-12-26 15:10:34 -05:00
Cameron Taylor 8640a2a18b assets submod fabs spaghetti 2025-12-26 15:03:46 -05:00
EliteMasterEricandCameron Taylor e248562592 Delete the test suite. 2025-12-26 14:48:14 -05:00
AbnormalPoofandKade 1d2a47aea0 Revert polymod back to 39b1d179b0ebabb43234fb02604d7a91250d7fc4 due to performance issues with Spaghetti on HTML5 2025-12-24 23:16:19 -08:00
MAJigsaw77andEric 5c338f8f5c Fix FlxMacro from breaking the completion server. 2025-12-24 17:39:06 -05:00
MoonDroidandEric dafd087783 Update flixel ref for FlxText fixes 2025-12-24 17:38:57 -05:00
Hundrec 1163ae5a41 Bump mod API version to 0.8.0+
Be sure to update your mods to make them work!
2025-12-24 16:03:54 -05:00
EliteMasterEricandHundrec 81d85711fc Update Polymod to fix an issue with objects being casted to strings. 2025-12-24 16:03:54 -05:00
AbnormalPoofandAbnormal 5567e76ce6 Fix gf-dark not properly transitioning between lit and unlit 2025-12-24 13:28:43 -06:00
MoonDroid e4ab06cd73 Exclude week titles from ASTC Compression 2025-12-25 00:52:37 +07:00
MoonDroid 622dd4b6fa Exclude ref from astc and add lipsync 2025-12-25 00:52:37 +07:00
MightyTheArmiddiloandAbnormal 3b5e5893c0 Make space detect HaxeUI Focus in the Chart Editor 2025-12-24 00:50:14 -06:00
AbnormalPoofandHundrec 7fd8a46e94 Fix Nene being too high in Pico's game over 2025-12-24 01:45:58 -05:00
AbnormalPoofandKade 1f310a1068 Bump submod to fix an issue with nene-tankmen 2025-12-23 22:16:26 -08:00
ExcodeandKade 342c3cd7d6 Update OffsetMenu.hx 2025-12-23 22:00:42 -08:00
Kade-gtihubandAbnormal 31b8faefca add nene rexport commit 2025-12-23 23:59:53 -06:00
AbnormalPoofandKade 9ddd3b1ee8 Re-export Nene and Nene (Dark) to texture atlas 2025-12-23 21:55:34 -08:00
Kade-gtihubandAbnormal a7b1a4546e Update assets commit to include pause fix 2025-12-23 23:49:02 -06:00
MoonDroidandKade 110200f6b2 Remove redundant nullSafety toggles 2025-12-23 21:40:57 -08:00
AbnormalPoofandKade b776e6e200 Add LE SSERAFIM Story Mode prop 2025-12-23 21:36:20 -08:00
AbnormalPoofandKade 580d6da37f Add startingAnimation functionality to level props 2025-12-23 21:36:20 -08:00
Kade fd6ae9cf6c Convert magic variable to Fixel Constant 2025-12-23 21:32:42 -08:00
Hyper_andKade 200f798437 Improve the math for the song name position 2025-12-23 21:32:42 -08:00
AbnormalPoofandHundrec 00bc6c3d19 Fix the rain shader not working at all 2025-12-23 21:49:42 -05:00
EliteMasterEricandHundrec 54a0234c6b Fix a bug where opening the welcome screen would restart the theme music 2025-12-23 20:45:23 -05:00
EliteMasterEricandHundrec e0fcb1a3ed Fix an issue where welcome music would resume after focus lost while playtesting. 2025-12-23 20:45:23 -05:00
EliteMasterEricandHundrec 696a1755b3 Clean up isPlaytesting check. 2025-12-23 20:45:23 -05:00
EliteMasterEricandHundrec 4631950db8 Fix an issue where welcome theme would play during playtesting. 2025-12-23 20:45:23 -05:00
AbnormalPoofandAbnormal 3bb9449c60 Make multi-animateatlas characters use a getAnimateAtlas function instead of making a new FunkinSprite 2025-12-23 14:09:57 -06:00
MAJigsaw77andMoonDroid a966299926 Bump extension-admob. 2025-12-24 01:10:03 +07:00
Hundrec c2bf9da77d Consistent Chart Editor Window vbox heights 2025-12-23 03:07:49 -05:00
EliteMasterEricandHundrec 2ac6471cff Fix a bug where selecting multiple events would replace one with the other (plus cleaned up logging!) 2025-12-23 03:07:49 -05:00
AbnormalPoofandCameron Taylor c79d27eca8 Implement the Freeplay icon for LE SSERAFIM
assets submod
2025-12-23 02:16:02 -05:00
Cameron Taylor 99c67e963b assets submod fabs spaghetti 2025-12-23 02:03:53 -05:00
fabsandCameron Taylor a7b253ce9f sserafim shader 2025-12-23 02:03:53 -05:00
EliteMasterEricandAbnormal c25c784feb Disable "DEBUG"-level OpenFL messages to avoid logspam from shader stuff. 2025-12-22 20:07:36 -06:00
AbnormalPoofandHundrec b2d7e0a681 Take global offsets into account for the character preview 2025-12-22 20:59:56 -05:00
EliteMasterEricandHundrec da40dfb824 Fix measure ticks drawing beyond the end of the song. 2025-12-22 20:59:44 -05:00
EliteMasterEricandAbnormal 2177534a5b Add support for stacked patches, improve capabilities of test operations 2025-12-22 19:55:01 -06:00
MoonDroid 77ac4d8d00 Remove old junk code 2025-12-23 05:18:25 +07:00
MAJigsaw77andMoonDroid d56fbf0318 Use our fork of newgrounds. 2025-12-23 05:18:25 +07:00
MAJigsaw77andMoonDroid e9ac655835 Polish the webview stuff. 2025-12-23 05:18:25 +07:00
MAJigsaw77andMoonDroid 203e4cce81 Dont shadow the NG API aswell as automaticaly close the webview. 2025-12-23 05:18:25 +07:00
MAJigsaw77andMoonDroid 4d842ec1c9 WIP Support for Newgrounds on Mobile. 2025-12-23 05:18:25 +07:00
EliteMasterEricandAbnormal 66d6e14edb Fix a build issue caused by copy-pasting AnsiUtil to the tools package. 2025-12-22 12:42:44 -06:00
EliteMasterEricandKade 25879b234c Added more new colors to the AnsiUtil 2025-12-22 10:01:48 -08:00
EliteMasterEricandKade fcc4df0342 Add ANSI logging to even MORE classes! 2025-12-22 10:01:48 -08:00
EliteMasterEricandKade 70e5ad8874 ANSIUtil.hx code style cleanup, plus use ANSI logging in registry classes! 2025-12-22 10:01:48 -08:00
Karim AkraandMoonDroid 612b5d8009 Remove the new pillarboxing system from mobile
Mobile is not resizable so we don't really need it there
2025-12-21 16:23:16 +07:00
Karim AkraandMoonDroid d2fdda09d5 Fix the game docking to the bottom of the screen for iPads 2025-12-21 16:23:16 +07:00
AbnormalPoofandHundrec fc4bdb04b9 Fix spirit againnnnn 2025-12-21 03:25:52 -05:00
AbnormalPoofandHundrec de4d1370c7 Fix multiple characters 2025-12-21 03:25:52 -05:00
AbnormalPoofandHundrec a06500d4ed Fixes with the None character and Blazin' characters 2025-12-21 03:25:52 -05:00
AbnormalPoofandHundrec 8d3b99fbf7 Set cornerPosition to 0 2025-12-21 03:25:52 -05:00
AbnormalPoofandHundrec c90b7832df Revert some bandaid fixes for animate atlas characters to make them appear properly 2025-12-21 03:25:52 -05:00
9f59231d58 Chart Editor Character Previews
Co-Authored-By: ComedyLost <106041578+comedylost@users.noreply.github.com>
2025-12-21 03:25:52 -05:00
FuroandMoonDroid 592bf1a922 Preload the Result Screen Music loop is there is an intro 2025-12-20 19:01:46 +07:00
sector-aandMoonDroid 6e326e8e18 Prevent the week choosing by tapping when a week is selected 2025-12-20 18:23:45 +07:00
EliteMasterEric e888708b0a Update assets submodule 2025-12-19 17:09:38 -05:00
AbnormalPoofandEric 3f668fa5c8 Add album text and story menu level title for Spaghetti 2025-12-19 17:09:38 -05:00
HundrecandKolo 6e08aaeb34 Fix Chart Editor event values getting reset
Co-Authored-By: Kolo <67389779+koloindacrib@users.noreply.github.com>
2025-12-19 08:03:37 -05:00
AbnormalPoofandKade c32f794a6f Bump polymod 2025-12-17 14:32:26 -08:00
EliteMasterEricandAbnormal d2680209eb Fix a crash when browsing for files while Pause on Unfocus is disabled. 2025-12-16 08:01:28 -06:00
AbnormalPoofandAbnormal 253bb2d7f6 Bump flixel-animate and simplify rimlights for atlases 2025-12-16 01:15:33 -06:00
AbnormalPoofandCameron Taylor 8d4ced40a6 Add album art for Spaghetti in Freeplay 2025-12-15 22:24:28 -05:00
HundrecandEric 2cc04ee79d Update art 2025-12-15 20:17:49 -05:00
EliteMasterEricandHundrec e123e7ba8f Make the 'Art' submodule shallow by default 2025-12-15 17:49:04 -05:00
MAJigsaw77andHundrec 8920d9f992 Implement clearAssets for Android when building an .apk. 2025-12-15 16:30:49 -05:00
Hyper_andHundrec 6c905a59c8 fix: Pause state not being properly set during cutscenes 2025-12-15 16:28:44 -05:00
MoonDroidandHundrec 5464394dff fix: Add callbacks that fades in the pause button for video cutscenes 2025-12-15 16:28:44 -05:00
AbnormalPoofandHundrec 90f8242dc0 Re-export BF to remove that one dot in the confirm animation 2025-12-15 13:21:45 -05:00
MoonDroidandHundrec 3fc00b02dd Decrease blocksize for higher quality flashes on otis' gun bangs 2025-12-15 04:49:22 -05:00
Hyper_andAbnormal 2b55a4c5d6 Skip processing dead notes 2025-12-14 22:03:44 -06:00
AbnormalPoofandHundrec a8799fd60c Fix atlases moving to the wrong place on restart
Bundled with a ton of manual offset fixes and adjustments
2025-12-13 22:41:49 -05:00
Hundrec 8fb4a985f6 Remove Chart Editor tick trace and bump assets 2025-12-12 22:59:24 -05:00
MoonDroidandHundrec 79d09e75fc Decrease blocksizes for rando and sniper in Week 7 Erect 2025-12-12 22:46:50 -05:00
AbnormalPoofandHundrec 5589b5a4d5 Re-export the background Tankmen in Week 7 Erect as texture atlases 2025-12-12 22:46:50 -05:00
sector-aandHundrec 1e661c597a Don't add the current difficulty in Difficulty entry of the Pause Menu.
Revert "Don't add the current difficulty in `Difficulty` entry of the Pause Menu."

This reverts commit e0518f636dceeafa6dd00c922972afa5840e1161.

Do nothing if you change difficulty to the same one.
2025-12-12 21:01:51 -05:00
LasercarandHundrec 4e7755865c Chart Editor - additional audio checks for vocal only songs 2025-12-12 20:45:53 -05:00
LasercarandHundrec 960d2f3cf1 Welcome back welcome music 2025-12-12 20:45:53 -05:00
Karim AkraandMoonDroid 7debbc155f Update lime ref to hide the ios home indicator 2025-12-13 08:06:11 +07:00
AbnormalPoofandHundrec fbdd68e9d3 Fine-tuned offsets for 2 characters 2025-12-12 19:55:16 -05:00
MoonDroidandHundrec 12b2222819 Exclude pico holding nene from astc compression 2025-12-12 19:55:16 -05:00
AbnormalPoofandHundrec da3b1ec2f2 Re-export Pico (holding Nene) to texture atlas 2025-12-12 19:55:16 -05:00
MoonDroidandHundrec 66b49a6369 Exclude gf Texture Atlases from ASTC Compression 2025-12-12 19:22:48 -05:00
Cameron TaylorandKade 9d3ae3c974 remove some redundant getControls() in FreeplayState, just access controls directly 2025-12-12 08:34:59 -08:00
Kade-gtihub 5f80db48de Crash Fix and Visual Bug Fix on InstSelect 2025-12-12 08:34:59 -08:00
Hyper_andEric 8481785b3b fix: Fix incorrect NaN checks & simplify finite check 2025-12-12 11:17:16 -05:00
Hyper_andHundrec 1a6f6aee96 Fix a syntax error with the RegistryMacro and a partial fix for code completion 2025-12-12 09:34:31 -05:00
Cameron TaylorandHundrec e1e49bb19b change capsuleOptionsMenu -> instSelectMenu in FreeplayState 2025-12-12 08:16:37 -05:00
Cameron TaylorandHundrec ca7891e5cb remove some redundant getControls() in FreeplayState, just access controls directly 2025-12-12 08:16:37 -05:00
Cameron TaylorandHundrec dbf0776dbf remove all the letterSort.inputEnabled stuff, only use the FreeplayState's uiStateMachine to check if inputs should be enabled 2025-12-12 08:16:37 -05:00
Cameron TaylorandHundrec 040605e2a9 use UIStateMachine in FreeplayState instead of checking/modifying controls.active everywhere
thus making it easier to fix the "unlocked inputs" issue (fix FUNK-2021)
2025-12-12 08:16:37 -05:00
AbnormalPoofandHundrec 16f047f643 Re-export Otis (Speaker Shooter) to texture atlas 2025-12-12 07:53:24 -05:00
EliteMasterEricandHundrec f99eeae7a6 Some optimization to highlighted note checks. 2025-12-12 07:29:18 -05:00
LasercarandHundrec 8cb5ed91ba A big commit for a small fix
Cursor in chart editor breaks in square after placing and removing note
2025-12-12 07:29:18 -05:00
Hundrec 3fe7644f72 Widen Events toolbox 2025-12-12 07:29:18 -05:00
KoloandKade 9f6879ecc9 add frames to the event data toolbox 2025-12-11 12:38:17 -08:00
AbnormalPoofandAbnormal 5c44707bc9 Re-export some Week 7 assets to texture atlases 2025-12-10 14:32:13 -06:00
MAJigsaw77andKade ee2a77d570 Add the ability to embed songs audio files. 2025-12-10 11:26:33 -08:00
AbnormalPoofandAbnormal 770bad3a6d Re-export Girlfriend (Dark) to texture atlas 2025-12-10 00:54:30 -06:00
AbnormalPoofandAbnormal e8ad0e605f Re-export Spooky Kids to remove a billion black lines 2025-12-10 00:52:03 -06:00
AbnormalPoofandAbnormal 3267ebf0d9 De-sharpen some atlases and optimize Monster's atlas 2025-12-10 00:39:14 -06:00
LasercarandHundrec 5a2c364c50 Chart - Fix notekinds changing on multi select
Also reverts a change by this author that caused selected events to be reset when the event toolbox is first opened.
2025-12-10 00:43:12 -05:00
LasercarandHundrec d3c16b45c6 Custom note kind field fix
Now gets hidden if the note selected isn't a custom one
2025-12-10 00:43:12 -05:00
VirtuGuyandHundrec dff2a1e487 Fix difficulty toolbox showing wrong song name 2025-12-09 23:41:27 -05:00
AbnormalPoofandHundrec 5dce8af395 Re-export bf and bf-dark to texture atlas form and sever the death animation from bf-car 2025-12-09 23:03:00 -05:00
a844618ca2 fix: Stop the game from muting in silent mode when you unfocus then focus
Co-Authored-By: Karim Akra <144803230+KarimAkra@users.noreply.github.com>
2025-12-09 22:53:41 -05:00
VirtuGuyandHundrec 8c43f6f67a Fix results screen scrolling text speed 2025-12-09 21:07:17 -05:00
AbnormalPoofandHundrec 09e1b17905 Make Tankman less shiny 2025-12-09 19:56:38 -05:00
AbnormalPoofandHundrec 2294ef80d5 Re-export Tankman and Tankman (Bloody) to texture atlas 2025-12-09 19:56:38 -05:00
KoloandAbnormal 4dd87e7308 make volume settings optionally carry over to playtest 2025-12-09 12:53:18 -06:00
AbnormalPoofandAbnormal 47e462e836 Bump flixel-animate and add useRenderTexture to FunkinSprite as an option. 2025-12-09 10:51:42 -06:00
Hyper_andHundrec 00a1a7d1e2 fix: solve crash when deleting save data 2025-12-09 02:52:15 -05:00
LasercarandAbnormal eeba677da9 Chart Editor playtest option for Results Screen 2025-12-09 01:12:11 -06:00
charlesisfelineandHundrec 9f0a4988a3 null checkin for set health icon 2025-12-09 01:44:07 -05:00
AbnormalPoofandHundrec ede100c9da Re-export Girlfriend to texture atlas 2025-12-09 01:09:32 -05:00
AbnormalPoofandHundrec 3b65050ef3 Shift the flames how did this happen 2025-12-09 00:19:25 -05:00
AbnormalPoofandHundrec 63fedc046b Re-export the Freeplay difficulty stars using BTA 2025-12-09 00:19:25 -05:00
HundrecandAbnormal 741c8aa5fc Quick chart tweak bump 2025-12-08 20:18:02 -06:00
Cameron TaylorandEric bdddf364a0 change fpScoreDisplay to private variable 2025-12-08 21:03:11 -05:00
Cameron TaylorandEric 109fa509fa change fp -> fpScoreDisplay, as a more descriptive variable name 2025-12-08 21:03:11 -05:00
Cameron Taylor 90f79be163 assets submod more le sserafim stuff from fabs 2025-12-08 19:32:05 -05:00
MAJigsaw77andEric 2d0a1cdabb Remove WINAPI_GetProcessMemoryWorkingSetSize dublicate. 2025-12-08 18:14:17 -05:00
EliteMasterEricandMAJigsaw77 3bc8b0317e Adjust WinAPI to allow setting theme on specific windows.
Co-authored-by: MAJigsaw77 <MAJigsaw77@users.noreply.github.com>
2025-12-08 18:14:17 -05:00
Cameron TaylorandEric d361a323a2 early return in playVocals() to remove a level of nesting 2025-12-08 15:30:02 -05:00
Cameron TaylorandEric ceb364472e reduce code copypaste a tiny smidge for chart editor waveforms 2025-12-08 15:30:02 -05:00
MoonDroidandAbnormal 41f04a11b3 Decrease blocksize compression for gfChill 2025-12-07 12:00:30 -06:00
AbnormalPoofandAbnormal 1a24ed42e1 Re-export GF's character select sprites to remove a stray line 2025-12-07 12:00:30 -06:00
AbnormalPoofandAbnormal 75bc9300fb Re-export the Parents in the Eggnog Erect cutscene with BTA 2025-12-07 11:58:29 -06:00
AbnormalPoofandHundrec e76d3c3d26 Add global offsets for Pico and Nene's pixel variants 2025-12-06 22:57:15 -05:00
FuroandHundrec 6f3264676c Added funnyColor backwards compatibility 2025-12-06 21:23:46 -05:00
FuroandHundrec 67846d6c4b Change BGScrollingText to a single FlxText
BGScrollingText is now a single FlxText drawn multiple time in a single frame which is more optimized that a FlxSpriteGroup with multiple FlxSprites taking the text's graphic
2025-12-06 21:23:46 -05:00
EliteMasterEricandCameron Taylor 2c588b6a93 Tweak vertical offset on the measure numbers. 2025-12-06 20:27:10 -05:00
EliteMasterEricandCameron Taylor f102b0d681 Redo the backing color of the measure ticks. 2025-12-06 20:27:10 -05:00
EliteMasterEricandCameron Taylor 86a0c8a04e Rewrite measure ticks to not get redrawn EVERY FRAME GODAM 2025-12-06 20:27:10 -05:00
Hyper_andCameron Taylor 75b13d0052 fix: Cursor not properly fading out when exiting CharSelect with backspace 2025-12-06 20:15:20 -05:00
Cameron TaylorandAbnormal 9654d5ad98 remove unused pitch stuff in CharSelect 2025-12-06 16:37:21 -06:00
Cameron TaylorandAbnormal 07fb33be62 fix char select inputs for keyb and controllers 2025-12-06 16:37:21 -06:00
EliteMasterEricandCameron Taylor 9d8a653ab8 Clean up the Lime target config dropdown by removing unused targets like AIR and Flash. 2025-12-04 18:49:40 -05:00
AbnormalPoofandAbnormal 4a7115b103 Re-export Pico's PERFECT rank animation with BTA 2025-12-04 12:58:49 -06:00
AbnormalPoofandAbnormal b78d989394 Fix the cursor lerping from the top left in character select 2025-12-04 10:50:32 -06:00
AbnormalPoofandAbnormal 88baab88e9 Spaghetti chart tweaks from Cory 2025-12-04 09:06:35 -06:00
AbnormalPoofandEric 3f0082d6e7 Add getFramesWithKeyword() 2025-12-03 02:13:47 -05:00
anysadandKade 0df50ecf5d Remove unused Chart Editor Context Menu code 2025-12-02 21:16:30 -08:00
MAJigsaw77andAbnormal 55144f0a9e Bump hxvlc to 2.2.5. 2025-11-30 16:06:35 -06:00
MAJigsaw77andAbnormal a8d5651ab0 Remove the yellow notice. 2025-11-30 15:29:12 -06:00
MAJigsaw77andAbnormal 535aecdc01 Improve library version checks. 2025-11-30 15:29:12 -06:00
AbnormalPoofandAbnormal f7727c0584 Lower him a little 2025-11-30 14:38:33 -06:00
AbnormalPoofandAbnormal 1e52320014 Re-export Boyfriend (Car) to texture atlas 2025-11-30 14:38:33 -06:00
AbnormalPoofandAbnormal 3cd12d4298 Revert "remove keyboard "scheme" related code, completely unused"
This reverts commit 7c8b7eee57.
2025-11-29 20:12:00 -06:00
EliteMasterEricandAbnormal f94ec82c96 Update HXCPP to support building with C++20 2025-11-29 13:03:42 -06:00
Cameron TaylorandEric 3a0e32d24f update our custom FunkinAction check() function with newer logic from FlxAction 2025-11-29 06:41:33 -05:00
Cameron TaylorandEric 6eba4bbeda remove the unused _P and _R stuff from the Action enum abstract 2025-11-29 06:41:33 -05:00
Cameron TaylorandEric 724286f497 more controls.hx cleanup, remove a few unused functions, and add Void return types to make checkstyle happier 2025-11-29 06:41:33 -05:00
Cameron TaylorandEric 7c8b7eee57 remove keyboard "scheme" related code, completely unused 2025-11-29 06:41:33 -05:00
Cameron TaylorandEric c23472903a remove more unused vars in controls.hx 2025-11-29 06:41:33 -05:00
AbnormalPoofandHundrec 846dbd6259 Adjust Darnell's idle animation 2025-11-29 04:14:54 -05:00
AbnormalPoofandHundrec 05447c7894 Re-export THE CHUDDER!!!! 2025-11-29 04:14:54 -05:00
KoloandHundrec c4c7e17361 better backwards compatibility for FlxAtlasSprite -> FunkinSprite 2025-11-29 02:32:47 -05:00
Cameron TaylorandHundrec f3057dd9c3 use FlxMath.wrap() to make this cursor wrapping cleaner in CharSelectSubState 2025-11-28 20:34:58 -05:00
Cameron TaylorandHundrec 0e195cfb9e clean up spamDirection code, use FlxDirectionFlags 2025-11-28 20:34:58 -05:00
MoonDroidandAbnormal 611280b069 fix: Make lerping consistent across different FPS 2025-11-28 19:30:36 -06:00
AbnormalPoofandHundrec 74e2ff7b85 Fix the cigarette not appearing in the Pico doppelganger cutscene 2025-11-28 20:21:56 -05:00
Cameron TaylorandAbnormal 326b81c225 move character select cursor sprite code into it's own class 2025-11-28 18:35:10 -06:00
VirtuGuyandHundrec 3d4a281da6 Fix ghost tapping behavior when unpausing 2025-11-28 14:31:30 -05:00
Cameron TaylorandKade ff4a08541c early return for Chart Editor handleCursor() 2025-11-28 10:36:08 -08:00
EliteMasterEricandHundrec 7f5acf3b2d Fix a bug where cycling difficulties wasn't calculated properly when switching variations. 2025-11-28 04:45:16 -05:00
EliteMasterEricandHundrec eed0665e42 Switching difficulty now forces the selection to be cleared. 2025-11-28 04:45:16 -05:00
EliteMasterEricandHundrec 85c549c15c Add difficulty switches to the undo/redo history. 2025-11-28 04:45:16 -05:00
Hyper_andHundrec 5763ae5b72 Add hardware keyboard detection on Android 2025-11-28 03:12:25 -05:00
MoonDroidandAbnormal 55cad46640 Update lime ref 2025-11-28 00:39:48 -06:00
EliteMasterEricandCameron Taylor 65985173fa Implemented Clone Difficulty button in the Difficulty toolbox.
Fixes to Clone Difficulty dialog, and added Move Difficulty dialog.

assets submod
2025-11-28 01:05:19 -05:00
EricandGitHub 7dfd6ab8cd Merge pull request #1992 from FunkinCrew/pr-6495/koloindacrib/chart-editor-playtest-pause
[PUBLIC PR] Fix the game immediately pausing when playtesting in the Chart Editor
2025-11-26 22:45:27 -05:00
KoloandAbnormalPoof ba3481356c fix starting a playtest opening pause menu 2025-11-26 15:49:22 -06:00
AbnormalPoofandHundrec 58467843d5 Make Monster's singLEFT-hold offset consistent with singLEFT 2025-11-26 14:21:54 -05:00
HundrecandAbnormal 3219c40629 Adjust lyrics for Monster and Winter Horrorland
Fix some errors and formatting
2025-11-26 13:12:43 -06:00
d35fe9726b Update assets and add getAtlasSettings() for texture atlas characters
Co-authored-by: VirtuGuy <Wyatt2010shaw@gmail.com>
2025-11-26 13:12:43 -06:00
EliteMasterEricandHundrec d311a3ad5b Implement a checkbox to hide the waveform, which improves FPS. 2025-11-26 01:07:53 -05:00
EliteMasterEricandHundrec 358d91251e Code cleanup and additional docs for WaveformSprite. 2025-11-26 01:07:53 -05:00
EliteMasterEricandHundrec 25256fad3c Pause the chart editor when focus is lost. 2025-11-26 00:57:43 -05:00
Hundrec 3f4056c9f9 Don't play Stress Pico cutscene if charting 2025-11-26 00:18:16 -05:00
PurSnakeandHundrec 055f62026e Properly load variation scripts in the Chart Editor when playtesting 2025-11-26 00:18:16 -05:00
amyspark-ng ✨andHundrec 593456e53e fix notelay sound when not in grid 2025-11-26 00:03:55 -05:00
AbnormalPoofandHundrec ef9f77c665 Re-export Pico's confirm backing card with BTA to bake the filters 2025-11-25 13:59:49 -05:00
Karim AkraandHundrec dc88d62f43 Fix stickers transition looking off-size when resizing the game during a song 2025-11-25 12:22:22 -05:00
VirtuGuyandHundrec 6909b5eca1 Fix anim editor onion skin offset problems 2025-11-25 12:15:00 -05:00
AbnormalPoofandHundrec 949195fb9c Get the first owned character ID and use that for results 2025-11-25 12:07:32 -05:00
AbnormalPoofandHundrec 23aa7156dc Fix the nametag erroneously showing as "Boyfriend" when entering character select as Pico 2025-11-25 11:59:31 -05:00
Hundrec 52694d0999 Bump version to 0.8.0 2025-11-25 11:59:31 -05:00
Cameron Taylor 3e4c15e019 fabs work for le sserafim in progress 2025-11-24 20:54:21 -05:00
MightyTheArmiddiloandHundrec 9e645db007 up and only up the notifs go 2025-11-23 06:51:25 -05:00
PurSnakeandHundrec 3040692852 Reduce volume on focus lost 2025-11-23 06:36:51 -05:00
193c443bc4 Add offset var to note in NoteStyle.hx
Co-authored-by: NebulaStellaNova <78160470+NebulaStellaNova@users.noreply.github.com>
2025-11-23 06:12:45 -05:00
cb184f89ec 2hot cutscene realignment
Co-authored-by: hucks5 <hucks5@users.noreply.github.com>
2025-11-23 06:01:38 -05:00
c2d89243b6 Nene now swings her legs in-game in the 2hot cutscene
Co-authored-by: hucks5 <hucks5@users.noreply.github.com>
2025-11-23 05:49:23 -05:00
Nebula S. NovaandAbnormal 75d085d733 Remove redundant/useless sharpness setting from infoDisplay
This value only sets if you have antiAliasType as ADVANCED, which v-slice does not.
2025-11-23 04:33:21 -06:00
AbnormalPoofandHundrec e550440407 Re-export Week 5 characters (and dad too I guess) 2025-11-23 05:17:13 -05:00
AbnormalPoofandHundrec f1eb67bf52 Fix an issue where the default results animation (BF) wouldn't show up if the character ID wasn't associated 2025-11-23 05:11:47 -05:00
AbnormalPoofandHundrec efafb1acea Re-export week 4 characters (Mom and GF) to texture atlases 2025-11-23 04:58:41 -05:00
AbnormalPoofandHundrec 56c6c392b3 Fix Nene's heart in Week 7 being affected by rimlight 2025-11-23 03:40:25 -05:00
AbnormalPoofandHundrec d5afb340a2 Add flush() back to Save 2025-11-23 03:29:46 -05:00
JVN-PixelsandAbnormal f95fbf57b6 Fix typo in Medals.hx 2025-11-23 02:22:34 -06:00
JVN-PixelsandAbnormal 6b5db32522 Fix typo in Conductor.hx 2025-11-23 02:22:34 -06:00
charlesisfelineandAbnormal 300ef8fef1 yeahh im listerning 2025-11-23 02:22:34 -06:00
MightyTheArmiddiloandAbnormal e09c256c80 The true different was the frie- 2025-11-23 02:22:34 -06:00
ExcodeandAbnormal 7174e776ac Remove useless comment on CharSelectSubState.hx 2025-11-23 02:22:34 -06:00
ExcodeandAbnormal f10ea83530 Update SpectogramSprite.hx 2025-11-23 02:22:34 -06:00
ComedyLostandAbnormal e19f2155b1 evil typo 2025-11-23 02:22:34 -06:00
86a18067dd Fix Pico Backing Card Confirm
Co-authored-by: VirtuGuy <VirtuGuy@users.noreply.github.com>
2025-11-23 02:57:30 -05:00
AbnormalPoofandHundrec 15caa7a6f7 literally one digit im krueing myself 2025-11-23 02:43:36 -05:00
KoloandHundrec b2897bc86c copy maps instead of assigning 2025-11-23 01:53:22 -05:00
MikolkaandHundrec cc486ad94b Add song prev fade in delay 2025-11-23 01:39:38 -05:00
AbnormalPoofandHundrec c5c22fe1a5 More strict null checking for animation 2025-11-22 19:46:20 -05:00
VirtuGuyandHundrec 302beb2d6e Fix Menu Music Not Playing After Freeplay Exit 2025-11-22 15:39:25 -07:00
ACrazyTownandAbnormal a702040dfb [fix] Replace wrong PRESSED checks with JUST_PRESSED checks 2025-11-22 13:14:55 -06:00
VirtuGuyandAbnormal 1510e5922e Fix character select cutout size 2025-11-22 13:13:57 -06:00
EliteMasterEricandHundrec 92344d2e30 Don't play the music if the game paused due to losing focus. 2025-11-22 05:19:50 -07:00
EliteMasterEricandHundrec 5ce3315967 Only do it if "Pause on Unfocus" is enabled. 2025-11-22 05:19:50 -07:00
EliteMasterEricandHundrec 858199e298 Pause the pause music if the window isn't focused (just like all other audio) 2025-11-22 05:19:50 -07:00
VirtuGuyandHundrec 0739a40abf Fix mobile compiling issues 2025-11-21 20:58:29 -07:00
AbnormalPoofandHundrec 215bbdb9da fix for "janky" pico rank anim 2025-11-21 18:53:42 -07:00
AbnormalPoofandHundrec c9d4d07dc7 bump to latest and include all classes when building 2025-11-21 18:53:42 -07:00
AbnormalPoofandHundrec 7e1442e969 maybe fix it 2025-11-21 18:53:42 -07:00
fcfde61b24 Replace FlxAnimate with flixel-animate
Co-Authored-By: Hyper_ <40342021+NotHyper-474@users.noreply.github.com>
Co-Authored-By: MaybeMaru <97055307+MaybeMaru@users.noreply.github.com>
Co-Authored-By: Trayfellow <48515908+trayfellow@users.noreply.github.com>
2025-11-21 18:53:42 -07:00
Cameron Taylor fe6f5eb821 small typo in pausesubstate UI_DOWN -> controls.UI_DOWN 2025-11-21 19:17:13 -05:00
Cameron Taylor 512a3fc927 some extra pausesubstate tidyings/organization 2025-11-21 18:58:16 -05:00
EliteMasterEricandCameron Taylor 3bf06a1cc9 Check if Pause menu inputs were JUST pressed to prevent firing immediately when opening. 2025-11-21 18:58:16 -05:00
EliteMasterEricandCameron Taylor 2c3d61e629 Implement justPressed and justReleased chesk for PAUSE and ACCEPT. 2025-11-21 18:58:16 -05:00
EliteMasterEricandCameron Taylor 20051109dd Remove unused control actions. 2025-11-21 18:58:16 -05:00
EliteMasterEricandCameron Taylor 267cc55e40 Don't reset inputs on state switch (fixes justPressed triggering as soon as you enter a state) 2025-11-21 18:58:16 -05:00
EliteMasterEricandCameron Taylor 766937d5fe Cancel ongoing timers when leaving Freeplay. 2025-11-21 02:06:50 -05:00
Cameron Taylor 18d8fe7248 only show that giant white debug box thing on mobile 2025-11-21 02:05:01 -05:00
EliteMasterEricandCameron Taylor f7cb738803 WIP on implementing multiple keys per direction 2025-11-21 02:01:35 -05:00
EliteMasterEricandHundrec 400ef63c5a Attract State shouldn't interrupt the GF ringtone song. 2025-11-20 05:25:52 -07:00
LasercarandHundrec 1ff6a1656c Open target song stage with song characters
Also, nullable test characters (but it's buggy)
2025-11-20 05:19:01 -07:00
EricandGitHub 20da3f85f1 Merge pull request #1933 from FunkinCrew/kade-github/non-pausestate-pausing
[ENHANCEMENT] Non-descript state pausing for PlayState
2025-11-18 22:28:39 -05:00
KadeandGitHub d5e14a7101 change shouldPause to shouldSubstatePause 2025-11-18 16:13:11 -08:00
Kade-gtihub ffa8622903 non-descript state pausing 2025-11-18 16:05:40 -08:00
Kade-gtihubandHundrec 8b559639e8 visible check for notes 2025-11-18 14:03:57 -07:00
Kade-gtihubandHundrec 628c0e8975 other notes 2025-11-18 14:03:57 -07:00
Kade-gtihubandHundrec 1734ec1dbf steps/fakes notekind 2025-11-18 14:00:18 -07:00
Karim AkraandHundrec b10ae8dfa5 Fixed playstate not removing the black bars when loading it 2025-11-18 12:00:11 -07:00
Karim AkraandHundrec dc634fa35c Fixed some substates looking a bit broken after resizing 2025-11-18 12:00:11 -07:00
Cameron Taylor 06246aa3d7 assets submod fix -> master 2025-11-17 17:05:45 -05:00
Karim AkraandKade 7c1eb4f88c block vertically wide resolutions 2025-11-15 10:08:45 -08:00
Karim AkraandKade 38634ef5ca Prevent the game bounds from breaking when resizing the window 2025-11-15 10:08:45 -08:00
EliteMasterEricandKade 5d76d23d33 Fix a script error while hot reloading in the Game Over screen. 2025-11-14 16:40:19 -08:00
EliteMasterEricandKade da422c913f Update chart editor layout to fix a bug 2025-11-14 16:36:55 -08:00
Kade-gtihub ba52b932ee controller crash fix 2025-11-14 16:34:05 -08:00
EricandKade 1b90b7bebf Include MacOS Actuator crash in HaxeUI update 2025-11-14 13:38:48 -08:00
Kade-gtihub 23ca9c14c1 Fix crash on non-existant tooltip 2025-11-14 13:38:48 -08:00
AbnormalPoofandAbnormal ab300be709 Fix a compilation error with UNLOCK_EVERYTHING disabled 2025-11-14 12:29:06 -06:00
Kade-gtihub e6aaca19f6 stepmania importing
fix stops
2025-11-07 21:59:07 -08:00
Cameron Taylor 0fe226132d quick fix for incorrect flush() usage 2025-11-04 15:24:28 -05:00
Cameron Taylor 452e3a58d8 modify chart editor to save some preferences more immediately 2025-11-04 14:59:24 -05:00
Cameron Taylor e8f60a23ef helpers for data binding in progress in Save.hx
move buncho save data stuff to our new SaveProperty class

SaveMacro to generate save property stuff
2025-11-04 14:59:24 -05:00
Cameron Taylor 958a6f49ce some data binding funzies i think 2025-11-04 14:59:24 -05:00
Cameron Taylor 2005628358 move these vars up with the rest of em 2025-11-04 14:59:24 -05:00
Cameron Taylor 439901b42e buncho lil null tidying 2025-11-04 14:59:24 -05:00
Cameron Taylor 7bbe356009 move a buncho stuff from Save.hx to SaveSystem.hx
to rebase
2025-11-04 14:59:24 -05:00
Cameron Taylor 9d710b0490 some more Save.hx cleanup worky 2025-11-04 14:59:24 -05:00
Cameron Taylor d140caf4ba move Save.hx consts to Constants.hx file 2025-11-04 14:59:24 -05:00
MAJigsaw77andCameron Taylor 15b691ad7b Make the haxelib warnings be properly handled on Git Dev libraries.
Adjust the haxelib warning message aligment.

Acually, it aint neccesary.
2025-11-03 18:03:50 -05:00
AbnormalandGitHub 02b53e4900 Fix a security vulnerability in ReflectUtil 2025-11-03 13:03:54 -08:00
AbnormalPoofandHundrec 5b5d76aa59 Make CLASSIC the default easing type for Focus Camera 2025-10-28 22:52:40 -04:00
AbnormalPoofandHundrec ae861214cb Revert "Fix type mismatch in smooth lerp functions"
This reverts commit 4f1bbd3f9b.
2025-10-27 18:59:13 -04:00
AbnormalPoofandAbnormal 4d5558f3d8 Add the new characters to the Results Screen song name font... thingyy.... 2025-10-27 17:54:33 -05:00
MoonDroidandAbnormal 0af6f61fb9 Update flixel ref.
* fix: Fix glitchy bitmapfont text for Results, etc. when using missing letters.
2025-10-27 17:54:33 -05:00
EricandGitHub 72a2e9c895 Merge pull request #1914 from FunkinCrew/fix/smoothlerp-mismatch
Fix type mismatch in smooth lerp functions
2025-10-27 15:43:35 -04:00
MoonDroid 4f1bbd3f9b Fix type mismatch in smooth lerp functions 2025-10-24 16:58:51 +07:00
Hundrec b99089dd88 Assets for ansi 2025-10-23 17:29:55 -04:00
MoonDroidandHundrec 380eb5cbaf Add remaining ansi flair woo to every class that needs it 2025-10-23 17:29:55 -04:00
MoonDroidandHundrec a398eb17c4 Add missing Ansi flair to macros 2025-10-23 17:29:55 -04:00
MoonDroidandHundrec 9765f5efe5 Use ansi for polymod logs 2025-10-23 17:29:55 -04:00
MoonDroidandHundrec d6e1f0c90d Remove dupe ERROR tag from getGitCommit() 2025-10-23 17:29:55 -04:00
MoonDroidandHundrec f56fa1955e Make AnsiUtil.apply() remove duplicate RESET codes from provided string 2025-10-23 17:29:55 -04:00
MAJigsaw77andHundrec 277abfab2b Fix html5 compile aswell as some other ANSI not showing up. 2025-10-23 17:29:55 -04:00
MoonDroidandHundrec 5c1b0ae184 Make AnsiUtil be more like StringTools instead. 2025-10-23 17:29:55 -04:00
MoonDroidandHundrec ee952c0789 Ansify Newgrounds, Discord, and HXVLC traces 2025-10-23 17:29:55 -04:00
MoonDroidandHundrec eb1d54e750 Use AnsiUtil for AnsiTrace, and highlight traces with headers 2025-10-23 17:29:55 -04:00
MoonDroidandHundrec 0d8fd214a1 Add BG_ORANGE 2025-10-23 17:29:55 -04:00
07b0af53f1 Add tools.AnsiUtil fir specific macros and project.hxp
Co-Authored-By: Mihai Alexandru <77043862+MAJigsaw77@users.noreply.github.com>
2025-10-23 17:29:55 -04:00
LasercarandAbnormal 7ffa3d5bf2 Fix UI file checks 2025-10-23 10:57:26 -05:00
HundrecandAbnormal 615d52aff2 Remove BF instrumentals for Pico 2025-10-22 17:54:11 -05:00
ImVeryBadandAbnormal 4961d4ee99 Allow level props to have a flipX field in JSON 2025-10-22 17:06:57 -05:00
ImVeryBadandAbnormal 33a020a34b StageProp animation offsets finally working ! 2025-10-22 16:57:34 -05:00
ComedyLostandAbnormal 6ba44d6cb0 one line banger 2025-10-22 16:51:39 -05:00
Nebula S. NovaandAbnormal 277d16895b Fix Healthicon bug when restarting 2025-10-22 16:26:43 -05:00
AbnormalandGitHub dbd19159b6 Merge pull request #1905 from FunkinCrew/merging-because-til-asked-me-to 2025-10-22 16:12:47 -05:00
HundrecandGitHub 9f5c88ffa1 Merging main into develop because Til asked me to 2025-10-22 17:01:52 -04:00
HundrecandKade a528a11b8c Bump assets 2025-10-22 13:14:35 -07:00
FuroandKade a52f4c5e95 Changes that Kade Requested 2025-10-22 12:50:11 -07:00
FuroandKade 0be42bf047 Osu!Mania Importing 2025-10-22 12:50:11 -07:00
AbnormalPoofandMoonDroid ba31f3611a Fix macos compilation due to a mobile-specific change 2025-10-21 06:14:49 +07:00
MoonDroidandHundrec 3119a0cb6e fix: Make it so external music gets paused when tabbing/opening into the game for mobile 2025-10-20 16:53:55 -04:00
LasercarandAbnormal 5326c40347 Stage the backup 2025-10-19 23:39:49 -05:00
Hyper_andAbnormal bbcb19d476 next time you create a Sprite that covers the whole screen pls don't make it receive mouse events 2025-10-19 23:31:44 -05:00
TechnikTilandHundrec e6d6972ab0 Fix htmlText subtitles 2025-10-20 00:14:21 -04:00
CrusherNotDripandAbnormal 4f8da59577 Make the backups folder more organized. 2025-10-19 23:03:14 -05:00
TechnikTilandHundrec 6686812504 Host README.md images on the repository 2025-10-19 19:49:52 -04:00
AbnormalPoofandHundrec 80993df87f Fix assets being cleared when running lime run 2025-10-19 19:34:07 -04:00
AbnormalPoofandHundrec 30361954f3 Fix the assets folder... not deleting assets at all. 2025-10-19 01:08:52 -06:00
ImVeryBadandKade 04965f38c6 Add support for opening folders on Linux 2025-10-17 15:15:05 -07:00
AbnormalPoofandMoonDroid 5d8360333a Update flixel, flixel-addons, and openfl 2025-10-17 15:19:39 +07:00
MAJigsaw77andMoonDroid 2a88adf6b3 Fix FunkinSoundTray. 2025-10-17 15:19:39 +07:00
MAJigsaw77andMoonDroid 38e70cb678 Fix uv refactor. 2025-10-17 15:19:39 +07:00
MAJigsaw77andMoonDroid 878427edb5 Remove the focus override in FunkinSound. 2025-10-17 15:19:39 +07:00
MAJigsaw77andMoonDroid 4dcf83f909 Change to24Bit() to rgb. 2025-10-17 15:19:39 +07:00
MAJigsaw77andMoonDroid c14bf35144 Fix GraphicCursorCross. 2025-10-17 15:19:39 +07:00
MAJigsaw77andMoonDroid 3ae09b6d5a Fix tileMatrix. 2025-10-17 15:19:39 +07:00
Karim AkraandAbnormal b778195dd1 Fixed the hxcpp check sometimes checking an incorrect path 2025-10-15 16:48:19 -05:00
sector-aandAbnormal 5f8bb643b8 whoa....a one-symbol fix for the subtitles 2025-10-15 13:36:41 -05:00
Hundrec af35608953 Bump assets 2025-10-15 07:59:39 -06:00
TilandHundrec 647f158788 Stop the haxe compilation server from clearing assets
dude i was fighting with this for DAYS
2025-10-15 07:59:39 -06:00
AbnormalPoofandHundrec e42b8c7b9f A much cleaner way to check for C++ 2025-10-15 07:59:39 -06:00
d9387f6a09 Automatically compile HXCPP Build Tools
Co-authored-by: ImVeryBad <80699736+Starexify@users.noreply.github.com>
2025-10-15 07:59:39 -06:00
MAJigsaw77andHundrec 1804cdb183 Fix the video not restarting properly on html5. 2025-10-14 10:18:23 -06:00
TechnikTilandAbnormal 2ad593341f Fix VideoCutscene checking by loading the videos OpenFL library 2025-10-14 08:33:29 -05:00
Karim AkraandHundrec 1d18a14356 Fixed compiling for html5 targets by excluding the FNFC loading stuff 2025-10-14 06:54:08 -06:00
f1a1db0834 fix normal gameover music not playing after starting music
Co-authored-by: Kolo <67389779+KoloInDaCrib@users.noreply.github.com>
Co-authored-by: MoonDroid <81515012+moondroidcoder@users.noreply.github.com>
2025-10-14 19:28:01 +07:00
1ebce748d4 Hide back button when selecting week
Co-authored-by: VirtuGuy <Wyatt2010shaw@gmail.com>
Co-authored-by: MoonDroid <81515012+moondroidcoder@users.noreply.github.com>
2025-10-14 19:13:53 +07:00
sector-aandHundrec 836129ec8a Add Back Button in the Character Select Menu 2025-10-14 06:13:25 -06:00
MoonDroid 97a5e2ba14 Update preferences' description for debug display 2025-10-14 19:09:08 +07:00
f75f83f653 Use Inverted Prefix
Co-authored-by: Til <techniktil@tilnotdrip.org>
2025-10-14 19:09:08 +07:00
FuroandMoonDroid 7a16a7465a Debug Display on Mobile Debug Builds 2025-10-14 19:09:08 +07:00
gamerbrossandHundrec 62a3f73de9 Better strumline note confirm animations 2025-10-14 05:43:13 -06:00
MAJigsaw77andHundrec 113fa19bed Fix MacOS latest sdk compile. 2025-10-14 04:50:41 -06:00
7b8d6577ed Fix Downscroll vwoosh
Co-authored-by: Furo <furogaming9@gmail.com>
Co-authored-by: MoonDroid <81515012+moondroidcoder@users.noreply.github.com>
2025-10-14 17:30:53 +07:00
sector-aandHundrec 36f9701b5b Don't play Character Select slide animation when entering 2025-10-13 21:20:41 -06:00
sector-aandHundrec ec4ebf5a15 Prevent char select from opening before transition ends 2025-10-13 21:16:43 -06:00
KoloandHundrec c1988f1858 make hotreloading during playtest only affect playtest 2025-10-13 20:48:07 -06:00
AbnormalPoofandHundrec be85dd06e5 Simplify camera retrieval for Stress (Pico Mix) camera 2025-10-13 20:41:50 -06:00
Sphis-SincoandHundrec 4366fbe512 Fix a crash when HARDCODED_CREDITS is disabled. 2025-10-13 20:15:39 -06:00
Hundrec 987b3846ef Bump json2object 2025-10-13 19:26:15 -06:00
ImVeryBadandHundrec f40c74b153 Exclude all *.iml files instead of just Funkin and unit 2025-10-13 18:54:04 -06:00
ImVeryBadandHundrec 23ca7357d4 Exclude IntelliJ IDEA stuff from git 2025-10-13 18:54:04 -06:00
ImVeryBadandHundrec 95af46cbf7 Double pressing not allowed, one press at a time 2025-10-13 18:52:47 -06:00
sector-aandHundrec 56af0d6a11 Clear assets for Desktop Platform Type only 2025-10-13 18:47:08 -06:00
sector-aandHundrec c39046d999 Improve the way positioning is done 2025-10-13 11:15:22 -06:00
sector-aandHundrec 476336be68 Tweak subtitles position for downscroll 2025-10-13 11:15:22 -06:00
sector-aandHundrec e0bd5b8038 Add Subtitles to the Chart Editor! 2025-10-13 11:15:22 -06:00
sector-aandHundrec 0691207413 Unneccessary stuff 2025-10-13 11:15:22 -06:00
Hundrec 16ea626c28 Adjust descriptions for all preferences
For clarity and readability
2025-10-13 11:15:22 -06:00
sector-aandHundrec 81788550bf Fix multiline subtitles a bit 2025-10-13 11:15:22 -06:00
sector-aandHundrec ed2bd14a71 Make overlapped timeline lines actually work lol 2025-10-13 11:15:22 -06:00
sector-aandHundrec 7b66ff2304 Make camSubtitles be over all the cameras except camPause 2025-10-13 11:15:22 -06:00
sector-aandHundrec 54f2e75139 remove video subtitles preference 2025-10-13 11:15:22 -06:00
sector-aandHundrec 279b0755ea Subtitles preference 2025-10-13 11:15:22 -06:00
sector-aandHundrec b44deb393d Add a separated camera for subtitles 2025-10-13 11:15:22 -06:00
sector-aandHundrec 714af8ebe0 Prevent the subtitles text from being set each frame 2025-10-13 11:15:22 -06:00
sector-aandHundrec b4a1730e37 SRT Implementation for in-game subtitles! 2025-10-13 11:15:22 -06:00
sector-aandHundrec a85b3be037 Properly working html text elements and some documentation 2025-10-13 11:15:22 -06:00
sector-aandHundrec 90b8946429 Basic in-game subtitles implementation 2025-10-13 11:15:22 -06:00
sector-aandHundrec 09192474d7 Add haptics when you tap to begin in Title Screen 2025-10-12 06:33:38 -06:00
sector-aandHundrec 6061d5f438 Don't open the Random capsule's inst menu on mobile 2025-10-12 02:16:46 -06:00
sector-aandAbnormal 7aa9a0c9a8 assets ref 2025-10-11 00:24:47 -05:00
EliteMasterEricandHundrec b1a37a7674 Haxelib updates to json2object and Polymod 2025-10-10 01:08:25 +08:00
HundrecandAbnormal ee413f06dd Bump FlxPartialSound to fix that one deprecation warning
Hey, did a cat just walk across your keyboard?
2025-10-09 08:59:20 -05:00
Mihai AlexandruandHundrec fbbc0b2dcc Adjust logic. 2025-10-09 21:17:49 +08:00
5c07c41e62 Dont add haxeui if the platform doesnt use it.
Co-Authored-By: sector-a <82838084+sector-a@users.noreply.github.com>
2025-10-09 21:17:49 +08:00
MoonDroidandHundrec 9dd4791bb8 Adjust how auto-pause works on mobile
Made it so it doesnt freeze the game lol!
2025-10-09 21:13:12 +08:00
Karim AkraandHundrec 9a842207ac Update lime ref 2025-10-09 18:29:16 +08:00
MAJigsaw77andHundrec e139ae252f Properly format and fix a mem leak. 2025-10-09 18:29:16 +08:00
Karim AkraandHundrec fb8fd8b7d4 Fixed some issues with iOS 2025-10-09 18:29:16 +08:00
Karim AkraandHundrec 6bab076329 Final bugfix before testflight 2025-10-09 18:29:16 +08:00
sector-aandHundrec ec65cb3d27 Add a better playtest menu
fix spelling mistake

Bring back neccessary stuff

Fix input a bit

Bring back neccessary stuff AGAIN
2025-10-09 18:29:16 +08:00
Karim AkraandHundrec 96720e235f Reduce Reflect overhead in CallbackUtil 2025-10-09 18:29:16 +08:00
Karim AkraandHundrec c2b68dcd90 Android cleanup & some finalization 2025-10-09 18:29:16 +08:00
Karim AkraandHundrec 7a09f0c02c Huge chunk of the iOS implementation 2025-10-09 18:29:16 +08:00
Karim AkraandHundrec 1b0a0a476d Base Android implementation 2025-10-09 18:29:16 +08:00
HucksandHundrec 28883fb9ed nglogo behind title text
that pesky ng logo isn't in front of the title text anymore
2025-10-09 06:11:21 +08:00
Karim AkraandHundrec 1b38dab9aa Change the title of the game on mobile to "FNF" 2025-10-09 06:10:33 +08:00
EricandGitHub ab99cc6f74 Merge pull request #1836 from FunkinCrew/PurSnake/in-game-subtitles
[PUBLIC PR] SRTUtil in-game subtitles
2025-10-08 17:49:13 -04:00
PurSnakeandEric 8ba14b0e7b {} -> <> convert 2025-10-08 17:42:18 -04:00
PurSnakeandEric a7fc70a5c9 srt 2025-10-08 17:42:18 -04:00
PurSnakeandHundrec 204dae213c Move migration porcces into ChartEditor itself, when we load events data 2025-10-09 04:41:34 +08:00
PurSnakeandHundrec fdaab2b229 Reduce width for ENUM fields in events 2025-10-09 04:41:34 +08:00
PurSnakeandHundrec e67643e0b1 1 hour, 39 minutes: Convertation method for old charts with old events. No, im not gonna kill myself
I think it should be removed in one of 0.8.x patches or in 0.9
Also some verbose code as this commit is a bit big
2025-10-09 04:41:34 +08:00
PurSnakeandHundrec d1d77dd85c Event easing direction + backwards compatibility 2025-10-09 04:41:34 +08:00
kade-githubandHundrec 766bdbcd62 set the alpha to 0 as well 2025-10-09 04:10:18 +08:00
kade-githubandHundrec 7245553588 Fix notes appearing in Calibration 2025-10-09 04:10:18 +08:00
MAJigsaw77andAbnormal fddf0e92cb Fix hang caused by git status. 2025-10-08 04:24:24 -05:00
AbnormalPoofandHundrec 83c2ea2881 Also make vid nullable on html5 2025-10-08 17:18:46 +08:00
Hyper_andAbnormal c170c25693 always bet on... TWO LINE BANGERS?! 2025-10-08 01:29:09 -05:00
HundrecandAbnormal 03f6f90636 Another batch of charting fixes 2025-10-07 20:17:11 -05:00
EliteMasterEricandAbnormal bcc0a44979 Improved printing of Capsule events 2025-10-07 20:08:51 -05:00
EliteMasterEricandAbnormal 6c20b70a14 Fixes to the event dispatcher to make it more obvious if one isn't being handled properly. 2025-10-07 20:08:51 -05:00
EricandAbnormal c6bdd753df You have to make the event cancelable first. 2025-10-07 20:08:51 -05:00
AbnormalPoofandAbnormal 99ad396fa3 Take into account the confirm sound's length when fading out in the Game Over screen 2025-10-07 20:08:51 -05:00
AbnormalPoofandAbnormal cdb3acc691 Add multiple new callbacks for Freeplay and Character Select 2025-10-07 20:08:51 -05:00
KoloandHundrec 71b4a58cab make going into chart editor thru a song go to the current song pos 2025-10-07 19:12:55 +08:00
ImVeryBadandAbnormal 934bce7bdc Round NumberPreferenceItem value to prevent near-zero precision errors 2025-10-07 04:26:55 -05:00
gamerbrossandAbnormal dd5ea7748b Fix NoteStyle.getCountdownSoundPath library separator code 2025-10-07 04:08:42 -05:00
AbnormalandGitHub 4fef6dc3b6 Merge pull request #1798 from FunkinCrew/pr-5118/anysad/funkin-ui-null-safety 2025-10-07 03:49:36 -05:00
Herman KhomichandHundrec cb669335a3 oopsy (#7) 2025-10-07 05:09:21 +08:00
KoloandHundrec 68970b6716 make stickers not persist 2025-10-07 05:09:21 +08:00
EliteMasterEricandHundrec d80682333a Use cleaner array utilities rather than for loops. 2025-10-07 05:06:45 +08:00
LasercarandHundrec a8dec0cd70 Get last modified valid editor backup 2025-10-07 05:06:45 +08:00
sector-aandHundrec 6d0279fc37 Fix a line being too high 2025-10-07 04:58:23 +08:00
sector-aandHundrec f5fb39538c Assets and Art submods 2025-10-07 04:58:23 +08:00
sector-a 1b693b0752 Update assets submod 2025-10-06 22:45:16 +03:00
anysadandsector-a 4ca0e0deb5 fix shit after rebase 2025-10-06 21:21:09 +03:00
anysadandsector-a 26dbf918cc gosh, charselect is crazy 2025-10-06 21:21:09 +03:00
sector-aandHundrec 82adf357d2 Update function's documentation 2025-10-06 02:57:41 +08:00
sector-aandHundrec 8022643b8e Prevent death on time travelling 2025-10-06 02:57:41 +08:00
MAJigsaw77andHundrec 3a44168d8c Unused import 2025-10-06 01:31:30 +08:00
MAJigsaw77andHundrec 2818539686 Add this back 2025-10-06 01:31:30 +08:00
EliteMasterEricandHundrec 6ad0502cce Update videos to actually include subtitles. 2025-10-06 01:31:30 +08:00
MAJigsaw77andHundrec dccafc5e27 Fix compile. 2025-10-06 01:31:30 +08:00
EliteMasterEricandHundrec 1653839753 Fix the feature flag condition for video subtitles. 2025-10-06 01:31:30 +08:00
MAJigsaw77andHundrec 7e09c25f5b If videoSubtitles are disabled specifically tell libvlc to not load any. 2025-10-06 01:31:30 +08:00
MAJigsaw77andHundrec f7fef85353 Dont load the subtitles at runtime. 2025-10-06 01:31:30 +08:00
MAJigsaw77andHundrec 83667e6232 Remove the options. 2025-10-06 01:31:30 +08:00
MAJigsaw77andHundrec fa86e53b7d Restart the video when setting the subtitle track. 2025-10-06 01:31:30 +08:00
MAJigsaw77andHundrec e43ec564e0 Add back FunkinVideoSprite. 2025-10-06 01:31:30 +08:00
MAJigsaw77andHundrec a25233ccdc Add the ability to use video files other than mp4. 2025-10-06 01:31:30 +08:00
MAJigsaw77andHundrec a3a75b89b9 Only add the option if its defined. 2025-10-06 01:31:30 +08:00
MAJigsaw77andHundrec 8733afe512 Added the option and the define for subtitles. 2025-10-06 01:31:30 +08:00
MAJigsaw77andHundrec 6059f21f2d [WIP] Basic subtitles support. 2025-10-06 01:31:30 +08:00
Karim AkraandHundrec 02d9a507b4 Generate every logfile with a specific timestamp 2025-10-06 00:54:35 +08:00
sector-aandMoonDroid d0a9a59de7 Add a separated camera for the Pause Menu 2025-10-05 21:23:33 +07:00
Karim AkraandKade 6dc5ac826e Fixed the log file causing the game to crash when opening multiple windows at the same time 2025-10-04 19:05:27 -07:00
AbnormalPoofandAbnormal 807b9ab09f clear out assets folder from export/ when compiling 2025-10-02 14:31:37 -05:00
MoonDroidandHundrec 45fc68f08a Update lime ref 2025-10-02 12:41:25 +08:00
Hundrec 18f2334ddd Bump funkVis and grig.audio 2025-10-02 03:20:12 +08:00
FuroandHundrec 6607645786 Fixed Memory Leaks on funkVis and grig.audio
The memory leak was due to not caching the audio levels and these libraries allocating arrays on every frame, it has been fixed in their each repository through a pull request.
2025-10-02 03:20:12 +08:00
KoloandAbnormal 9497378600 it's another what?? ONE LINER 🗣🗣
Fixes the bug where going into a song through the story menu then exiting forcefully via the F4 keybind (which moves you to the main menu but doesn't clear out the PlayStatePlaylist) and then going into the Chart Editor to playtest a song moving you further along the week once the song reaches the end. What a mouthful.
2025-10-01 13:25:49 -05:00
PurSnakeandMoonDroid 4215b1c72e Render Transition Stickers on OpenFL Sprite 2025-10-01 20:55:17 +07:00
4300aff991 Bump polymod with the latest commits
Co-authored-by: MoonDroid <81515012+moondroidcoder@users.noreply.github.com>
2025-10-01 19:31:26 +07:00
AwesomezPro99andMoonDroid ca88b00d95 Fixed Random! song fade 2025-10-01 18:36:54 +07:00
Cameron TaylorandKade e3a5f329ad some spring cleaning on AtlasText.hx
these shouldn't affect functionality at all, just some things to make haxe checkstyle a bit less annoying and other lil cleanings

Merge branch 'master' into atlas-text-tidy

more AtlasText cleaning
2025-09-30 14:22:28 -07:00
Karim AkraandHundrec 42916a8b6a bump admob to fix a crash from ads preloading 2025-10-01 05:17:02 +08:00
MAJigsaw77andHundrec 12ed0107ab Oops 2025-10-01 05:17:02 +08:00
MAJigsaw77andHundrec 6016ee966e Load interstitial ADS more efficiently on Android. 2025-10-01 05:17:02 +08:00
Noobz4LifeandHundrec 555ec09ef5 Feral gamemode support 2025-10-01 05:07:49 +08:00
KoloandHundrec b30ea36fcc the evil and intimidating haxeflixel icon 2025-10-01 05:00:09 +08:00
Hyper_andHundrec 4c0d92507a Disable Haxe UI autolocalization
This is an English-only game (for now)
2025-10-01 04:59:16 +08:00
KoloandHundrec 8421e4c150 super minor fix for levels i noticed randomly lol 2025-10-01 04:58:14 +08:00
AwesomezPro99andHundrec be3a6dee93 Fix onion skin global offsets and crash 2025-10-01 04:49:59 +08:00
AwesomezPro99andHundrec a76a868cb6 Fix anim editor onion skin scaling 2025-10-01 04:49:59 +08:00
Hundrec 707d519110 Bump assets 2025-10-01 00:10:42 +08:00
Hundrec a93e8a9f10 Add useCache argument to getBitmapData 2025-10-01 00:10:42 +08:00
Hundrec 65da3049ba Don't cache DropShadowShader
False...
2025-10-01 00:10:42 +08:00
AbnormalandMoonDroid 4a00429a57 feat: Enable time travel for playtesting 2025-09-30 18:46:45 +07:00
da6303a0b0 Fix DropShadowShader not searching for astc textures when setting the mask
Co-authored-by: MoonDroid <81515012+moondroidcoder@users.noreply.github.com>
2025-09-30 18:26:06 +07:00
AwesomezPro99andHundrec 96f8ee85e0 Fix char select static pixel icon crash 2025-09-30 14:12:01 +08:00
PurSnakeandHundrec 9f9a6924a7 Fix frame-snap and pause/unpause 2025-09-29 22:00:41 +08:00
PurSnakeandHundrec 8e6fa51c27 un-fps your icons *evil ass laugth* 2025-09-29 22:00:41 +08:00
AwesomezPro99andHundrec 047187257b Hide cursor when playtesting chart 2025-09-29 18:26:22 +08:00
VioletSnowLeopardandHundrec 2f77ccfc6e Save preferences when exiting the Chart Editor 2025-09-29 18:14:47 +08:00
ACrazyTownandHundrec d19245c23d Improve message depending on the target platform 2025-09-29 17:58:21 +08:00
ACrazyTownandHundrec 52703536a6 Provide a nicer error message when software rendering is used 2025-09-29 17:58:21 +08:00
Hundrec c5dc625550 Bump those assets 2025-09-29 17:42:33 +08:00
Hyper_andHundrec 6405fe6bb3 ...danrell 2025-09-29 17:42:33 +08:00
Hyper_andHundrec c7367ae33d fix: Also take '[' bracket into account when sanitizing JSON 2025-09-29 17:42:33 +08:00
LasercarandHundrec 1df9632e31 Better time change placement and ms label after TS 2025-09-29 16:36:17 +08:00
Hundrec 94a1ec3902 Batch of charting fixes
- Added the new No Animation notekind when certain animations play
- Adjusted precise timings of some playAnimation events to match the audio
- Fixed some issues reported on GitHub
- Implemented some suggestions reported on GitHub

Check the 0.7.6 Milestone to see which ones were fixed!
2025-09-29 16:30:01 +08:00
LasercarandHundrec c05c0160c3 Render the measuring stick before loading a chart 2025-09-28 15:20:10 +08:00
HundrecandAbnormal 6193aa2faa Nuh uh gimme back that False...
Fixes input offsets being applied in the Chart Editor after a refactor
2025-09-27 14:15:02 -05:00
LasercarandAbnormal ca79c71851 Fix ghost notes having a zindex below measure ticks 2025-09-27 13:19:21 -05:00
MAJigsaw77andHundrec d83149dc70 FIx html5 compile. 2025-09-28 01:38:50 +08:00
LasercarandHundrec 2c4167bc11 Shorten playbar text when too l... 2025-09-28 00:27:51 +08:00
mintandHundrec 117f3be390 fix(ci): move env vars to .env 2025-09-28 00:25:35 +08:00
mintandHundrec b81035f9bf fix(ci): add newground creds to env 2025-09-28 00:25:35 +08:00
8b38bcfbdd Freeplay DJ render types and softcoded TV anim
Added support for renderTypes such as: Sparrow, Multisparrow and Packer.

Added support for Scriptable DJ's

BF's TV logic softcoded now (wow)

Added pathSafety parameter on creating new FlxAtlasSprite.

Co-Authored-By: Abnormal <86753001+AbnormalPoof@users.noreply.github.com>
2025-09-27 20:03:43 +08:00
sector-aandHundrec f47de1b71c Add an ability to set Debug Display using a hotkey 2025-09-27 19:45:16 +08:00
kade-githubandHundrec 61f2ea3511 fix FUNK-3429 2025-09-27 18:55:39 +08:00
mintandHundrec 9169beaa12 fix(ci): gather-changes errors if no commits in payload 2025-09-27 18:52:56 +08:00
mintandHundrec 9f0041b196 ci: update to github-script@v8 2025-09-27 18:52:56 +08:00
KeoikiandHundrec 6cc35815ba Fix Song ID field accidentally being removed. 2025-09-27 03:57:37 +08:00
KeoikiandHundrec c88dfb4947 Bump to latest assets 2025-09-27 03:57:37 +08:00
KeoikiandHundrec a27149aa70 Fix a few issues from conflict merging. 2025-09-27 03:57:37 +08:00
KeoikiandHundrec 8e9dea898f Fix "Live Input" note positioning being wack
The Live Input positioning now uses the same snapping math that the cursor placement does. (Why wasn't this the case before??)
2025-09-27 03:57:37 +08:00
KeoikiandHundrec 73d3c27c25 Fix 2 issues + add numerators 17 to 32
- Fixed Skip Forward and Backwards buttons, they now properly set the position to the start of a measure.
- Fixed having the playhead (the red thing) at any position than the default would cause the song time to be wrong when passing through denominator changes.
2025-09-27 03:57:37 +08:00
KeoikiandHundrec b9eeebdbea Fix metronome measure beeps. 2025-09-27 03:57:37 +08:00
KeoikiandHundrec 1f33aec825 FIX MEASURE TICKS GRAAAAAAAAAAAHH!!!!!! 2025-09-27 03:57:37 +08:00
LasercarandHundrec 2d891a4248 Refresh time change inputs 2025-09-27 03:57:37 +08:00
LasercarandHundrec e533c87d08 Final fix (I hope) 2025-09-27 03:57:37 +08:00
LasercarandHundrec c5f5fdb089 Whoops 2.0 2025-09-27 03:57:37 +08:00
LasercarandHundrec 87fdb84202 Whoops. 2025-09-27 03:57:37 +08:00
LasercarandHundrec d78ead2927 Refactor time change commands, add @AbnormalPoof suggestion 2025-09-27 03:57:37 +08:00
LasercarandHundrec a4148102eb Fix crash, dropdown text not updating and timestamp.step is step
Timestamp step steps in step now

Nearly forgot to include the assets in this.
2025-09-27 03:57:37 +08:00
LasercarandHundrec 69affd43bb Forgot to remove these traces 2025-09-27 03:57:37 +08:00
LasercarandHundrec 1dce731aa4 Modify time changes in the metadata toolbox 2025-09-27 03:57:37 +08:00
KeoikiandHundrec f3c0541ade Fix note preview lines being in incorrect places.
Fun fact: This already affected "Monster" in current release versions and wasn't something caused by me.
2025-09-27 03:57:37 +08:00
KeoikiandHundrec eb40d3bda5 Fix measure number values and positions
(and remove 2 comments I forgot to remove last time)
2025-09-27 03:57:37 +08:00
KeoikiandHundrec 533c8f7ede Remove unused variables, I forgor 💀 2025-09-27 03:57:37 +08:00
KeoikiandHundrec b29339cd1a Several things. (Check description)
- Fix "beat time" when dealing with time signatures.
- Remove old, now unused stuff from previous commit in the Conductor.
- Overhaul getTimeInSteps() and getStepTimeInMs() to be a cumulative calculation.
2025-09-27 03:57:37 +08:00
KeoikiandHundrec 814bc4a3bf just to get this pushed i guess 2025-09-27 03:57:37 +08:00
KeoikiandHundrec b05a5c7f54 Changes from the old branch. 2025-09-27 03:57:37 +08:00
EliteMasterEricandHundrec eba71982c6 Also validate ADMOD_APPID in the project file. 2025-09-26 13:47:59 +08:00
EliteMasterEricandHundrec 4a6c81b136 Require the .env file include ad keys for release builds.
REMEMBER: You can build with `-debug` or with `-DNO_FEATURE_MOBILE_ADVERTISEMENTS` to skip this check.
2025-09-26 13:47:59 +08:00
EliteMasterEricandHundrec 4f671f1e5b Add some utility functions for working with constants from macros. 2025-09-26 13:47:59 +08:00
Karim AkraandHundrec 01a076b6ed Fix incorrect vars names on admob null checks 2025-09-26 04:25:23 +08:00
Karim AkraandHundrec f0f3e65bb0 fixed an oopsie in RegistryMacro 2025-09-26 03:44:22 +08:00
Karim AkraandHundrec 2398fabffb Fixed the build cache failing for iOS 2025-09-26 03:44:22 +08:00
EliteMasterEricandAbnormal a03c93b006 The chart editor now uses the vocal data from the metadata rather than hardcoding to the character IDs. NOTE: This is correct but it breaks voices on some charts, we need some way of allowing users to easily correct messed up vocals. 2025-09-25 06:01:30 -05:00
EliteMasterEricandAbnormal 5c78c5ba2c Errors display a popup rather than crashing now. 2025-09-25 06:01:30 -05:00
Karim AkraandAbnormal c8d0ae6a9d Reset camera zoom as well 2025-09-25 06:01:30 -05:00
EliteMasterEricandAbnormal 3867e4bdca This should keep the text visible after playtesting. 2025-09-25 06:01:30 -05:00
EliteMasterEricandAbnormal bdad46e96d Fix a bug where variations weren't accounted for with chart variations. 2025-09-25 06:01:30 -05:00
EliteMasterEricandAbnormal d03199ccda Implement a debug menu to select difficulty/variation 2025-09-25 06:01:30 -05:00
EliteMasterEricandAbnormal 0c4f22173e Created a function to playtest a song directly from its FNFC file (TODO: difficulty select) 2025-09-25 06:01:30 -05:00
Cameron TaylorandAbnormal 17bafc2b5e cleanup on DebugDisplayMode enum abstract so we don't need to cast/convert to string all the time 2025-09-25 04:19:19 -05:00
EliteMasterEricandHundrec 666aaf4428 Restructred note kinds a bit to add hard-coded note kinds, then implemented the "noanim" note kind into the chart editor. 2025-09-25 16:45:50 +08:00
EviethecoderandHundrec 618e093e1c character sing overides and no anim notes 2025-09-25 16:45:50 +08:00
AwesomezPro99andAbnormal 2f865a5c7f Fix script create event 2025-09-24 23:24:02 -05:00
JVN-PixelsandAbnormal aaf5084fd9 Freeplay Previews - Fading Out 2025-09-24 17:00:34 -05:00
KoloandHundrec 44c14ec4ce fortunately the fix for hundrecs issue was just moving the line earlier 2025-09-25 01:28:47 +08:00
KoloandHundrec 66882d9677 kill off the cover brah 2025-09-25 01:28:47 +08:00
KoloandHundrec bc1d36ddea one liner number three billion 2025-09-24 22:39:36 +08:00
JVN-PixelsandHundrec 2d5ef19b3b Chart Editor - Remaining Time Milliseconds 2025-09-24 21:32:56 +08:00
AwesomezPro99andHundrec a61016d2df Fix Animation Editor crash 2025-09-24 21:03:00 +08:00
AwesomezPro99andHundrec f2f6b4c859 Fix cursor visibility when exiting Anim Editor 2025-09-24 20:54:17 +08:00
EricandGitHub 9f92d64641 Merge pull request #1761 from FunkinCrew/Hyper/debug-logging-crash
[PUBLIC PR] Prevent crash when closing certain states on debug builds
2025-09-23 20:30:10 -04:00
EricandGitHub bb71dcba91 Merge pull request #1759 from FunkinCrew/VioletSnowLeopard/stop-test-on-exit
[PUBLIC PR] Fix Offset Calibration/Test starting after exiting the Lag Adjustment menu
2025-09-23 20:29:50 -04:00
VioletSnowLeopardandEric 23a38f76f9 Don't start calibration/test if no longer in offset menu 2025-09-23 20:29:31 -04:00
Hyper_andEric 9a8bda8b11 this should be running last... 2025-09-23 19:50:40 -04:00
EricandGitHub 3c10cf5181 Merge pull request #1757 from FunkinCrew/hmm/update-haxeui-core
Update `haxeui-core`.
2025-09-23 19:50:22 -04:00
EricandGitHub c5095e0a8d Merge branch 'master' into hmm/update-haxeui-core 2025-09-23 19:49:07 -04:00
HeroandHundrec 3486fe3dde refactor: cleaner and faster time formatter 2025-09-24 07:44:46 +08:00
MAJigsaw77andEric 56fe047e3a Update haxeui-core. 2025-09-23 19:38:35 -04:00
AwesomezPro99andHundrec 26b253d8ca Fix save data options back 2025-09-24 01:16:21 +08:00
AbnormalandHundrec 3f85f1dd70 Bump hscript to fix null coalescing assignment (??=) 2025-09-20 16:31:19 +08:00
AbnormalandHundrec dad829dd91 bump hxcpp to fix a build error with macos 26 2025-09-19 03:20:52 +08:00
HundrecandGitHub e62c5a223c Merge to sync commit histories
Build and Upload nightly game builds / gather-changes (push) Canceled after 0s
Build and Upload nightly game builds / gather-tags (push) Canceled after 0s
Build and Upload nightly game builds / build-game-on-host (macos, macos) (push) Canceled after 0s
Build and Upload nightly game builds / build-game-on-host (windows, windows) (push) Canceled after 0s
Build and Upload nightly game builds / docker-image (push) Canceled after 0s
Build and Upload nightly game builds / build-game-in-container (html5) (push) Canceled after 0s
Build and Upload nightly game builds / build-game-in-container (linux) (push) Canceled after 0s
2025-09-19 01:33:29 +08:00
Hundrec 5fc7491121 Merge changes from main 2025-09-19 01:28:01 +08:00
Hundrec 9598455118 KoloInDaKrib 2025-09-19 01:27:42 +08:00
Nightfall-16andHundrec 0e4b9753a0 contribute I guess 2025-09-19 01:26:12 +08:00
Nightfall-16andHundrec 2bbe3d788d crash 2025-09-19 01:26:12 +08:00
Nightfall-16andHundrec cd731d7e40 work 2025-09-19 01:26:12 +08:00
Nightfall-16andHundrec e11e3d51c2 please work 2025-09-19 01:26:12 +08:00
Nightfall-16andHundrec b19b78df5d please work now 2025-09-19 01:26:12 +08:00
Hundrec 1a061cbbcc 0.7.5 changelog and version bump
Set the date for the 0.7.5 patch
2025-09-18 16:24:18 +08:00
MoonDroid 9908c8be32 Fix html5 builds from crashing and not compiling 2025-09-18 14:55:52 +07:00
MoonDroid 8522deab64 Adjust warning message 2025-09-18 14:55:52 +07:00
MoonDroid 7e93521666 Add custom context warnings for environment errors
woo!! im proud of this !!
2025-09-18 14:55:52 +07:00
MoonDroidandMihai Alexandru 5aeb3a28fb Add AnsiUtil for ease of ansi use
Co-Authored-By: Mihai Alexandru <77043862+MAJigsaw77@users.noreply.github.com>
2025-09-18 14:55:52 +07:00
MAJigsaw77andMoonDroid b5db93726a Rewrite Environment macro. 2025-09-18 14:55:52 +07:00
EliteMasterEricandHundrec 20de8db41b Update to the latest commit on the hxcpp-debugger fork. 2025-09-18 14:43:08 +08:00
Hundrec 5d73d65fdc Fix Eggnog Erect cutscene crash 2025-09-18 03:03:31 +08:00
8c750d368d Update openfl and flixel for ASTC fixes.
Co-authored-by: MoonDroid <81515012+moondroidcoder@users.noreply.github.com>
2025-09-18 01:44:25 +07:00
Hundrec 8d328ace84 Fix HTML5 compile error 2025-09-18 01:03:00 +08:00
MAJigsaw77andHundrec 9d825f00aa Use Sys.println instead of trace` for Admob events. 2025-09-16 22:56:11 +08:00
Honton129andAbnormal c7b68507a8 Typo Fix on PlayStatePlaylist
that was bothering me
2025-09-15 05:08:37 -05:00
Hyper_andHundrec 970ccf2f4a fix: Chart loading fixes
* Prevents charter from deleting `default` variation when there's more than one variation available.
* Fixes song variation list not being saved into `default` metadata file.
* Fixes chart data not being assigned its respective variation when loading from FNFC.
2025-09-15 03:12:43 +08:00
PixelandHundrec 479036a400 Add ability to pause during Credits scroll 2025-09-15 03:09:33 +08:00
KoloandHundrec 80d7ace714 remove the stupid chud gibberish 2025-09-15 03:06:44 +08:00
Hundrec a7d5736e14 Adjust Stage Editor exit confirm prompt 2025-09-15 03:02:15 +08:00
LasercarandHundrec b31be1043c chart name in backup filename 2025-09-15 03:02:14 +08:00
Hundrec 6c4d27a842 Nudge Chart Editor menubar for new debug display 2025-09-15 02:55:39 +08:00
Hundrec fdab26ce56 Fix Pico not shooting in Stress upon restarting 2025-09-15 02:26:22 +08:00
VioletSnowLeopardandHundrec 8ead5eeb08 change save shortcuts to CTRL+S and CTRL+SHIFT+S 2025-09-15 02:18:53 +08:00
Hundrec c4ebd3175d Remove beatHit() flash from Freeplay backing card
It's a cool idea, but too distracting, especially for Pico's blue backing card
2025-09-15 02:13:45 +08:00
e593bc8a61 Fix Debug Display memory leak on Linux
Co-authored-by: Linus Torvalds <torvalds@linux-foundation.org>
2025-09-15 02:03:55 +08:00
Hyper_andHundrec cb554bd5f7 the one line fix 2025-09-15 01:59:37 +08:00
Hyper_andHundrec dd54acfbd1 this pointless condition was pissing me off 2025-09-15 01:59:37 +08:00
LasercarandHundrec 6783f04139 No Chudding allowed 2025-09-15 01:56:03 +08:00
PurSnakeandHundrec 11eb4060d0 duh 2025-09-15 01:56:03 +08:00
VioletSnowLeopardandHundrec b5357acf98 Fallback to default if metadata voices don't exist 2025-09-15 01:50:09 +08:00
Hyper_andHundrec f85f2f03d9 another one liner god damn it
Fixes a crash when there's some previously focused text fields,
and the user tries to exit via the confirmation dialog.
Before a state switch HaxeUI tries to reset the tooltip manager and
remove all components, which in this case causes it to refocus a text field,
which tries to access a destroyed camera to retrieve the bounds.
Doing this ensures there's no text fields for it to do that.
2025-09-15 01:41:38 +08:00
VioletSnowLeopardandHundrec 146a6cde68 Fix typo - pause *button* 2025-09-14 18:23:41 +08:00
AbnormalandHundrec 65105df9ac disable smoothing on the baked bitmaps 2025-09-14 17:31:01 +08:00
AbnormalandHundrec 9106ef215a add the lighting to pico's rank animation (had to bake the blendmode lol) 2025-09-14 17:31:01 +08:00
Megalives6193andHundrec 38426bc5d7 [GITHUB] Fix Logo In README.md 2025-09-14 04:35:22 +08:00
KoloandHundrec 79e8b5d71e fix: Prevent crashing by replacing the state constructor if needed 2025-09-14 03:14:38 +08:00
MoonDroidandHundrec 6e954cfac0 Update astc compression rate for results excellent bf 2025-09-13 21:56:48 +08:00
MAJigsaw77andAbnormal 041d33b2d3 Fixes to the audio config stuff. 2025-09-12 16:26:15 -05:00
MoonDroid 1b5b0925ed chore: Rename the define and remove redundant block 2025-09-11 03:39:25 +07:00
Karim AkraandMoonDroid cd0bc85b76 added FEATURE_LOG_TRACE_FILE for logging all the logs into a text file 2025-09-11 03:39:25 +07:00
MAJigsaw77andHundrec d4929164b6 Update hxvlc. 2025-09-10 12:35:35 +08:00
Abnormal 7bc06861b5 add an extra line 2025-09-09 12:39:19 -05:00
Abnormal 3ccbed2cbb clarify dev build in outdated libraries 2025-09-09 12:39:19 -05:00
MAJigsaw77andAbnormal 29c0b0f8eb Add support for dev libs in project.hxp haxelibs check. 2025-09-09 12:39:19 -05:00
AbnormalandEric 7ff0753472 scripted freeplay and popups shot dead on august 32nd, 2031 with no traces found 2025-09-09 05:35:35 -04:00
AbnormalandEric 19328abb9e and then there were 7 2025-09-09 05:35:35 -04:00
AbnormalandEric feb0863358 and then there were 8 2025-09-09 05:35:35 -04:00
AbnormalandEric 43e51e9a4a erm......sorry modders you're gonna have to do the work yourself lol 2025-09-09 05:35:35 -04:00
AbnormalandEric 3b610d8910 add a TON of modding stuff 2025-09-09 05:35:35 -04:00
PurSnakeandHundrec 63ec749c7a Disable Freeplay intro when coming from stickers 2025-09-09 03:53:51 +08:00
Hyper_andHundrec c8e6a78154 don't check libraries on display 2025-09-09 03:45:48 +08:00
Az989YTandHundrec 89b2b7fcc3 change event dispatching to match comment order 2025-09-09 03:14:55 +08:00
kade-githubandAbnormal 1b1c781426 bump polymod 2025-09-08 14:11:42 -05:00
kade-githubandEric 5c8a888b77 remove includes that dont matter 2025-09-08 15:00:28 -04:00
kade-githubandEric 257576b4d3 Allow for scripted state reloading 2025-09-08 15:00:28 -04:00
sector-aandEric 56f0a7780d Debug Display HTML5 Fixes 2025-09-08 13:02:48 -04:00
MAJigsaw77andEric faa47e8f3e More tweaks 2025-09-08 13:02:48 -04:00
MAJigsaw77andEric b99d599bce Fix iOS compile. 2025-09-08 13:02:48 -04:00
92bc856f5b Fix macos compile.
Co-Authored-By: MoonDroid <81515012+moondroidcoder@users.noreply.github.com>
2025-09-08 13:02:48 -04:00
sector-aandEric 0490ca8145 Debug display bg opacity preference 2025-09-08 13:02:48 -04:00
sector-aandEric c9b8f21f61 Debug Display modes!!! 2025-09-08 13:02:48 -04:00
MAJigsaw77andEric 60caa80a84 One more graph adjustment 2025-09-08 13:02:48 -04:00
MAJigsaw77andEric 477f85a71e Adjust graphs width. 2025-09-08 13:02:48 -04:00
MAJigsaw77andEric b5aed5fb4f Adjusted the graphs. 2025-09-08 13:02:48 -04:00
MAJigsaw77andEric 7d714ecfeb Add MemoryUtil for Apple external. 2025-09-08 13:02:48 -04:00
MAJigsaw77andEric f3ff132e90 Adjust the external API a little. 2025-09-08 13:02:48 -04:00
MAJigsaw77andEric 1912155b32 Organized some small stuff 2025-09-08 13:02:48 -04:00
sector-aandEric daa87c8c58 The new debug display is mostly done! 2025-09-08 13:02:48 -04:00
sector-aandEric ad87832662 The graph is done 2025-09-08 13:02:48 -04:00
MAJigsaw77andEric b8b0a5bf73 Added task mem and adjusted the counter. 2025-09-08 13:02:48 -04:00
sector-aandEric 5a8b681e2f FPS Graph (unfinished) 2025-09-08 13:02:48 -04:00
sector-aandEric 5a6a3f5599 Small tweaks 2025-09-08 13:02:48 -04:00
sector-aandEric bab422029b FunkinCounter (unfinished) 2025-09-08 13:02:48 -04:00
Hyper_andHundrec 88fc5f9ed6 refactor: use null-safe list functions in Medals and Leaderboards 2025-09-08 22:45:32 +08:00
Hundrec f3c7cfd799 Hide the A-Bot visualizer when restarting 2025-09-08 17:10:38 +08:00
Hundrec 231439d808 Adjust haxelib warning message 2025-09-08 16:51:37 +08:00
AbnormalandHundrec d6f80730a5 check the library versions and warn the user of any outdated ones 2025-09-08 16:51:37 +08:00
AwesomezPro99andHundrec 9cfee0398a Fix stress pico bricks 2025-09-07 14:29:39 +08:00
Hyper_andAbnormal aa5956a30b Force texture to be uploaded to the GPU
Fixes an issue where multi-sparrow characters cause a stutter when the alternative sprites are displayed
2025-09-06 22:15:28 -05:00
Hundrec b9d68957e0 Cache Stress Tankmen into FunkinMemory 2025-09-07 02:37:57 +08:00
kade-githubandHundrec 1b9f5c62fb Fix a bug with Lag Adjustment strumline 2025-09-07 02:23:59 +08:00
kade-githubandHundrec a0a57ca94d Make strumlineNotes and noteHoldCovers public 2025-09-07 02:23:59 +08:00
sector-aandHundrec 271451b9df Update assets 2025-09-07 01:39:13 +08:00
sector-aandHundrec 21458f26f2 Fix dialogue proceeding when pressing the pause button 2025-09-07 01:39:13 +08:00
kade-github c1b4d4d53e space... STATE_CREATE 2025-09-06 09:41:24 -07:00
kade-github 28d71dc2fe add onStateCreate to Module 2025-09-06 09:41:24 -07:00
AwesomezPro99andHundrec a10bbf60ac Fix menu music fade in 2025-09-06 16:37:08 +08:00
KoloandHundrec 4e572b171f cancel login if ongoing 2025-09-06 16:15:36 +08:00
Hundrec cc768c83aa Remove extra line 2025-09-06 16:09:06 +08:00
LasercarandHundrec 035f3770d5 No more audio playback while dragging playheads
This commit specifically fixes the note preview playhead.
2025-09-06 16:09:06 +08:00
LasercarandHundrec 2d95754d49 Chart editor audio playback while dragging playheads fix
Two down, one to go
2025-09-06 16:09:06 +08:00
VioletSnowLeopardandHundrec 47b1cee6d5 don't open welcome dialog multiple times 2025-09-06 15:59:38 +08:00
lemz1andHundrec 4ebd00bc06 haxe ui focus check 2025-09-06 15:40:00 +08:00
AwesomezPro99andHundrec 66554ec4b6 Fix backing card beat hit check 2025-09-06 15:22:24 +08:00
MAJigsaw77andHundrec bfc798c629 Update extension-admob. 2025-09-06 15:11:46 +08:00
EliteMasterEric 6b334b941a Add some import aliases so scripts don't break. 2025-09-05 17:16:54 -04:00
sector-aandEric b06090a477 Move CharacterData to data/character 2025-09-05 17:16:54 -04:00
VioletSnowLeopardandHundrec ca5fbb65e5 add *the* checkbox to crash template 2025-09-06 05:08:28 +08:00
KoloandEric c2305f32f0 this bug seems kinda old why has nobody reported it before 2025-09-05 15:51:55 -04:00
VioletSnowLeopardandEric 747446e2e4 rank anim only plays when changing difficulties 2025-09-05 14:15:56 -04:00
MrMaderaandEric 250d2188b9 Add ability to press back to exit Character Select 2025-09-05 13:23:51 -04:00
kade-github 61c710fe20 some fixes and goodness 2025-09-05 09:45:56 -07:00
KoloandKade 0fd6f5b3b7 that fucking ghost that i hate 2025-09-05 09:45:56 -07:00
HundrecandAbnormal 8a204c606a Bring back Freeplay rank slamming animation
Another Freeplay fix caused by the same commit!

Manually reverts changes from https://github.com/FunkinCrew/Funkin/commit/f7a48bcb3b9ab0f5393552b828b8687c6ae6926c where Cam converted the FreeplayRank class into a FlxSpriteGroup.
2025-09-04 14:04:28 -05:00
MAJigsaw77andMoonDroid 4401ea0c9a Organize ios project folder a little. 2025-09-04 13:32:06 +07:00
sector-aandMoonDroid 12ace54b2b Update assets 2025-09-04 13:15:58 +07:00
f15cd73dff revert changes to cache in flxpartialsound
Co-authored-by: MoonDroid <81515012+moondroidcoder@users.noreply.github.com>
2025-09-04 10:49:40 +07:00
b39d7bb5a0 Don't log info if the command is clean (or display)
Co-authored-by: MoonDroid <81515012+moondroidcoder@users.noreply.github.com>
2025-09-04 10:12:20 +07:00
f1066a2b0d Fix Freeplay capsule offsetting once and for all
And nudge the Gold Perfect sparkles to where they used to be in 0.5.3

Both of these issues were introduced in 0.6.0 and have finally been uprooted. Huzzah!

Co-Authored-By: Lasercar <64717068+lasercar@users.noreply.github.com>
Co-Authored-By: Kolo <67389779+koloindacrib@users.noreply.github.com>
Co-Authored-By: Abnormal <86753001+AbnormalPoof@users.noreply.github.com>
Co-Authored-By: Hyper_ <40342021+nothyper-474@users.noreply.github.com>
Co-Authored-By: Linus Torvalds <1024025+torvalds@users.noreply.github.com>
2025-09-03 18:44:24 -05:00
MAJigsaw77andHundrec b04b555b38 Intialize hxvlc asynchronously. 2025-09-04 04:28:02 +08:00
MoonDroid f349eedc72 fix: Re-add backbutton transitioning 2025-09-03 18:28:19 +07:00
VioletSnowLeopardandMoonDroid ee2f3c32eb Fixes for the recent update 2025-09-03 18:28:19 +07:00
LasercarandMoonDroid 0f99a1c7c2 Fix for https://github.com/FunkinCrew/Funkin/issues/3627 2025-09-03 18:28:19 +07:00
LasercarandMoonDroid 95aba09cd6 Fix target variation
Also no need to do instrumental stuff if not random
2025-09-03 18:28:19 +07:00
VioletSnowLeopardandMoonDroid 811b9e3343 don't freeze game if it fails to find target song/difficulty 2025-09-03 18:28:19 +07:00
VioletSnowLeopardandMoonDroid 37b9823b36 minor fix
Don't allow letter sort input while the instrumental selector is up
2025-09-03 18:28:19 +07:00
VioletSnowLeopardandMoonDroid 555684afda choose if you want the instrumental to be random 2025-09-03 18:28:19 +07:00
VioletSnowLeopardandMoonDroid 0d5c6ac963 selecting random can choose a random available instrumental 2025-09-03 18:28:19 +07:00
Cameron TaylorandMoonDroid db67a6928c ignroe VarTypeHint since it's a bit incompatible with final 2025-09-03 17:35:24 +07:00
Cameron TaylorandMoonDroid 9c34ab5f9c checkstyle fixins for AnsiTrace.hx 2025-09-03 17:35:24 +07:00
Cameron TaylorandMoonDroid f09074b79d add checkstyle regex to ignore long lines if they are strings (w/ whitespace) 2025-09-03 17:35:24 +07:00
Cameron TaylorandMoonDroid a5842fbe3f some checkstyle changes 2025-09-03 17:35:24 +07:00
8db654e63d Add support for 16K page sizes on Android.
Co-authored-by: MoonDroid <81515012+moondroidcoder@users.noreply.github.com>
2025-09-03 17:11:36 +07:00
MoonDroid 60144c0dc7 refactor: make time formatter extensible and easier to read 2025-09-03 16:12:59 +07:00
MAJigsaw77andMoonDroid 830b5dfd71 Properly formatted build time. 2025-09-03 16:12:59 +07:00
MAJigsaw77andHundrec 12d81f0f3b Fix setting the window fullscreen from preloader. 2025-09-03 16:56:23 +08:00
sector-aandMoonDroid 8c835447c8 Improved positioning of the Dialogue Box and the Portrait 2025-09-03 06:32:53 +07:00
425 changed files with 18817 additions and 15795 deletions
+3 -3
View File
@@ -48,19 +48,19 @@ body:
attributes:
label: Mobile Device Model
description: (Mobile users only) What mobile device are you playing on?
placeholder: ex. iPhone 16, Galaxy S25, iPad 11th Gen
placeholder: ex. iPhone 17, Galaxy S25, iPad 11th Gen
- type: input
attributes:
label: Mobile OS Version
description: (Mobile users only) What version is your Operating System?
placeholder: ex. iOS 18.5, Android 15, iPadOS 18.5
placeholder: ex. iOS 26.2, Android 16, iPadOS 26.2
- type: input
attributes:
label: Version
description: Which version are you playing on? The game version is in the bottom left corner of the main menu.
placeholder: ex. 0.7.4
placeholder: ex. 0.8.1
validations:
required: true
+1 -1
View File
@@ -36,7 +36,7 @@ body:
attributes:
label: Version
description: Which version are you playing on? The game version is in the bottom left corner of the main menu.
placeholder: ex. 0.7.4
placeholder: ex. 0.8.1
validations:
required: true
+1 -1
View File
@@ -36,7 +36,7 @@ body:
attributes:
label: Version
description: Which version are you compiling? The game version is in the bottom left corner of the main menu or in the project.hxp file.
placeholder: ex. 0.7.4
placeholder: ex. 0.8.1
validations:
required: true
+4 -3
View File
@@ -15,6 +15,7 @@ body:
- label: I have read the [Contributing Guide](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md)
- label: I have checked the Issues/Discussions pages to see if my issue has already been reported
- label: I have properly titled my issue
- label: I did not have any mods installed when I encountered this issue
- type: dropdown
attributes:
@@ -48,19 +49,19 @@ body:
attributes:
label: Mobile Device Model
description: (Mobile users only) What mobile device are you playing on?
placeholder: ex. iPhone 16, Galaxy S25, iPad 11th Gen
placeholder: ex. iPhone 17, Galaxy S25, iPad 11th Gen
- type: input
attributes:
label: Mobile OS Version
description: (Mobile users only) What version is your Operating System?
placeholder: ex. iOS 18.5, Android 15, iPadOS 18.5
placeholder: ex. iOS 26.2, Android 16, iPadOS 26.2
- type: input
attributes:
label: Version
description: Which version are you playing on? The game version is in the bottom left corner of the main menu.
placeholder: ex. 0.7.4
placeholder: ex. 0.8.1
validations:
required: true
+12 -2
View File
@@ -35,14 +35,14 @@ jobs:
docker:
- '.github/workflows/build-game.yml'
- '**/Dockerfile'
- uses: actions/github-script@v7
- uses: actions/github-script@v8
id: should-trigger
with:
result-encoding: string
script: |
const { payload } = context
const changes = ${{ steps.filter.outputs.changes }}
const manual = payload.commits
const manual = payload?.commits
.some(c => c.message.toLowerCase().replace(/\W/g, " ").includes("docker rebuild"))
console.log({ payload, changes, manual, commits: payload.commits })
@@ -167,6 +167,11 @@ jobs:
cd .haxelib/hxcpp/git/tools/hxcpp
haxe compile.hxml
cd ../../../../..
- name: Populate .env
run: |
touch .env
grep API_NG_APP_ID .env || echo API_NG_APP_ID=${{ secrets.API_NG_APP_ID }} >> .env
grep API_NG_ENC_KEY .env || echo API_NG_ENC_KEY=${{ secrets.API_NG_ENC_KEY }} >> .env
- name: Build game (windows)
if: ${{ matrix.target == 'windows' }}
run: |
@@ -246,6 +251,11 @@ jobs:
path: /usr/share/hxcpp
key: haxe-hxcpp-${{ runner.os }}-${{ github.ref_name }}-${{ github.sha }}
restore-keys: haxe-hxcpp-${{ runner.os }}-${{ github.ref_name }}
- name: Populate .env
run: |
touch .env
grep API_NG_APP_ID .env || echo API_NG_APP_ID=${{ secrets.API_NG_APP_ID }} >> .env
grep API_NG_ENC_KEY .env || echo API_NG_ENC_KEY=${{ secrets.API_NG_ENC_KEY }} >> .env
- name: Build game
run: |
haxelib run lime build ${{ matrix.target }} -v -release --times $BUILD_DEFINES
+5 -1
View File
@@ -13,10 +13,14 @@ shitAudio/
.swp
NewgroundsCredentials.hx
# Exclude IntelliJ IDEA stuff
.idea/
*.iml
# Mobile signing stuff
.apple/
.env
key.keystore
*.keystore
# Exclude JS stuff
node_modules/
+1
View File
@@ -4,3 +4,4 @@
[submodule "art"]
path = art
url = https://github.com/FunkinCrew/Funkin.art
shallow = true
+25 -87
View File
@@ -1,31 +1,22 @@
{
"[haxe]": {
// Automatically keep Haxe files formatted.
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "never"
},
"editor.codeActionsOnSave": { "source.organizeImports": "never" },
"editor.defaultFormatter": "nadako.vshaxe",
"editor.tabSize": 2
},
"[json]": {
// Automatically keep JSON files formatted.
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
// Automatically keep JSONC files formatted.
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"prettier.tabWidth": 2,
// XML formatting style configuration
// XML Formatting
"xml.format.enabled": true,
"xml.format.legacy": false,
"xml.format.emptyElements": "collapse",
@@ -33,7 +24,7 @@
"xml.format.enforceQuoteStyle": "preferred",
"xml.format.preserveAttributeLineBreaks": false,
"xml.format.preservedNewlines": 0,
"xml.format.splitAttributes": false,
"xml.format.splitAttributes": "preserve",
"xml.format.joinCDATALines": true,
"xml.format.preserveEmptyContent": false,
"xml.format.joinCommentLines": false,
@@ -56,26 +47,19 @@
"xml.format.maxLineWidth": 0,
"xml.format.grammarAwareFormatting": true,
// Generic file formatting style configuration
// General formatting
"files.insertFinalNewline": true,
"files.trimFinalNewlines": false,
"files.trimTrailingWhitespace": true,
// Automatically detect indentation.
"editor.detectIndentation": true,
"editor.insertSpaces": true,
"editor.tabSize": 2,
// Automatically enforce Linux style line endings.
"prettier.tabWidth": 2,
"files.eol": "\n",
"haxe.displayPort": "auto",
"haxe.enableCompilationServer": true,
"haxe.enableServerView": true,
"haxe.displayServer": {
"arguments": ["-v"]
},
// Fix file associations for HScript.
"haxe.displayServer": { "arguments": ["-v"] },
"files.associations": {
"*.hxp": "haxe",
"*.hscript": "haxe",
@@ -84,14 +68,27 @@
"*.hxc": "haxe"
},
"projectManager.git.baseFolders": ["./"],
"haxecheckstyle.sourceFolders": ["src", "source"],
"haxecheckstyle.externalSourceRoots": [],
"haxecheckstyle.configurationFile": "checkstyle.json",
"haxecheckstyle.codeSimilarityBufferSize": 100,
"lime.projectFile": "project.hxp",
"lime.targets": [
{ "name": "windows", "enabled": true, "label": "Windows" },
{ "name": "mac", "enabled": true, "label": "macOS" },
{ "name": "linux", "enabled": true, "label": "Linux" },
{ "name": "html5", "enabled": true, "label": "HTML5" },
{ "name": "android", "enabled": true, "label": "Android" },
{ "name": "ios", "enabled": true, "label": "iOS" },
// Disabled targets
{ "name": "hl", "enabled": false, "label": "HashLink" },
{ "name": "air", "enabled": false },
{ "name": "electron", "enabled": false },
{ "name": "flash", "enabled": false },
{ "name": "neko", "enabled": false },
{ "name": "tvos", "enabled": false }
],
"lime.defaultTargetConfiguration": "Windows / Debug",
"lime.targetConfigurations": [
{
"label": "Windows / Debug (Discord)",
@@ -103,21 +100,11 @@
"target": "windows",
"args": ["-debug", "-DANIMATE", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (FlxAnimate Test)",
"target": "hl",
"args": ["-debug", "-DANIMATE"]
},
{
"label": "Windows / Debug (Straight to Freeplay)",
"target": "windows",
"args": ["-debug", "-DFREEPLAY", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Straight to Freeplay)",
"target": "hl",
"args": ["-debug", "-DFREEPLAY"]
},
{
"label": "Windows / Debug (Straight to Play - Bopeebo Normal)",
"target": "windows",
@@ -132,26 +119,6 @@
"target": "windows",
"args": ["-debug", "-DSONG=2hot", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Straight to Play - Bopeebo Normal)",
"target": "hl",
"args": ["-debug", "-DSONG=bopeebo -DDIFFICULTY=normal"]
},
{
"label": "Windows / Debug (Conversation Test)",
"target": "windows",
"args": ["-debug", "-DDIALOGUE", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Conversation Test)",
"target": "hl",
"args": ["-debug", "-DDIALOGUE"]
},
{
"label": "Windows / Debug (Results Screen Test)",
"target": "windows",
"args": ["-debug", "-DRESULTS"]
},
{
"label": "Windows / Debug (Straight to Stage Editor)",
"target": "windows",
@@ -172,36 +139,16 @@
"target": "windows",
"args": ["-debug", "-DHXVLC_LOGGING", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Straight to Animation Editor)",
"target": "hl",
"args": ["-debug", "-DANIMDEBUG"]
},
{
"label": "Windows / Debug (Latency Test)",
"target": "windows",
"args": ["-debug", "-DLATENCY", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Latency Test)",
"target": "hl",
"args": ["-debug", "-DLATENCY"]
},
{
"label": "Windows / Debug (Waveform Test)",
"target": "windows",
"args": ["-debug", "-DWAVEFORM", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "Windows / Release (GitHub Actions)",
"target": "windows",
"args": ["-release", "-DGITHUB_BUILD"]
},
{
"label": "HashLink / Debug (Waveform Test)",
"target": "hl",
"args": ["-debug", "-DWAVEFORM"]
},
{
"label": "HTML5 / Debug (Watch)",
"target": "html5",
@@ -209,10 +156,7 @@
}
],
"lime.buildTypes": [
{
"label": "Debug",
"args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{ "label": "Debug", "args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"] },
{
"label": "Debug (Unlock Everything)",
"args": ["-debug", "-DUNLOCK_EVERYTHING"]
@@ -225,10 +169,7 @@
"label": "Debug (Straight to Chart Editor)",
"args": ["-debug", "-DCHARTING", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "Release",
"args": ["-release"]
},
{ "label": "Release", "args": ["-release"] },
{
"label": "Release (GitHub Actions)",
"args": ["-release", "-DGITHUB_BUILD"]
@@ -244,9 +185,6 @@
],
"vscord.app.privacyMode.enable": true,
"json.schemas": [
{
"fileMatch": ["/hmm.json"],
"url": "./.vscode/schema/hmm.json"
}
{ "fileMatch": ["/hmm.json"], "url": "./.vscode/schema/hmm.json" }
]
}
+281 -1
View File
@@ -4,6 +4,286 @@ All notable changes will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.8.1] - 2026-01-04
### Added
- Added a new "Non-scoreable" notekind to the Chart Editor that doesn't affect scores and ranks, or play miss animations.
### Changed
- Changed the order in which events are dispatched in PlayState. ([16f3e03](https://github.com/FunkinCrew/Funkin/commit/16f3e038d80c37d016c9d2b6cdd1012c94ef88b9)) - by @ComedyLost in [#6606](https://github.com/FunkinCrew/Funkin/pull/6606)
### Fixed
- The last note of SPAGHETTI no longer counts toward scoring, allowing players to earn Perfect ranks.
## [0.8.0] - 2025-12-31
The LE SSERAFIM collab update!
### Added
- Added a new playable song: SPAGHETTI (feat. j-hope of BTS) (Clean ver.) by LE SSERAFIM (feat. j-hope)!
- Try it out from either Story Mode or the Freeplay Menu!
- Added subtitles for various cutscenes and songs throughout the game.
- This can be toggled in the Preferences menu.
- [MOBILE] Added support for Newgrounds logins and saving/loading from the cloud.
- [MOBILE] Added support for opening FNFC chart files through the filesystem.
- [MOBILE] Added haptics for tapping to begin on the Title Screen.
- [MOBILE] Added the back button to the Character Select menu.
- Added new function callbacks for Module scripts in Freeplay and Character Select.
- Added `getOtherNotes()` function for counting notes that are not of a certain notekind.
- Implemented stacked patches to JSON merging.
- This allows mods to add new variations to existing songs without any extra fuss. See the [Code Cookbook](https://thekade.net/funkin-cookbook/) for more information.
- Added the ability to import Osu!Mania and StepMania charts in the Chart Editor.
- Added Move Difficulty and Clone Difficulty buttons to the Chart Editor Difficulty window.
- Added a checkbox to hide vocal waveforms in the Chart Editor.
- Added support for SRT subtitles that can display in game. ([a7fc70a](https://github.com/FunkinCrew/Funkin/commit/a7fc70a5c9cbd811b48cf2eb2f6c221814c2a3cd)) - by @PurSnake in [#6206](https://github.com/FunkinCrew/Funkin/pull/6206)
- Added support for various render types and scripting for the Freeplay DJ. ([8b38bcf](https://github.com/FunkinCrew/Funkin/commit/8b38bcfbdd9ef1b135046022b1b93eec5dedbd28)) - by @PurSnake and @AbnormalPoof in [#5698](https://github.com/FunkinCrew/Funkin/pull/5698)
- Level props can now have a flipX field. ([4961d4e](https://github.com/FunkinCrew/Funkin/commit/4961d4ee992445a487587dd3a7f3ab965eb4f5cf)) - by @Starexify in [#6218](https://github.com/FunkinCrew/Funkin/pull/6218)
- [LINUX] Added support for Feral Gamemode. ([555ec09](https://github.com/FunkinCrew/Funkin/commit/555ec09ef5be3c1d1966eeaa0275835e59aa5709)) - by @Noobz4Life in [#2473](https://github.com/FunkinCrew/Funkin/pull/2473)
- Added support for importing charts from Osu!Mania. ([0be42bf](https://github.com/FunkinCrew/Funkin/commit/0be42bf0475975d3195d4da85e653899df72a28c)) - by @FuroYT in [#6155](https://github.com/FunkinCrew/Funkin/pull/6155)
- Added a "No Animation" notekind to the Chart Editor. ([618e093](https://github.com/FunkinCrew/Funkin/commit/618e093e1ce2d4f45fa4da5d34ed84e58394126e)) - by @Eviethecoder in [#4036](https://github.com/FunkinCrew/Funkin/pull/4036)
- Added a way to adjust time signature/BPM changes through the Metadata window in the Chart Editor. ([b05a5c7](https://github.com/FunkinCrew/Funkin/commit/b05a5c7f54f9b41d63081837dd2c7392203b4530)) - by @Keoiki and @Lasercar in [#4770](https://github.com/FunkinCrew/Funkin/pull/4770)
- Added character preview windows for the player and opponent in the Chart Editor. ([9f59231](https://github.com/FunkinCrew/Funkin/commit/9f59231d58a889e2c9a0f8e7e30b2e1c370e0d9a)) - by @PurSnake and @ComedyLost in [#6221](https://github.com/FunkinCrew/Funkin/pull/6221)
- Added the option to view the Results Screen after Chart Editor playtests. ([eeba677](https://github.com/FunkinCrew/Funkin/commit/eeba677da9fac75cd34481ead01dc05dfa074bce)) - by @Lasercar in [#4087](https://github.com/FunkinCrew/Funkin/pull/4087)
- Added the option to carry over Chart Editor volume levels into a playtest. ([4dd87e7](https://github.com/FunkinCrew/Funkin/commit/4dd87e7308789bae9a9e903bb15737e8149012f7)) - by @KoloInDaCrib in [#6302](https://github.com/FunkinCrew/Funkin/pull/6302)
- Added the ability to time-travel during a Chart Editor playtest in release builds with PgUp and PgDown. ([4a00429](https://github.com/FunkinCrew/Funkin/commit/4a00429a57f03f60159a4e2e4f531bd6fa118658)) - by @AbnormalPoof in [#4209](https://github.com/FunkinCrew/Funkin/pull/4209)
- Added the Rift of the Necrodancer collab trailer to the pool of videos in Attract Mode.
### Changed
- The mod API version is now 0.8.0. Please update your mods to ensure they work.
- Made some improvements to Polymod:
- Scripts can no longer accidentally redefine variables defined in their super class.
- `Math` and `Std` can now properly be used in static functions. (Thanks NotHyper-474!)
- Significantly improved the speed of retrieving modded assets, improving the performance of the game with a lot of mods installed. (Thanks PurSnake!)
- Replaced `FlxAnimate` with `flixel-animate` to overhaul texture atlas handling:
- Significantly improved performance in the Character Select Menu.
- Pico (holding Nene)’s game over confirm animation now plays properly.
- The camera now properly focuses on atlas characters after restarting the song.
- Atlas characters now properly display the rimlight shader.
- Implemented multiple new settings for atlas sprites.
- Fixed a ton of bugs across various menus.
- Re-exported many assets to improve memory usage and performance throughout the game.
- [MOBILE] Music from external sources now pauses when opening the game.
- [MOBILE] The game is now named “FNF” on the Home Screen.
- [DESKTOP] Reworked window resizing behavior to behave more consistently.
- Made it easier to implement custom Pause Menu substates.
- Pressing F6 (rebindable) now cycles through debug display modes.
- Boyfriend and Girlfriend’s scared animations in Week 2 now last longer.
- Added “Change difficulty” to the list of undo-able actions in the Chart Editor.
- Changed the default easing type for the Chart Editor Focus Camera event from Linear to Classic.
- The game now displays a user-friendly crash message when attempting to play without a graphics card. ([5270353](https://github.com/FunkinCrew/Funkin/commit/52703536a62fc004fb7be0457da89705f812364f)) - by @ACrazyTown in [#6160](https://github.com/FunkinCrew/Funkin/pull/6160)
- Adjusted strumline confirm animations when hitting notes to feel more responsive. ([62a3f73](https://github.com/FunkinCrew/Funkin/commit/62a3f73de9d653d4ca5a7d7f4c895fc9fd1a4be7)) - by @gamerbross in [#6261](https://github.com/FunkinCrew/Funkin/pull/6261)
- The Lag Adjustment menu now displays a note splash when hitting perfectly. ([342c3cd](https://github.com/FunkinCrew/Funkin/commit/342c3cd7d6f33b3babb70f3d1fb604b0bdb90464)) - by @ExtraCode75 in [#6602](https://github.com/FunkinCrew/Funkin/pull/6602)
- Freeplay song previews now fade out before restarting. ([aaf5084](https://github.com/FunkinCrew/Funkin/commit/aaf5084fd9e9c2d3b7e823767a93db82ce908d86)) - by @JVNpixels in [#6094](https://github.com/FunkinCrew/Funkin/pull/6094)
- The game's volume is now reduced when unfocused without pausing. ([3040692](https://github.com/FunkinCrew/Funkin/commit/30406928522f88762eab4bbf571fedf1424f7569)) - by @PurSnake in [#6250](https://github.com/FunkinCrew/Funkin/pull/6250)
- Optimized the way the windows in Week 3 are handled. ([70d433d](https://github.com/FunkinCrew/funkin.assets/commit/70d433de3027badb39027d295d1b18d20a130741)) - by @CrusherNotDrip in [funkin.assets#291](https://github.com/FunkinCrew/funkin.assets/pull/291)
- Script create events now also run after hot-reloading with F5, rather than only during initialization. ([2f865a5](https://github.com/FunkinCrew/Funkin/commit/2f865a5c7f4d8712f1b1e0bc70cb910f4fed08dc)) - by @VirtuGuy in [#6084](https://github.com/FunkinCrew/Funkin/pull/6084)
- Hot-reloading with F5 during a Chart Editor playtest no longer returns to the Chart Editor. ([c1988f1](https://github.com/FunkinCrew/Funkin/commit/c1988f1858344a78a82bf335774d986703cac942)) - by @KoloInDaCrib in [#6275](https://github.com/FunkinCrew/Funkin/pull/6275)
- Opening the Chart Editor during a song now places the playhead at the current song position. ([71b4a58](https://github.com/FunkinCrew/Funkin/commit/71b4a58cab821c3b5a530e3a2be6d73966faa2a6)) - by @KoloInDaCrib in [#6210](https://github.com/FunkinCrew/Funkin/pull/6210)
- The Easing property for Chart Editor events is now split into two separate dropdowns: type and direction. ([d1d77dd](https://github.com/FunkinCrew/Funkin/commit/d1d77dd85c3daed22ee954353fa41e81279ced3c)) - by @PurSnake in [#5612](https://github.com/FunkinCrew/Funkin/pull/5612)
- The Chart Editor Events window now supports collapsible groups. ([9f6879e](https://github.com/FunkinCrew/Funkin/commit/9f6879ecc98e725afa3de8f9b02755047b353cd7)) - by @KoloInDaCrib in [#6554](https://github.com/FunkinCrew/Funkin/pull/6554)
- Chart Editor BPM fields now support up to 3 decimal places. ([c863ef3](https://github.com/FunkinCrew/funkin.assets/commit/c863ef378569ce0d6b0804029e5d395b1c321cbd)) - by @roma-perec-bp in [funkin.assets#273](https://github.com/FunkinCrew/funkin.assets/pull/273)
- Long difficulty and variation names are now shortened in the Chart Editor playbar. ([2c4167b](https://github.com/FunkinCrew/Funkin/commit/2c4167bc11aec92693494816735063c381279b3e)) - by @Lasercar in [#6150](https://github.com/FunkinCrew/Funkin/pull/6150)
- The remaining time in the playbar now also shows a millisecond value in the Chart Editor. ([2d5ef19](https://github.com/FunkinCrew/Funkin/commit/2d5ef19b3b3c55db1eb70506b00d4f820e7ccb20)) - by @JVNpixels in [#6089](https://github.com/FunkinCrew/Funkin/pull/6089)
- Added more hotkeys to the Chart Editor user guide. ([7d0d9eb](https://github.com/FunkinCrew/funkin.assets/commit/7d0d9ebf7ea60e04d09c9b140772c36493dc6291)) - by @afreetoplaynoob in [funkin.assets#294](https://github.com/FunkinCrew/funkin.assets/pull/294)
- The Stage Editor can now be opened from Freeplay to the selected capsule's stage. ([1ff6a16](https://github.com/FunkinCrew/Funkin/commit/1ff6a1656cebdfe415768ffe38bd43b37c663eda)) - by @Lasercar in [#5264](https://github.com/FunkinCrew/Funkin/pull/5264)
- The Stage Editor now saves backups upon quitting or crashing. ([5326c40](https://github.com/FunkinCrew/Funkin/commit/5326c40347b1548a682ef1f79eeb4a70618256ff)) - by @Lasercar in [#6190](https://github.com/FunkinCrew/Funkin/pull/6190)
- Stage Editor backups are now saved in the same folder as Chart Editor backups. ([4f8da59](https://github.com/FunkinCrew/Funkin/commit/4f8da595777c4b7fe6ddf6536ffa29a50c23dcb3)) - by @CrusherNotDrip in [#6297](https://github.com/FunkinCrew/Funkin/pull/6297)
### Fixed
- Fixed some issues with Polymod:
- Modded assets now properly go in their respective asset libraries. (Thanks NotHyper-474!)
- Static functions and fields from imported scripted classes can now be used.
- Properties can now be used in a static context.
- Local variables in a function no longer get dropped if said function calls its own `scriptCall` function. (Thanks KoloInDaCrib!)
- Local functions now properly report the class name.
- [DESKTOP] Switching audio devices no longer breaks audio processing.
- [HTML5] The Character Select Menu no longer crashes on Firefox.
- Pressing Escape and an arrow key with the Freeplay alternate instrumental selector open no longer crashes the game.
- Hot reloading with F5 on the Game Over screen no longer crashes the game.
- [HTML5] Restarting a video cutscene now properly plays the video again.
- [iOS] The navigation bar is no longer unlocked after watching an ad.
- [iOS] The “larger text” accessibility setting no longer causes the game to not fill the whole screen.
- [iOS] The game now displays error popups.
- [ANDROID] Keyboard detection and strumline positioning now work properly.
- [ANDROID] Taking a screenshot no longer pauses the game for a moment.
- The Title Screen no longer switches to Attract Mode while the Girlfriend’s Ringtone easter egg is playing.
- Holding Escape in the Main Menu no longer quickly closes the game.
- [MOBILE] Scrolling to another Week is no longer possible after entering a Story Week.
- [MOBILE] Long Freeplay capsule song names now properly scroll sideways.
- Exiting Character Select no longer allows mashing inputs in Freeplay.
- Controller inputs now register in the Character Select Menu.
- The player character in Character Select no longer slides in upon entering.
- The Character Select cursor no longer flies in from the top left corner.
- The Character Select cursor no longer continues to move without input after unfocusing the game.
- Video cutscenes no longer start at maximum volume for a split second.
- The input system now properly handles alternating between two keybinds bound to the same note.
- The camera now bops independently from the framerate during songs.
- The Pause Menu music now properly pauses when unfocusing with Pause on Unfocus enabled.
- Changing to the same difficulty as the current one during a Story Week no longer resets the Week score.
- More characters including parentheses now display properly in the Results Screen.
- [MOBILE] The rimlight for Girlfriend (Tankman Stickup) now renders properly.
- Character sprites with high global offsets no longer disappear near the edge of the screen.
- Fixed positions for Pico (Pixel) and Nene (Pixel), and removed the School (Pico) stage.
- Tweaked a few charts.
- [macOS] Fixed a crash related to notifications in the Chart Editor.
- [Windows] Opening the File Explorer no longer sometimes hangs in the Chart Editor.
- Pressing F4 to exit a debug editor before a tooltip appears no longer crashes the game.
- Changing the theme in the Chart Editor more than once no longer crashes the game.
- Unplugging a controller no longer crashes the Chart Editor.
- The Chart Editor measure ticks no longer cause a memory leak.
- Selecting multiple events by Ctrl-clicking in the Chart Editor no longer converts one into the other.
- Event tooltips now display all values including default ones.
- Pause on Unfocus now properly applies to Chart Editor playtests.
- Unfocusing while the Chart Editor is playing audio now pauses the playback.
- Fixed a memory leak with the Freeplay backing card scrolling text. ([67846d6](https://github.com/FunkinCrew/Funkin/commit/67846d6c4bfd2f42e002fa129a51772f0717329d)) - by @FuroYT in [#5963](https://github.com/FunkinCrew/Funkin/pull/5963)
- Using a static pixel icon in Character Select no longer crashes the game. ([96f8ee8](https://github.com/FunkinCrew/Funkin/commit/96f8ee85e0c2281834b7334b87d1862003e5fa05)) - by @VirtuGuy in [#6171](https://github.com/FunkinCrew/Funkin/pull/6171)
- A-Bot's visualizer no longer causes a memory leak during gameplay or in Character Select. ([6607645](https://github.com/FunkinCrew/Funkin/commit/6607645786ed8611436fa5f92a35027c2a64f1d0)) - by @FuroYT in [#5908](https://github.com/FunkinCrew/Funkin/pull/5908)
- The Credits menu no longer crashes when not using hardcoded credits. ([4366fbe](https://github.com/FunkinCrew/Funkin/commit/4366fbe51274438ad2bc190afcca42e37987386d)) - by @sphis-sinco in [#5982](https://github.com/FunkinCrew/Funkin/pull/5982)
- The game no longer lag spikes when Darnell throws his can in 2hot. ([a0bc6d6](https://github.com/FunkinCrew/funkin.assets/commit/a0bc6d61fde5799a7fc619b7fdea691b0d8bae05)) - by @KoloInDaCrib in [funkin.assets#313](https://github.com/FunkinCrew/funkin.assets/pull/313)
- Hold note covers no longer get permanently stuck playing after a lag spike. ([66882d9](https://github.com/FunkinCrew/Funkin/commit/66882d9677268396320ee003478a3e2627655755)) - by @KoloInDaCrib in [#6066](https://github.com/FunkinCrew/Funkin/pull/6066)
- Scrolling through Freeplay songs for the first time no longer stutters. ([cc486ad](https://github.com/FunkinCrew/Funkin/commit/cc486ad94b5e9806d18fb65165663fea0ce0d7b4)) - by @mikolka9144 in [#4851](https://github.com/FunkinCrew/Funkin/pull/4851)
- Re-implemented an error that appears when a video file does not exist. ([2ad5933](https://github.com/FunkinCrew/Funkin/commit/2ad593341f8bea8384646f4803dc8795a9efe32a)) - by @TechnikTil in [#6253](https://github.com/FunkinCrew/Funkin/pull/6253)
- The game now properly parses .json files beginning with a bracket. ([c7367ae](https://github.com/FunkinCrew/Funkin/commit/c7367ae33d7e11f57b821f7d5fbff39a3edc1604)) - by @NotHyper-474 in [#6164](https://github.com/FunkinCrew/Funkin/pull/6164)
- The Newgrounds logo no longer covers the intro text. ([28883fb](https://github.com/FunkinCrew/Funkin/commit/28883fb9ed025abf9f9aee2621f365cd6f4d8b6c)) - by @hucks5 in [#6196](https://github.com/FunkinCrew/Funkin/pull/6196)
- Exiting from the Save Data Options menu no longer exits the Options Menu as well. ([26b253d](https://github.com/FunkinCrew/Funkin/commit/26b253d8cac97f0dcd09a5ef233f7058cd26008e)) - by @VirtuGuy in [#6036](https://github.com/FunkinCrew/Funkin/pull/6036)
- Numeric values in the Options Menu are now rounded to prevent near-zero precision errors. ([934bce7](https://github.com/FunkinCrew/Funkin/commit/934bce7bdc5780e09623d845eda97a27c0ad0ecf)) - by @Starexify in [#6225](https://github.com/FunkinCrew/Funkin/pull/6225)
- The Test/Offset Calibration menu no longer starts playing after exiting the Lag Adjustment menu. ([23a38f7](https://github.com/FunkinCrew/Funkin/commit/23a38f76f9c79c5fab5b9af5e2d98e51597e05a8)) - by @VioletSnowLeopard in [#6088](https://github.com/FunkinCrew/Funkin/pull/6088)
- [MOBILE] The Back button now fades out when entering a Week in Story Mode. ([1ebce74](https://github.com/FunkinCrew/Funkin/commit/1ebce748d4e3306b4464a7bf82fab56bcb427a62)) - by @VirtuGuy in [#6255](https://github.com/FunkinCrew/Funkin/pull/6255)
- The Freeplay difficulty can now only be changed in one direction at a time. ([95af46c](https://github.com/FunkinCrew/Funkin/commit/95af46cbf72291d9d3226df91ed17f1aa3d3839f)) - by @Starexify in [#6262](https://github.com/FunkinCrew/Funkin/pull/6262)
- Pico's backing card now renders properly in widescreen aspect ratios. ([aa29b3d](https://github.com/FunkinCrew/funkin.assets/commit/aa29b3dcc1ee2f8e89d2622c480b2e997410d57d)) - by @VirtuGuy in [funkin.assets#297](https://github.com/FunkinCrew/funkin.assets/pull/297)
- The album titles in Freeplay are now positioned consistently. ([ed8cca4](https://github.com/FunkinCrew/funkin.assets/commit/ed8cca4694a098ae6aa56c4d2a231587edb9688c)) - by @Donothan73 in [funkin.assets#257](https://github.com/FunkinCrew/funkin.assets/pull/257)
- Health icons now bop independently from the framerate. ([8e6fa51](https://github.com/FunkinCrew/Funkin/commit/8e6fa51c27ceaa830efec06bb8a52e281a008255)) - by @PurSnake in [#6035](https://github.com/FunkinCrew/Funkin/pull/6035)
- The note resetting animation on song restart is now consistent between Downscroll and Upscroll. ([7b8d657](https://github.com/FunkinCrew/Funkin/commit/7b8d6577eddba568f20c3d23663dded054699090)) - by @FuroYT in [#6220](https://github.com/FunkinCrew/Funkin/pull/6220)
- Offsets are now properly applied to stage props. ([33a020a](https://github.com/FunkinCrew/Funkin/commit/33a020a34bb220f56c0ff9fbad8688e10ca5b44f)) - by @Starexify in [#6224](https://github.com/FunkinCrew/Funkin/pull/6224)
- Notestyles now accept offset values. ([193c443](https://github.com/FunkinCrew/Funkin/commit/193c443bc47c1a7d1481d280a1d0674b920a323c)) - by @NebulaStellaNova in [#6326](https://github.com/FunkinCrew/Funkin/pull/6326)
- The path for a notestyle's countdown sound is now properly loaded as a sound instead of an image. ([dd5ea77](https://github.com/FunkinCrew/Funkin/commit/dd5ea7748b3b1878139998c209aa06db0fa05e0b)) - by @gamerbross in [#6232](https://github.com/FunkinCrew/Funkin/pull/6232)
- The Pause Menu theme no longer plays at maximum volume for a split second when pausing. ([6ba44d6](https://github.com/FunkinCrew/Funkin/commit/6ba44d6cb0c71deb29eb23b82b61f58b2e95193e)) - by @ComedyLost in [#6334](https://github.com/FunkinCrew/Funkin/pull/6334)
- The scrolling text in the Results Screen is now positioned consistently. ([200f798](https://github.com/FunkinCrew/Funkin/commit/200f7984379cb1bd911e49de92be312750893420)) - by @NotHyper-474 in [#6595](https://github.com/FunkinCrew/Funkin/pull/6595)
- The scrolling text in the Results Screen is now FPS-independent. ([8c43f6f](https://github.com/FunkinCrew/Funkin/commit/8c43f6f67a362bde4de4ad8dbd72966d7c4a9f60)) - by @VirtuGuy in [#6476](https://github.com/FunkinCrew/Funkin/pull/6476)
- The Results Screen theme now loops more smoothly. ([592bf1a](https://github.com/FunkinCrew/Funkin/commit/592bf1a9221d4dcef53ae421571eddf78ed631e4)) - by @FuroYT in [#6477](https://github.com/FunkinCrew/Funkin/pull/6477)
- Transition stickers now render properly with the window resized to any aspect ratio. ([4215b1c](https://github.com/FunkinCrew/Funkin/commit/4215b1c72e4391b2176014825885799b81dde2ba)) - by @PurSnake in [#6156](https://github.com/FunkinCrew/Funkin/pull/6156)
- Boyfriend now stands in the same position in the Week 3 and Week 3 Erect stages. ([d2d15f1](https://github.com/FunkinCrew/funkin.assets/commit/d2d15f114256125d53c6331b46415701ae291c1f)) - by @CEliuxJV in [funkin.assets#261](https://github.com/FunkinCrew/funkin.assets/pull/261)
- Pico's game over music now properly loops after his explosion death in 2hot. ([f1a1db0](https://github.com/FunkinCrew/Funkin/commit/f1a1db08347c3f9248fd16909cff06027297ad13)) - by @KoloInDaCrib in [#6216](https://github.com/FunkinCrew/Funkin/pull/6216)
- 2hot now smoothly transitions into the cutscene. ([462b1b6](https://github.com/FunkinCrew/funkin.assets/commit/462b1b6e6e9b622ad8fd15f15a1f39c7dffa4764)) - by @hucks5 in [funkin.assets#286](https://github.com/FunkinCrew/funkin.assets/pull/286)
- Nene now swings her legs during in-game cutscenes. ([6a5d9b2](https://github.com/FunkinCrew/funkin.assets/commit/6a5d9b265732d60da28083fb1c3d7b7b854edf63)) - by @hucks5 in [funkin.assets#289](https://github.com/FunkinCrew/funkin.assets/pull/289)
- Nene's 50 combo animation now renders its heart properly. ([bcd96ee](https://github.com/FunkinCrew/funkin.assets/commit/bcd96eeb78b9b03cf7b24dc956ac3bc36438193e)) - by @VirtuGuy in [funkin.assets#296](https://github.com/FunkinCrew/funkin.assets/pull/296)
- A-Bot (Christmas) now properly looks at the opponent. ([f81cc16](https://github.com/FunkinCrew/funkin.assets/commit/f81cc16c8f99655f76b7e27ce221819798c9c94e)) - by @ComedyLost in [funkin.assets#279](https://github.com/FunkinCrew/funkin.assets/pull/279)
- Nene (Pixel) now raises and lowers her knife properly. ([1d9f50d](https://github.com/FunkinCrew/funkin.assets/commit/1d9f50d405f202e07efd883aeef5d6c1e9eaaa90)) - by @VirtuGuy in [funkin.assets#274](https://github.com/FunkinCrew/funkin.assets/pull/274)
- Pico (Pixel)'s game over theme now plays at the right time. ([e8a4d24](https://github.com/FunkinCrew/funkin.assets/commit/e8a4d2472ca2dc28b03be05953dff3d7b5ec7433)) - by @JackXson-Real and @ComedyLost in [funkin.assets#288](https://github.com/FunkinCrew/funkin.assets/pull/288)
- Pico holding Nene's idle animation no longer lasts too long. ([3483345](https://github.com/FunkinCrew/funkin.assets/commit/348334583746ec45f1c6ab2c5e1715a67038cc5c)) - by @JackXson-Real in [funkin.assets#271](https://github.com/FunkinCrew/funkin.assets/pull/271)
- The tankmen in Stress no longer briefly appear behind the speakers when restarting. ([7f79ad8](https://github.com/FunkinCrew/funkin.assets/commit/7f79ad8c08d66fb909a9382de2c09f0430b756f1)) - by @VirtuGuy in [funkin.assets#298](https://github.com/FunkinCrew/funkin.assets/pull/298)
- Encountering a Set Health Icon event during Minimal playtesting no longer crashes the Chart Editor. ([9f0a498](https://github.com/FunkinCrew/Funkin/commit/9f0a4988a3cc47581916a9750731bb7e63f168f3)) - by @charlesisfeline in [#6481](https://github.com/FunkinCrew/Funkin/pull/6481)
- Typing in Chart Editor text fields no longer triggers bound key functions. ([3b5e589](https://github.com/FunkinCrew/Funkin/commit/3b5e5893c01c2b56036e53626eaea8d41e70f640)) - by @MightyTheArmiddilo in [#6601](https://github.com/FunkinCrew/Funkin/pull/6601)
- Clicking between Chart Editor events no longer converts the second event's dropdown values into those of the first. ([c170c25](https://github.com/FunkinCrew/Funkin/commit/c170c256934ae316a735d38620ad5e24c2f15f4d)) - by @NotHyper-474 in [#6211](https://github.com/FunkinCrew/Funkin/pull/6211)
- Chart Editor note kinds no longer lose their data when selecting multiple notes with different data. ([5a2c364](https://github.com/FunkinCrew/Funkin/commit/5a2c364c507fb4b86a740c3b2476c336496cd729)) - by @Lasercar in [#4248](https://github.com/FunkinCrew/Funkin/pull/4248)
- Placing a note and removing it no longer breaks the cursor in the Chart Editor. ([8cb5ed9](https://github.com/FunkinCrew/Funkin/commit/8cb5ed91baf50b6f78eefc2e50612a01195a2742)) - by @Lasercar in [#4237](https://github.com/FunkinCrew/Funkin/pull/4237)
- Playtesting in the Chart Editor now properly loads song variation scripts. ([055f620](https://github.com/FunkinCrew/Funkin/commit/055f62026ef44a928876d9065cc388da0905e9f1)) - by @PurSnake in [#6405](https://github.com/FunkinCrew/Funkin/pull/6405)
- Playtesting in the Chart Editor no longer sometimes moves on to another song like Story Mode. ([9497378](https://github.com/FunkinCrew/Funkin/commit/9497378600bad44639ff8b3fc92cb329052fd08a)) - by @KoloInDaCrib in [#6192](https://github.com/FunkinCrew/Funkin/pull/6192)
- The cursor is now hidden when playtesting through the Chart Editor. ([0471872](https://github.com/FunkinCrew/Funkin/commit/047187257b6712e61bf05fdebab33c7b3e9ac0f6)) - by @VirtuGuy in [#6137](https://github.com/FunkinCrew/Funkin/pull/6137)
- The Chart Editor note moving sound no longer plays when dragging outside the grid. ([593456e](https://github.com/FunkinCrew/Funkin/commit/593456e53e02c53e71189fdb154e4a3e4f2b3092)) - by @amyspark-ng in [#4157](https://github.com/FunkinCrew/Funkin/pull/4157)
- The Chart Editor music now fades in after a period of silence when enabled. ([960d2f3](https://github.com/FunkinCrew/Funkin/commit/960d2f3cf166400c6fecee212b87961f3df10c2e)) - by @Lasercar in [#4148](https://github.com/FunkinCrew/Funkin/pull/4148)
- The Chart Editor Difficulty toolbox no longer displays the incorrect song name. ([dff2a1e](https://github.com/FunkinCrew/Funkin/commit/dff2a1e487dc20f86ef4f526a259083a7726ea6a)) - by @VirtuGuy in [#6547](https://github.com/FunkinCrew/Funkin/pull/6547)
- Chart Editor notifications now display a bit higher. ([9e645db](https://github.com/FunkinCrew/Funkin/commit/9e645db0074424ffb2de42b27afb3adaff55de62)) - by @MightyTheArmiddilo in [#6445](https://github.com/FunkinCrew/Funkin/pull/6445)
- The Yes and No text in the Chart Editor exit confirmation prompt buttons now always render properly. ([4c0d925](https://github.com/FunkinCrew/Funkin/commit/4c0d92507ae0b81a744d223e5292bf8d27013abd)) - by @NotHyper-474 in [#6173](https://github.com/FunkinCrew/Funkin/pull/6173)
- Exiting the Chart Editor now consistently saves your audio preferences. ([2f77ccf](https://github.com/FunkinCrew/Funkin/commit/2f77ccfc6e93fabef27d3e06447f9f6edd8e840d)) - by @VioletSnowLeopard in [#6127](https://github.com/FunkinCrew/Funkin/pull/6127)
- The Chart Editor and Stage Editor backup window now properly retrieves the most recently edited backup. ([a8dec0c](https://github.com/FunkinCrew/Funkin/commit/a8dec0cd7053f88865757f30a7d9f0ca5eb0fb02)) - by @Lasercar in [#6119](https://github.com/FunkinCrew/Funkin/pull/6119)
- The Chart Editor backup dialog no longer appears if the latest backup is deleted. ([bc1d36d](https://github.com/FunkinCrew/Funkin/commit/bc1d36ddeab81a790e3604598248a9b2fe869cac)) - by @KoloInDaCrib in [#6027](https://github.com/FunkinCrew/Funkin/pull/6027)
- Switching between Animation and Spritesheet mode no longer crashes the Animation Editor. ([a61016d](https://github.com/FunkinCrew/Funkin/commit/a61016d2dfc2ac6a50aae230969c036f0753cfaa)) - by @VirtuGuy in [#6114](https://github.com/FunkinCrew/Funkin/pull/6114)
- The Animation Editor no longer displays a missing object icon next to the dropdown menus. ([b30ea36](https://github.com/FunkinCrew/Funkin/commit/b30ea36fcc982ce4ac0be9143fd4701e180fc684)) - by @KoloInDaCrib in [#6177](https://github.com/FunkinCrew/Funkin/pull/6177)
- The Animation Editor onion skin now properly accounts for character scale and offsets. ([a76a868](https://github.com/FunkinCrew/Funkin/commit/a76a868cb6a27c1c76d09177b90cf6df3df29d17)) - by @VirtuGuy in [#6123](https://github.com/FunkinCrew/Funkin/pull/6123)
- The debug cursor is now properly hidden when exiting the Animation Editor. ([f2f6b4c](https://github.com/FunkinCrew/Funkin/commit/f2f6b4c8591e1ab4a8c6f0ed998d6d25d7cc9f43)) - by @VirtuGuy in [#6112](https://github.com/FunkinCrew/Funkin/pull/6112)
- Plenty more tiny fixes.
## New Contributors for 0.8.0
* @Eviethecoder made their first contribution in [#4036](https://github.com/FunkinCrew/Funkin/pull/4036)
* @FuroYT made their first contribution in [#5908](https://github.com/FunkinCrew/Funkin/pull/5908)
* @HeroEyad made their first contribution in [#6049](https://github.com/FunkinCrew/Funkin/pull/6049)
* @hucks5 made their first contribution in [#6196](https://github.com/FunkinCrew/Funkin/pull/6196)
* @Starexify made their first contribution in [#6225](https://github.com/FunkinCrew/Funkin/pull/6225)
* @NebulaStellaNova made their first contribution in [#6326](https://github.com/FunkinCrew/Funkin/pull/6326)
* @MightyTheArmiddilo made their first contribution in [#6445](https://github.com/FunkinCrew/Funkin/pull/6445)
* @Donothan73 made their first contribution in [funkin.assets#257](https://github.com/FunkinCrew/funkin.assets/pull/257)
* @CEliuxJV made their first contribution in [funkin.assets#261](https://github.com/FunkinCrew/funkin.assets/pull/261)
* @roma-perec-bp made their first contribution in [funkin.assets#273](https://github.com/FunkinCrew/funkin.assets/pull/273)
* @ComedyLost made their first contribution in [funkin.assets#279](https://github.com/FunkinCrew/funkin.assets/pull/279)
## [0.7.5] - 2025-09-19
### Added
- Added an `onStateCreate` event to the Module class.
### Changed
- Gave the debug display a huge upgrade!
- Choose from three settings in the Preferences Menu: Off/Simple/Advanced
- New “Task memory” counter shows how much RAM the game is using on your computer.
- Advanced mode displays graphs for FPS and RAM usage.
- Hot reloading now works in scripted states.
- Made a few more classes scriptable.
- FunkinVideoSprite
- FlxStrip
- FlxTypedGroup
- Strumline
- StageProp​​
- The Random capsule now brings up the option to select a random instrumental. ([0d5c6ac](https://github.com/FunkinCrew/Funkin/commit/0d5c6ac9632ee3bf3977b9bd369daab9f2d95fea)) - by @VioletSnowLeopard in [#5891](https://github.com/FunkinCrew/Funkin/pull/5891)
- The Character Select menu now returns to Freeplay after pressing a back button. ([250d218](https://github.com/FunkinCrew/Funkin/commit/250d2188b9cb2c92d283d8b12ed7de0286324592)) - by @MrMadera in [#5887](https://github.com/FunkinCrew/Funkin/pull/5887)
- Pressing the Shift key in the Credits menu now pauses scrolling. ([479036a](https://github.com/FunkinCrew/Funkin/commit/479036a4008fe1c54adc970c0a476fae6b1b3e94)) - by @JVNpixels in [#2924](https://github.com/FunkinCrew/Funkin/pull/2924)
- Chart Editor and Stage Editor backup files are now given relevant filenames. ([b31be10](https://github.com/FunkinCrew/Funkin/commit/b31be1043ccd1e906a656913b947bae160b451fe)) - by @Lasercar in [#4118](https://github.com/FunkinCrew/Funkin/pull/4118)
- The Animation Editor's onion skin now properly accounts for offsets and displays the idle animation. ([0fd6f5b](https://github.com/FunkinCrew/Funkin/commit/0fd6f5b3b7bd115a3b6821096daa929c220bd235)) - by @KoloInDaCrib in [#5810](https://github.com/FunkinCrew/Funkin/pull/5810)
### Fixed
- The Freeplay new rank animation now shows the new rank slamming onto the old one.
- Freeplay capsules with a Gold Perfect rank no longer become randomly offset when scrolling.
- The Pico Freeplay DJ’s new rank animation now properly renders its lighting.
- Freeplay song previews no longer play replaced or deleted audio files.
- The strumline no longer disappears after re-entering Lag Adjustment.
- Fixed various issues with Week 6 dialogue boxes, portraits, and audio.
- [MOBILE] The strumlines are no longer offset to the left in Blazin’.
- The preloader no longer disables fullscreen mode after completion.
- Attempting to log in to Newgrounds during an ongoing login attempt no longer crashes the game. ([4e572b1](https://github.com/FunkinCrew/Funkin/commit/4e572b171fe901a84f12b4566963fd3c1f4ae6fb)) - by @KoloInDaCrib in [#5922](https://github.com/FunkinCrew/Funkin/pull/5922)
- Added null-safety to Leaderboards and Medals to prevent a rare crash. ([88fc5f9](https://github.com/FunkinCrew/Funkin/commit/88fc5f9ed684e287d7411a59fb4fdf170acfed5a)) - by @NotHyper-474 in [#5926](https://github.com/FunkinCrew/Funkin/pull/5926)
- Freeplay ranks no longer pop out when scrolling through song capsules. ([747446e](https://github.com/FunkinCrew/Funkin/commit/747446e2e420449f422fbc69aafda6128ac8d16e)) - by @VioletSnowLeopard in [#5822](https://github.com/FunkinCrew/Funkin/pull/5822)
- The Freeplay intro no longer plays when returning from a sticker transition. ([63ec749](https://github.com/FunkinCrew/Funkin/commit/63ec749c7a3c90d529e9e8f2f3ed1c8913e8f9a1)) - by @PurSnake in [#5942](https://github.com/FunkinCrew/Funkin/pull/5942)
- Hot-reloading with F5 after earning a rank no longer crashes Freeplay. ([79e8b5d](https://github.com/FunkinCrew/Funkin/commit/79e8b5d71e052953b77b81b66e0a83a7bbeeeeed)) - by @KoloInDaCrib in [#5971](https://github.com/FunkinCrew/Funkin/pull/5971)
- The Main Menu music now fades in consistently when exiting from Freeplay to the Title Screen. ([a10bbf6](https://github.com/FunkinCrew/Funkin/commit/a10bbf60acbbeadfb5a91930745ef4fd338b1a24)) - by @VirtuGuy in [#5855](https://github.com/FunkinCrew/Funkin/pull/5855)
- The game now falls back to default audio files when files specified by the metadata don't exist. ([b5357ac](https://github.com/FunkinCrew/Funkin/commit/b5357acf98bde81a4121a3723061f7c559a0640f)) - by @VioletSnowLeopard in [#5954](https://github.com/FunkinCrew/Funkin/pull/5954)
- The cutscene pause menu no longer persists across songs after exiting a video cutscene. ([c2305f3](https://github.com/FunkinCrew/Funkin/commit/c2305f32f0147f6326e8665d21a6b8a793f45033)) - by @KoloInDaCrib in [#5880](https://github.com/FunkinCrew/Funkin/pull/5880)
- Mashing to skip the Eggnog Erect cutscene no longer crashes the game. ([5ca3c6a](https://github.com/FunkinCrew/funkin.assets/commit/5ca3c6a5745b61a457db149c289784c08e560ee9)) - by @VioletSnowLeopard in [funkin.assets#258](https://github.com/FunkinCrew/funkin.assets/pull/258)
- Stress (Pico Mix) no longer lag spikes when loading in the background tankmen. ([6701405](https://github.com/FunkinCrew/funkin.assets/commit/670140571239ca08c1ae1c5286773d99c2875c2d)) - by @NotHyper-474 and @KoloInDaCrib in [funkin.assets#242](https://github.com/FunkinCrew/funkin.assets/pull/242)
- Stress (Pico Mix) no longer lag spikes when Tankman changes sprites. ([aa5956a](https://github.com/FunkinCrew/Funkin/commit/aa5956a30bdc47d53ebebb4b61a7ebb3f04c7dc6)) - by @NotHyper-474 in [#5939](https://github.com/FunkinCrew/Funkin/pull/5939)
- The bricks in Stress (Pico Mix) no longer move after restarting the song. ([9cfee03](https://github.com/FunkinCrew/Funkin/commit/9cfee0398a057567f80ad68ff6052964da63af09)) - by @VirtuGuy in [#5875](https://github.com/FunkinCrew/Funkin/pull/5875)
- Pico now properly shoots after restarting Stress. ([b0ef0d3](https://github.com/FunkinCrew/funkin.assets/commit/b0ef0d352c199e93903652a95ffdf16a3dc2d569)) - by @7oltan in [funkin.assets#254](https://github.com/FunkinCrew/funkin.assets/pull/254)
- A-Bot's visualizer now zeroes out when restarting the song. ([ba3a8b0](https://github.com/FunkinCrew/funkin.assets/commit/ba3a8b0cbbf785a0729dcf85e25b6e59c0c12657)) - by @KoloInDaCrib in [funkin.assets#246](https://github.com/FunkinCrew/funkin.assets/pull/246)
- Notekinds now properly receive dispatched events last in PlayState. ([89b2b7f](https://github.com/FunkinCrew/Funkin/commit/89b2b7fcc3130db0af1f782594af72ae1f4ce320)) - by @Az989YT in [#5936](https://github.com/FunkinCrew/Funkin/pull/5936)
- Text from .json files is now properly sanitized before it is parsed. ([80d7ace](https://github.com/FunkinCrew/Funkin/commit/80d7ace714efb729cb8f1b23b61fdab4ac655275)) - by @KoloInDaCrib in [#5108](https://github.com/FunkinCrew/Funkin/pull/5108)
- Fixed a crash when exiting the Chart Editor after interacting with a text field. ([f85f2f0](https://github.com/FunkinCrew/Funkin/commit/f85f2f03d97fc1332275f088ddce2b4056f0ded2)) - by @NotHyper-474 in [#5992](https://github.com/FunkinCrew/Funkin/pull/5992)
- Fixed some issues with variation handling in the Chart Editor. ([970ccf2](https://github.com/FunkinCrew/Funkin/commit/970ccf2f4a9b5802b70d0b3c269c8b9e541d53f2)) - by @NotHyper-474 in [#4396](https://github.com/FunkinCrew/Funkin/pull/4396)
- Audio no longer plays when dragging a playhead in the Chart Editor. ([2d95754](https://github.com/FunkinCrew/Funkin/commit/2d95754d492da06920d56de2b2bcbb9b81182af0)) - by @Lasercar in [#4140](https://github.com/FunkinCrew/Funkin/pull/4140)
- The Chart Editor Offsets window song preview no longer continues playing during playtesting. ([11eb406](https://github.com/FunkinCrew/Funkin/commit/11eb4060d03b18a2f3604297eb9a24e54077fe3c)) - by @PurSnake and @Lasercar in [#5973](https://github.com/FunkinCrew/Funkin/pull/5973)
- The fullscreen keybind no longer triggers when typing in a field. ([4ebd00b](https://github.com/FunkinCrew/Funkin/commit/4ebd00bc06a5c94e935f557ba14ba9729faec104)) - by @lemz1 in [#4131](https://github.com/FunkinCrew/Funkin/pull/4131)
- The Stage Editor new stage menu can no longer be opened multiple times at once. ([47b1cee](https://github.com/FunkinCrew/Funkin/commit/47b1cee6d5ee37cd18146ca8098da85610c6b544)) - by @VioletSnowLeopard in [#5873](https://github.com/FunkinCrew/Funkin/pull/5873)
- Adjusted the save hotkeys in the Animation Editor to be usable on Windows. ([8ead5ee](https://github.com/FunkinCrew/Funkin/commit/8ead5eeb0840756a843fdcf0f3f347b087e7182d)) - by @VioletSnowLeopard in [#5974](https://github.com/FunkinCrew/Funkin/pull/5974)
- A few more tiny fixes.
## New Contributors for 0.7.5
* @Az989YT made their first contribution in [#5936](https://github.com/FunkinCrew/Funkin/pull/5936)
## [0.7.4] - 2025-09-01
### Added
@@ -47,7 +327,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improved properties support, behaving more like regular Haxe. (Thanks NotHyper-474!)
- Fixed a null object reference when evaluating static variables with no explicit values. (Thanks NotHyper-474!)
- Fixed a null object reference when iterating over a null variable. (Thanks NotHyper-474!)
- Scripts are now automatically cleared when Polymod is initialized. (Thanks KoloInDaKrib!)
- Scripts are now automatically cleared when Polymod is initialized. (Thanks KoloInDaCrib!)
- Polished every chart in the game!
- The lengths of hold notes have been adjusted to match the vocals as closely as possible, across all variations and difficulties.
- Easy and Normal charts from Week 1 to Week 3 have been adjusted for more consistency between the player and opponent.
+4 -4
View File
@@ -1,4 +1,4 @@
<div align='center'><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/d/d2/Friday_Night_Funkin%27_logo.svg/2560px-Friday_Night_Funkin%27_logo.svg.png" width="800">
<div align='center'><img src="docs/readme_images/FNF_logo.png" width="800">
<h2>Friday Night Funkin' is a rhythm game. Built using HaxeFlixel for <a href="https://ldjam.com/events/ludum-dare/47">Ludum Dare 47.</a></h2>
@@ -14,8 +14,8 @@ This game was made with love to Newgrounds and its community. Extra love to Tom
<div align='center'>
<table>
<tr>
<td><img src="https://fridaynightfunkin.wiki.gg/images/d/d7/Title_Card.gif" alt="Title Screen" width="350"/></td>
<td><img src="https://fridaynightfunkin.wiki.gg/images/9/99/Menu.png" alt="Main Menu" width="350"/></td>
<td><img src="docs/readme_images/Title_Card.gif" alt="Title Screen" width="350"/></td>
<td><img src="docs/readme_images/Menu.png" alt="Main Menu" width="350"/></td>
</tr>
</table>
</div>
@@ -63,4 +63,4 @@ Full credits can be found in-game, or in the `credits.json` file which is locate
## Special Thanks
- [Tom Fulp](https://twitter.com/tomfulp) - For being a great guy and for Newgrounds
- [JohnnyUtah](https://twitter.com/JohnnyUtahNG/) - Voice of Tankman
- [L0Litsmonica](https://twitter.com/L0Litsmonica) - Voice of Mommy Mearest
- [L0Litsmonica](https://twitter.com/L0Litsmonica) - Voice of Mommy Mearest
+1 -1
Submodule art updated: 094ff10919...2e68e371f8
+1 -1
Submodule assets updated: e6f463fb64...39d4b3fc95
+67 -16
View File
@@ -5,22 +5,6 @@
"blocksize": "10x10",
"colorprofile": "cl",
"custom": [
{
"asset": "asset/shared/images/resultScreen/results-pico/resultsPERFECT/spritemap1.png",
"blocksize": "4x4"
},
{
"asset": "asset/shared/images/resultScreen/results-pico/resultsGREAT/spritemap1.png",
"blocksize": "4x4"
},
{
"asset": "asset/shared/images/resultScreen/results-pico/resultsGOOD/spritemap1.png",
"blocksize": "4x4"
},
{
"asset": "asset/shared/images/resultScreen/results-pico/resultsSHIT/spritemap1.png",
"blocksize": "4x4"
},
{
"asset": "assets/preload/images/NOTE_assets.png",
"blocksize": "4x4"
@@ -37,10 +21,26 @@
"asset": "assets/preload/images/alphabet.png",
"blocksize": "4x4"
},
{
"asset": "assets/preload/images/charSelect/gfChill/spritemap1.png",
"blocksize": "6x6"
},
{
"asset": "assets/shared/images/NOTE_hold_assets.png",
"blocksize": "4x4"
},
{
"asset": "assets/shared/images/characters/nene-dark/spritemap1.png",
"blocksize": "6x6"
},
{
"asset": "assets/shared/images/characters/otis/muzzle-flashes/otis_flashes.png",
"blocksize": "4x4"
},
{
"asset": "assets/shared/images/characters/sserafim/chaewon/spritemap1.png",
"blocksize": "4x4"
},
{
"asset": "assets/shared/images/holdCoverBlue.png",
"blocksize": "4x4"
@@ -77,6 +77,30 @@
"asset": "assets/shared/images/notes.png",
"blocksize": "4x4"
},
{
"asset": "assets/shared/images/resultScreen/results-pico/resultsGOOD/spritemap1.png",
"blocksize": "4x4"
},
{
"asset": "assets/shared/images/resultScreen/results-pico/resultsGREAT/spritemap1.png",
"blocksize": "4x4"
},
{
"asset": "assets/shared/images/resultScreen/results-pico/resultsPERFECT/spritemap1.png",
"blocksize": "4x4"
},
{
"asset": "assets/shared/images/resultScreen/results-pico/resultsSHIT/spritemap1.png",
"blocksize": "4x4"
},
{
"asset": "assets/sserafim/images/sserafim-lipsync-yunjin/spritemap1.png",
"blocksize": "4x4"
},
{
"asset": "assets/sserafim/images/sserafim-lipsync/spritemap1.png",
"blocksize": "4x4"
},
{
"asset": "assets/week1/image/erect/bg.png",
"blocksize": "8x8"
@@ -92,6 +116,14 @@
{
"asset": "assets/week1/images/erect/server.png",
"blocksize": "8x8"
},
{
"asset": "assets/week7/images/erect/rando/spritemap1.png",
"blocksize": "6x6"
},
{
"asset": "assets/week7/images/erect/sniper/spritemap1.png",
"blocksize": "6x6"
}
],
"excludes": [
@@ -101,23 +133,42 @@
"assets/preload/images/icons/",
"assets/preload/images/soundtray/",
"assets/preload/images/stageBuild/",
"assets/preload/images/storymenu/titles/",
"assets/preload/images/titleEnter.png",
"assets/preload/images/titleEnter_mobile.png",
"assets/preload/images/ui/popup/pixel/*",
"assets/shared/images/characters/abotPixel/*",
"assets/shared/images/characters/bfPixel.png",
"assets/shared/images/characters/bfPixelsDEAD.png",
"assets/shared/images/characters/gf-christmas/spritemap1.png",
"assets/shared/images/characters/gf-dark/spritemap1.png",
"assets/shared/images/characters/gf/spritemap1.png",
"assets/shared/images/characters/gfCar/spritemap1.png",
"assets/shared/images/characters/gfPixel.png",
"assets/shared/images/characters/nene/spritemap1.png",
"assets/shared/images/characters/nenePixel/*",
"assets/shared/images/characters/pico-holding-nene/spritemap1.png",
"assets/shared/images/characters/picoPixel/*",
"assets/shared/images/characters/senpai.png",
"assets/shared/images/characters/spirit.png",
"assets/shared/images/characters/sserafim/eunchae/spritemap1.png",
"assets/shared/images/characters/sserafim/kazuha/spritemap1.png",
"assets/shared/images/characters/sserafim/sserafim-gf/spritemap1.png",
"assets/shared/images/characters/sserafim/yunjin/spritemap1.png",
"assets/shared/images/characters/sserafim/sakura/spritemap1.png",
"assets/shared/images/healthBar.png",
"assets/shared/images/resultScreen/*",
"assets/shared/images/resultScreen/clearPercent/",
"assets/shared/images/resultScreen/rankText/",
"assets/shared/images/resultScreen/results-bf/resultsEXCELLENT/spritemap1.png",
"assets/shared/images/resultScreen/results-bf/resultsGOOD/resultGirlfriendGOOD.png",
"assets/shared/images/resultScreen/results-bf/resultsGREAT/gf/spritemap1.png",
"assets/shared/images/resultScreen/results-bf/resultsSHIT/gf/spritemap1.png",
"assets/shared/images/ui/chart-editor/",
"assets/shared/images/ui/countdown/pixel/*",
"assets/sserafim/images/cutscene/cutsceneMain/spritemap1.png",
"assets/sserafim/images/cutscene/ref.png",
"assets/sserafim/images/ref.png",
"assets/week1/*",
"assets/week1/images/erect/brightLightSmall.png",
"assets/week1/images/erect/lightAbove.png",
+33 -20
View File
@@ -1,10 +1,9 @@
{
"extendsConfigPath": "",
"defineCombinations": [],
"defaultSeverity": "INFO",
"extendsConfigPath": "",
"numberOfCheckerThreads": 5,
"baseDefines": [],
"version": 1,
"exclude": {},
"defineCombinations": [],
"checks": [
{
"props": {},
@@ -40,12 +39,6 @@
"props": {},
"type": "AvoidStarImport"
},
{
"props": {
"severity": "IGNORE"
},
"type": "AvoidTernaryOperator"
},
{
"props": {},
"type": "BlockBreakingConditional"
@@ -71,7 +64,6 @@
{
"props": {
"severity": "IGNORE",
"policy": "aligned",
"allowSingleline": true
},
@@ -108,13 +100,12 @@
{
"props": {
"lineStyle": "onestar",
"startStyle": "twostars",
"severity": "IGNORE"
"startStyle": "ignore"
},
"type": "DocCommentStyle"
},
{
"props": {},
"props": { "severity": "IGNORE" },
"type": "Dynamic"
},
{
@@ -193,6 +184,14 @@
},
"type": "FileLength"
},
{
"type": "FileNameCase",
"props": {}
},
{
"type": "Final",
"props": {}
},
{
"props": {
"option": "upperCase"
@@ -245,7 +244,7 @@
{
"props": {
"max": 160,
"ignorePattern": "^$"
"ignorePattern": "^(\\s*['\"][^'\"]*['\"].*)$"
},
"type": "LineLength"
},
@@ -348,7 +347,7 @@
{
"props": {
"allowSingleLineStatement": true,
"tokens": ["FOR", "ELSE_IF", "WHILE", "DO_WHILE"]
"tokens": ["ELSE_IF", "WHILE", "DO_WHILE"]
},
"type": "NeedBraces"
},
@@ -580,6 +579,12 @@
"props": {},
"type": "TrailingWhitespace"
},
{
"type": "Type",
"props": {
"ignoreEnumAbstractValues": true
}
},
{
"props": {
"tokens": ["CLASS_DEF", "ENUM_DEF", "INTERFACE_DEF", "TYPEDEF_DEF"]
@@ -612,11 +617,18 @@
"type": "UnusedLocalVar"
},
{
"type": "VarTypeHint",
"props": {
"typeHintPolicy": "enforce_all",
"ignoreEnumAbstractValues": true
},
"type": "VarTypeHint"
"ignoreEnumAbstractValues": true,
"severity": "IGNORE"
}
},
{
"type": "VariableInitialisation",
"props": {
"allowFinal": false
}
},
{
"props": {
@@ -665,5 +677,6 @@
"type": "WhitespaceAround"
}
],
"numberOfCheckerThreads": 5
"exclude": {},
"version": 1
}
+1 -1
View File
@@ -60,7 +60,7 @@ This section provides guidelines to follow when [opening an issue](https://githu
## Requirements
Make sure you're playing:
- the latest version of the game (currently v0.7.4)
- the latest version of the game (currently v0.8.1)
- without any mods
- on [Newgrounds](https://www.newgrounds.com/portal/view/770371) or downloaded from [itch.io](https://ninja-muffin24.itch.io/funkin)
Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

+5
View File
@@ -0,0 +1,5 @@
Images stored in this folder come from these links:
FNF_logo.png - https://fridaynightfunkin.wiki.gg/images/FNF_logo.png
Menu.png - https://fridaynightfunkin.wiki.gg/images/Menu.png
Title_Card.gif - https://fridaynightfunkin.wiki.gg/images/d/d7/Title_Card.gif
+37
View File
@@ -0,0 +1,37 @@
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeIdentifier</key>
<string>me.funkin.fnfc</string>
<key>UTTypeDescription</key>
<string>FNFC Document</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.zip-archive</string>
</array>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>fnfc</string>
</array>
<key>public.mime-type</key>
<string>application/zip</string>
</dict>
</dict>
</array>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>FNFC Document</string>
<key>LSHandlerRank</key>
<string>Owner</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSItemContentTypes</key>
<array>
<string>me.funkin.fnfc</string>
</array>
</dict>
</array>
+40 -34
View File
@@ -4,8 +4,8 @@
"name": "FlxPartialSound",
"type": "git",
"dir": null,
"ref": "3c9f63e3501c20c0b60442089dc05306f5a87968",
"url": "https://github.com/FunkinDroidTeam/FlxPartialSound.git"
"ref": "26ed20b9c181a576f4cbe18ccdea38912c01476e",
"url": "https://github.com/FunkinCrew/FlxPartialSound.git"
},
{
"name": "astc-compressor",
@@ -18,7 +18,7 @@
"name": "extension-admob",
"type": "git",
"dir": null,
"ref": "02334589ff9603a5f483077a44395009644f6274",
"ref": "4c69bb43f1fb89fd6003dc98d2f3a9d75c5364b6",
"url": "https://github.com/FunkinCrew/extension-admob"
},
{
@@ -41,26 +41,33 @@
"type": "haxelib",
"version": "1.0.3"
},
{
"name": "extension-webviewcore",
"type": "git",
"dir": null,
"ref": "d13109450d65c40678244ca23b1aa88ac18d77e6",
"url": "https://github.com/LimeExtensions/extension-webviewcore"
},
{
"name": "flixel",
"type": "git",
"dir": null,
"ref": "ac2a34fe4b3400bc04218f46320a65af0f337fc0",
"ref": "c8e79d2a42f9c11c639519ef2a6fd7ef2379a8aa",
"url": "https://github.com/FunkinCrew/flixel"
},
{
"name": "flixel-addons",
"type": "git",
"dir": null,
"ref": "b9118f47f43a66bc0e5fbfcfd9903f0425e918ee",
"ref": "6fa30b3f5209146c852c25f3d1003e08898083e2",
"url": "https://github.com/FunkinCrew/flixel-addons"
},
{
"name": "flxanimate",
"name": "flixel-animate",
"type": "git",
"dir": null,
"ref": "49214278b9124823582cdcecd94f4a1de9a4b36b",
"url": "https://github.com/FunkinCrew/flxanimate"
"ref": "c61476f4b3a3d225631ab3065e4e925a4b63c076",
"url": "https://github.com/MaybeMaru/flixel-animate"
},
{
"name": "format",
@@ -71,14 +78,14 @@
"name": "funkin.vis",
"type": "git",
"dir": null,
"ref": "1966f8fbbbc509ed90d4b520f3c49c084fc92fd6",
"ref": "02bada154b474c2554709b9d12aef0cbf0da3ec9",
"url": "https://github.com/FunkinCrew/funkVis"
},
{
"name": "grig.audio",
"type": "git",
"dir": "src",
"ref": "8567c4dad34cfeaf2ff23fe12c3796f5db80685e",
"ref": "6409f3c6d1b4c52176813d3ede86c0d34e8af2c1",
"url": "https://github.com/FunkinCrew/grig.audio"
},
{
@@ -90,8 +97,8 @@
"name": "haxeui-core",
"type": "git",
"dir": null,
"ref": "47ee9f5fa02d422e186e844f87e68220cabfcc5b",
"url": "https://github.com/haxeui/haxeui-core"
"ref": "088b153b35985f8e4419ef710057ebd5b3749513",
"url": "https://github.com/FunkinCrew/haxeui-core"
},
{
"name": "haxeui-flixel",
@@ -104,29 +111,27 @@
"name": "hscript",
"type": "git",
"dir": null,
"ref": "8c1d238b069ef97cec13f5121b29d2afe2b8985d",
"ref": "dcc6f822e0866ca4fda300ed23e99559260f7bf5",
"url": "https://github.com/FunkinCrew/hscript"
},
{
"name": "hxcpp",
"type": "git",
"dir": null,
"ref": "5a0dc3f644dc676a4a092b7e6c8edc8be941f024",
"ref": "5932340d095a7eea8635fe4d1355f1c0efd0b3c2",
"url": "https://github.com/FunkinCrew/hxcpp"
},
{
"name": "hxcpp-debug-server",
"type": "git",
"dir": "hxcpp-debug-server",
"ref": "147294123f983e35f50a966741474438069a7a8f",
"ref": "7459934666a473a4cc4d066ba4a93ef92f1ce94c",
"url": "https://github.com/FunkinCrew/hxcpp-debugger"
},
{
"name": "hxdiscord_rpc",
"type": "git",
"dir": null,
"ref": "82c47ecc1a454b7dd644e4fcac7e91155f176dec",
"url": "https://github.com/FunkinCrew/hxdiscord_rpc"
"type": "haxelib",
"version": "1.3.0"
},
{
"name": "hxjsonast",
@@ -135,6 +140,11 @@
"ref": "20e72cc68c823496359775ac1f06500e67f189d5",
"url": "https://github.com/nadako/hxjsonast/"
},
{
"name": "hxgamemode",
"type": "haxelib",
"version": "1.0.1"
},
{
"name": "hxp",
"type": "haxelib",
@@ -143,55 +153,51 @@
{
"name": "hxvlc",
"type": "haxelib",
"version": "2.2.2"
"version": "2.2.5"
},
{
"name": "json2object",
"type": "git",
"dir": null,
"ref": "a8c26f18463c98da32f744c214fe02273e1823fa",
"ref": "a7e4cef3d444bea8d8244d6e59ab82f5a593f597",
"url": "https://github.com/FunkinCrew/json2object"
},
{
"name": "jsonpatch",
"type": "git",
"dir": null,
"ref": "f9b83215acd586dc28754b4ae7f69d4c06c3b4d3",
"url": "https://github.com/EliteMasterEric/jsonpatch"
"type": "haxelib",
"version": "1.1.0"
},
{
"name": "jsonpath",
"type": "git",
"dir": null,
"ref": "7a24193717b36393458c15c0435bb7c4470ecdda",
"url": "https://github.com/EliteMasterEric/jsonpath"
"type": "haxelib",
"version": "1.1.0"
},
{
"name": "lime",
"type": "git",
"dir": null,
"ref": "e5f8c27124598505917a001588b560244731adfb",
"ref": "318253ea3a28a3299a167748912c40444f403777",
"url": "https://github.com/FunkinCrew/lime"
},
{
"name": "newgrounds",
"type": "git",
"dir": null,
"ref": "c8b30832027fa04f48fe6a45fa7903d5b265d56e",
"url": "https://github.com/Geokureli/Newgrounds/"
"ref": "b2b53cd0a5f030af74efd325732951cddbea8826",
"url": "https://github.com/FunkinCrew/Newgrounds"
},
{
"name": "openfl",
"type": "git",
"dir": null,
"ref": "a0df7c3afe360c9af59a76e45007dbf4e53b5131",
"ref": "a69a6e778816c6a57eba7a91bfb0b60295508e88",
"url": "https://github.com/FunkinCrew/openfl"
},
{
"name": "polymod",
"type": "git",
"dir": null,
"ref": "55ee00236806330c89c3bff1e1563eb6425a9d96",
"ref": "9824bf4f00863c152d5afe3ac4bbcac173d80684",
"url": "https://github.com/larsiusprime/polymod"
},
{
+691 -88
View File
File diff suppressed because it is too large Load Diff
+83 -53
View File
@@ -1,18 +1,19 @@
package;
import lime.system.System;
import flixel.FlxG;
import flixel.FlxGame;
import flixel.FlxState;
import funkin.ui.FullScreenScaleMode;
import funkin.Preferences;
import funkin.PlayerSettings;
import funkin.util.logging.CrashHandler;
import funkin.ui.debug.MemoryCounter;
import funkin.ui.debug.FunkinDebugDisplay;
import funkin.ui.debug.FunkinDebugDisplay.DebugDisplayMode;
import funkin.save.Save;
import haxe.ui.Toolkit;
#if hxvlc
import hxvlc.util.Handle;
#end
import openfl.display.FPS;
import openfl.display.Sprite;
import openfl.events.Event;
import openfl.Lib;
@@ -20,6 +21,8 @@ import openfl.media.Video;
import openfl.net.NetStream;
import funkin.util.WindowUtil;
using funkin.util.AnsiUtil;
/**
* The main class which initializes HaxeFlixel and starts the game in its initial state.
*/
@@ -30,7 +33,6 @@ class Main extends Sprite
var initialState:Class<FlxState> = funkin.InitState; // The FlxState the game starts with.
var zoom:Float = -1; // If -1, zoom is automatically calculated to fit the window dimensions.
var skipSplash:Bool = true; // Whether to skip the flixel splash screen that appears in release mode.
var startFullscreen:Bool = false; // Whether to start the game in fullscreen on desktop targets
// You can pretty much ignore everything from here on - your code should go in your states.
@@ -60,6 +62,10 @@ class Main extends Sprite
haxe.Log.trace = funkin.util.logging.AnsiTrace.trace;
funkin.util.logging.AnsiTrace.traceBF();
// Get OpenFL to stop complaining so much.
// You can remove this line if you want to read debug messages.
openfl.utils._internal.Log.level = openfl.utils._internal.Log.LogLevel.INFO;
// Load mods to override assets.
// TODO: Replace with loadEnabledMods() once the user can configure the mod list.
funkin.modding.PolymodHandler.loadAllMods();
@@ -81,36 +87,45 @@ class Main extends Sprite
removeEventListener(Event.ADDED_TO_STAGE, init);
}
// Manually crash the game when using a software renderer in order to give a nicer error message.
var context = stage.window.context.type;
if (context != WEBGL && context != OPENGL && context != OPENGLES)
{
var tech:String = #if web "WebGL" #elseif desktop "OpenGL" #else "OpenGL ES" #end;
var requiredVersion:String = #if web '$tech 1.0 or newer' #elseif desktop '$tech 3.0 or newer' #else '$tech 2.0 or newer' #end;
var desc:String = 'Failed to initialize the $tech rendering context!\n\n';
#if web
desc += 'Make sure your graphics card supports $requiredVersion, your graphics drivers are up to date, and hardware acceleration is enabled on your browser.';
#elseif desktop
desc += 'Make sure your graphics card supports $requiredVersion, and your graphics drivers are up to date.';
#else
desc += 'Make sure your device supports $requiredVersion.';
#end
WindowUtil.showError('Failed to initialize $tech', desc);
System.exit(1);
}
setupGame();
}
var video:Video;
var netStream:NetStream;
var overlay:Sprite;
/**
* A frame counter displayed at the top left.
* The debug display at the top left.
*/
public static var fpsCounter:FPS;
/**
* A RAM counter displayed at the top left.
*/
public static var memoryCounter:MemoryCounter;
public static var debugDisplay:FunkinDebugDisplay;
function setupGame():Void
{
#if FEATURE_HAXEUI
initHaxeUI();
// addChild gets called by the user settings code.
fpsCounter = new FPS(10, 3, 0xFFFFFF);
#if !html5
// addChild gets called by the user settings code.
// TODO: disabled on HTML5 (todo: find another method that works?)
memoryCounter = new MemoryCounter(10, 13, 0xFFFFFF);
#end
// addChild gets called by the user settings code.
debugDisplay = new FunkinDebugDisplay(10, 10, 0xFFFFFF);
// Add this signal so the player can toggle the debug display using a hotkey.
FlxG.signals.postUpdate.add(handleDebugDisplayKeys);
#if mobile
// Add this signal so we can reposition and resize the memory and fps counter.
FlxG.signals.preUpdate.add(repositionCounters.bind(true));
@@ -121,7 +136,16 @@ class Main extends Sprite
#if hxvlc
// Initialize hxvlc's Handle here so the videos are loading faster.
Handle.init();
Handle.initAsync(function(success:Bool):Void {
if (success)
{
trace(' HXVLC '.bold().bg_orange() + ' LibVLC instance initialized!');
}
else
{
trace(' HXVLC '.bold().bg_orange() + ' LibVLC instance failed to initialize!');
}
});
#end
// Don't call anything from the preferences until the save is loaded!
@@ -133,7 +157,8 @@ class Main extends Sprite
WindowUtil.setVSyncMode(funkin.Preferences.vsyncMode);
var game:FlxGame = new FlxGame(gameWidth, gameHeight, initialState, Preferences.framerate, Preferences.framerate, skipSplash, startFullscreen);
var game:FlxGame = new FlxGame(gameWidth, gameHeight, initialState, Preferences.framerate, Preferences.framerate, skipSplash,
(FlxG.stage.window.fullscreen || Preferences.autoFullscreen));
// FlxG.game._customSoundTray wants just the class, it calls new from
// create() in there, which gets called when it's added to the stage
@@ -164,20 +189,43 @@ class Main extends Sprite
#end
}
#if FEATURE_HAXEUI
function initHaxeUI():Void
{
// This has to come before Toolkit.init since locales get initialized there
haxe.ui.locale.LocaleManager.instance.autoSetLocale = false;
// Calling this before any HaxeUI components get used is important:
// - It initializes the theme styles.
// - It scans the class path and registers any HaxeUI components.
Toolkit.init();
Toolkit.theme = 'dark'; // don't be cringe
// Toolkit.theme = 'light'; // embrace cringe
Toolkit.autoScale = false;
haxe.ui.Toolkit.init();
haxe.ui.Toolkit.theme = 'dark'; // don't be cringe
// haxe.ui.Toolkit.theme = 'light'; // embrace cringe
haxe.ui.Toolkit.autoScale = false;
// Don't focus on UI elements when they first appear.
haxe.ui.focus.FocusManager.instance.autoFocus = false;
funkin.input.Cursor.registerHaxeUICursors();
haxe.ui.tooltips.ToolTipManager.defaultDelay = 200;
}
#end
function handleDebugDisplayKeys():Void
{
if (PlayerSettings.player1.controls == null || !PlayerSettings.player1.controls.check(DEBUG_DISPLAY)) return;
var nextMode:DebugDisplayMode;
switch (Preferences.debugDisplay)
{
case DebugDisplayMode.Off:
nextMode = DebugDisplayMode.Simple;
case DebugDisplayMode.Simple:
nextMode = DebugDisplayMode.Advanced;
case DebugDisplayMode.Advanced:
nextMode = DebugDisplayMode.Off;
}
Preferences.debugDisplay = nextMode;
}
#if mobile
function repositionCounters(lerp:Bool):Void
@@ -191,41 +239,23 @@ class Main extends Sprite
scale = Math.min(scale, 1);
#end
final thypos:Float = Math.max(FullScreenScaleMode.notchSize.x, 10);
if (fpsCounter != null)
if (debugDisplay != null)
{
fpsCounter.scaleX = fpsCounter.scaleY = scale;
debugDisplay.scaleX = debugDisplay.scaleY = scale;
if (FlxG.game != null)
{
if (lerp)
{
fpsCounter.x = flixel.math.FlxMath.lerp(fpsCounter.x, FlxG.game.x + thypos, FlxG.elapsed * 3);
debugDisplay.x = flixel.math.FlxMath.lerp(debugDisplay.x, FlxG.game.x + thypos, FlxG.elapsed * 3);
}
else
{
fpsCounter.x = FlxG.game.x + FullScreenScaleMode.notchSize.x + 10;
debugDisplay.x = FlxG.game.x + FullScreenScaleMode.notchSize.x + 10;
}
fpsCounter.y = FlxG.game.y + (3 * scale);
}
}
if (memoryCounter != null)
{
memoryCounter.scaleX = memoryCounter.scaleY = scale;
if (FlxG.game != null)
{
if (lerp)
{
memoryCounter.x = flixel.math.FlxMath.lerp(memoryCounter.x, FlxG.game.x + thypos, FlxG.elapsed * 3);
}
else
{
memoryCounter.x = FlxG.game.x + FullScreenScaleMode.notchSize.x + 10;
}
memoryCounter.y = FlxG.game.y + (13 * scale);
debugDisplay.y = FlxG.game.y + (3 * scale);
}
}
}
+26 -5
View File
@@ -3,6 +3,9 @@ package source; // Yeah, I know...
import sys.FileSystem;
import sys.io.File;
using StringTools;
using tools.AnsiUtil;
/**
* A script which executes after the game is built.
*/
@@ -27,14 +30,32 @@ class Postbuild
sys.FileSystem.deleteFile(BUILD_TIME_FILE);
var buildTime:Float = roundToTwoDecimals(end - start);
Sys.println('[INFO] Build took: ${buildTime} seconds');
Sys.println(' INFO '.info() + ' Build took: ${format(end - start)}');
}
}
static function roundToTwoDecimals(value:Float):Float
static function format(time:Float, decimals:Int = 1):String
{
return Math.round(value * 100) / 100;
var units = [
{name: "day", secs: 86400},
{name: "hour", secs: 3600},
{name: "minute", secs: 60},
{name: "second", secs: 1}
];
var parts:Array<String> = [];
var remaining:Float = time;
var factor = Math.pow(10, decimals); // compute once because the old code was computing it twice.
for (u in units)
{
var value:Float = (u.name == "second") ? Math.round(remaining * factor) / factor : Math.floor(remaining / u.secs);
if (u.name != "second") remaining %= u.secs;
if (value > 0 || (u.name == "second" && parts.length == 0)) parts.push('${value} ${u.name}${value == 1 ? "" : "s"}');
}
return parts.join(" ");
}
}
+4 -3
View File
@@ -1,13 +1,14 @@
package funkin;
import openfl.utils.Future;
import funkin.util.macro.ConsoleMacro;
/**
* A wrapper around `openfl.utils.Assets` which disallows access to the harmful functions.
* Later we'll add Funkin-specific caching to this.
*/
@:nullSafety
class Assets
class Assets implements ConsoleClass
{
/**
* The assets cache.
@@ -114,9 +115,9 @@ class Assets
* @param path The asset path to load from
* @return The loaded Bitmap image
*/
public static function getBitmapData(path:String):openfl.display.BitmapData
public static function getBitmapData(path:String, useCache:Bool = true):openfl.display.BitmapData
{
return openfl.utils.Assets.getBitmapData(path);
return openfl.utils.Assets.getBitmapData(path, useCache);
}
/**
+222 -42
View File
@@ -18,20 +18,32 @@ import flixel.sound.FlxSound;
@:nullSafety
class Conductor
{
// onBeatHit is called every quarter note
// onStepHit is called every sixteenth note
// onBeatHit is called on every note determined by the denominator (4 = quarter, 8 = eighth, etc.)
// onStepHit is called on every note which is a quarter of a beat (4 = sixteenth, 8 = thirty-second, etc.)
// 4/4 = 4 beats per measure = 16 steps per measure
// 120 BPM = 120 quarter notes per minute = 2 onBeatHit per second
// 120 BPM = 480 sixteenth notes per minute = 8 onStepHit per second
// 60 BPM = 60 quarter notes per minute = 1 onBeatHit per second
// 60 BPM = 240 sixteenth notes per minute = 4 onStepHit per second
// 3/4 = 3 beats per measure = 12 steps per measure
// (IDENTICAL TO 4/4 but shorter measure length)
// (Identical to 4/4 but has a shorter measure length)
// 120 BPM = 120 quarter notes per minute = 2 onBeatHit per second
// 120 BPM = 480 sixteenth notes per minute = 8 onStepHit per second
// 60 BPM = 60 quarter notes per minute = 1 onBeatHit per second
// 60 BPM = 240 sixteenth notes per minute = 4 onStepHit per second
// 7/8 = 3.5 beats per measure = 14 steps per measure
// 7/8 = 7 beats per measure = 28 steps per measure
// Beats are EIGHTH NOTES!!
// (Identical to 7/4 but beats happen twice as fast)
// 120 BPM = 240 eighth notes per minute = 4 onBeatHit per second
// 120 BPM = 960 twenty-second notes per minute = 16 onStepHit per second
// 15/16 = 15 beats per measure = 60 steps per measure
// Beats are SIXTEENTH NOTES!!
// 120 BPM = 480 sixteenth notes per minute = 8 onBeatHit per second
// 120 BPM = 1920 sixty-fourth notes per minute = 32 onStepHit per second
// 3/2 = 3 beats per measure = 12 steps per measure
// Beats are HALF NOTES!!
// 120 BPM = 60 half notes per minute = 1 onBeatHit per second
// 120 BPM = 240 eighth notes per minute = 4 onStepHit per second
/**
* The current instance of the Conductor.
@@ -113,7 +125,6 @@ class Conductor
if (bpmOverride != null) return bpmOverride;
if (currentTimeChange == null) return Constants.DEFAULT_BPM;
@:privateAccess
if (PlayState.instance != null && PlayState.instance.startingSong)
{
@@ -160,24 +171,24 @@ class Conductor
}
/**
* Duration of a beat (quarter note) in milliseconds. Calculated based on bpm.
* Duration of a beat in milliseconds. Calculated based on bpm.
*/
public var beatLengthMs(get, never):Float;
function get_beatLengthMs():Float
{
// Tied directly to BPM.
return ((Constants.SECS_PER_MIN / bpm) * Constants.MS_PER_SEC);
return ((Constants.SECS_PER_MIN / bpm) * Constants.MS_PER_SEC) * (4 / timeSignatureDenominator);
}
/**
* Duration of a step (sixtennth note) in milliseconds. Calculated based on bpm.
* Duration of a step in milliseconds. Calculated based on bpm.
*/
public var stepLengthMs(get, never):Float;
function get_stepLengthMs():Float
{
return beatLengthMs / timeSignatureNumerator;
return beatLengthMs / Constants.STEPS_PER_BEAT;
}
/**
@@ -246,7 +257,7 @@ class Conductor
function get_instrumentalOffsetSteps():Float
{
var startingStepLengthMs:Float = ((Constants.SECS_PER_MIN / startingBPM) * Constants.MS_PER_SEC) / timeSignatureNumerator;
var startingStepLengthMs:Float = (((Constants.SECS_PER_MIN / startingBPM) * Constants.MS_PER_SEC) * (4 / timeSignatureDenominator)) / Constants.STEPS_PER_BEAT;
return instrumentalOffset / startingStepLengthMs;
}
@@ -288,26 +299,23 @@ class Conductor
}
/**
* The number of beats in a measure. May be fractional depending on the time signature.
* The number of beats in a measure.
*/
public var beatsPerMeasure(get, never):Float;
function get_beatsPerMeasure():Float
{
// NOTE: Not always an integer, for example 7/8 is 3.5 beats per measure
return stepsPerMeasure / Constants.STEPS_PER_BEAT;
return timeSignatureNumerator;
}
/**
* The number of steps in a measure.
* TODO: I don't think this can be fractional?
*/
public var stepsPerMeasure(get, never):Int;
function get_stepsPerMeasure():Int
{
// TODO: Is this always an integer?
return Std.int(timeSignatureNumerator / timeSignatureDenominator * Constants.STEPS_PER_BEAT * Constants.STEPS_PER_BEAT);
return Std.int(timeSignatureNumerator * Constants.STEPS_PER_BEAT);
}
/**
@@ -389,11 +397,11 @@ class Conductor
{
if (bpm != null)
{
trace('[CONDUCTOR] Forcing BPM to ${bpm}');
log('Forcing BPM to ${bpm}');
}
else
{
trace('[CONDUCTOR] Resetting BPM to default');
log('Resetting BPM to default');
}
this.bpmOverride = bpm;
@@ -452,7 +460,7 @@ class Conductor
if (currentTimeChange == null && bpmOverride == null && FlxG.sound.music != null)
{
trace('WARNING: Conductor is broken, timeChanges is empty.');
log(' WARNING '.warning() + 'Conductor is broken, timeChanges is empty.');
}
else if (currentTimeChange != null && this.songPosition > 0.0)
{
@@ -460,7 +468,7 @@ class Conductor
this.currentStepTime = FlxMath.roundDecimal((currentTimeChange.beatTime * Constants.STEPS_PER_BEAT)
+ (this.songPosition - currentTimeChange.timeStamp) / stepLengthMs, 6);
this.currentBeatTime = currentStepTime / Constants.STEPS_PER_BEAT;
this.currentMeasureTime = currentStepTime / stepsPerMeasure;
this.currentMeasureTime = getTimeInMeasures(this.songPosition);
this.currentStep = Math.floor(currentStepTime);
this.currentBeat = Math.floor(currentBeatTime);
this.currentMeasure = Math.floor(currentMeasureTime);
@@ -517,7 +525,7 @@ class Conductor
/**
* Can be called in-between frames, usually for input related things
* that can potentially get processed on exact milliseconds/timestmaps.
* that can potentially get processed on exact milliseconds/timestamps.
* If you need song position, use `Conductor.instance.songPosition` instead
* for use in update() related functions.
* @param soundToCheck Which FlxSound object to check, defaults to FlxG.sound.music if no input
@@ -526,12 +534,9 @@ class Conductor
public function getTimeWithDiff(?soundToCheck:FlxSound):Float
{
if (soundToCheck == null) soundToCheck = FlxG.sound.music;
// trace(this.songPosition);
@:privateAccess
this.songPosition = soundToCheck._channel.position;
// return this.songPosition + (Std.int(Timer.stamp() * 1000) - prevTimestamp);
// trace("\t--> " + this.songPosition);
return this.songPosition;
}
@@ -566,7 +571,8 @@ class Conductor
{
var prevTimeChange:SongTimeChange = timeChanges[timeChanges.length - 1];
songTimeChange.beatTime = FlxMath.roundDecimal(prevTimeChange.beatTime
+ ((songTimeChange.timeStamp - prevTimeChange.timeStamp) * prevTimeChange.bpm / Constants.SECS_PER_MIN / Constants.MS_PER_SEC),
+
((songTimeChange.timeStamp - prevTimeChange.timeStamp) * prevTimeChange.bpm / Constants.SECS_PER_MIN / Constants.MS_PER_SEC * (prevTimeChange.timeSignatureDen / 4)),
4);
}
}
@@ -574,15 +580,118 @@ class Conductor
timeChanges.push(songTimeChange);
}
if (timeChanges.length > 0)
if (timeChanges.length == 1)
{
trace('Done mapping time changes: ${timeChanges}');
log('Done mapping single time change to ${timeChanges[0].bpm} BPM');
}
else if (timeChanges.length > 1)
{
log('Done mapping ${timeChanges.length} time changes (starting at ${timeChanges[0].bpm} BPM)');
}
else
{
log(' WARNING '.warning() + ' Conductor mapped no time changes?');
}
// Update currentStepTime
this.update(this.songPosition, false);
}
/**
* Given a time in milliseconds, return a time in measures.
* @param ms The time in milliseconds.
* @return The time in measures.
*/
public function getTimeInMeasures(ms:Float):Float
{
if (timeChanges.length == 0)
{
// Assume a constant BPM equal to the forced value.
return ms / stepLengthMs / stepsPerMeasure;
}
else
{
var resultMeasureTime:Float = 0;
ms = ms < 0 ? 0 : ms;
var lastTimeChange:SongTimeChange = timeChanges[0];
var i:Int = -1;
for (timeChange in timeChanges)
{
if (ms >= timeChange.timeStamp)
{
// We do NOT want to add the current time change's MEASURE LENGTH to the result, same for if we're inside the song's last time change.
if (ms < timeChange.timeStamp || i == timeChanges.length - 1)
{
// However, we still want this for the ending calculation.
lastTimeChange = timeChange;
break;
}
var currentStepLengthMs:Float = (((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) * (4 / lastTimeChange.timeSignatureDen)) / Constants.STEPS_PER_BEAT;
var currentStepsPerMeasure:Int = lastTimeChange.timeSignatureNum * Constants.STEPS_PER_BEAT;
resultMeasureTime += (timeChange.timeStamp - lastTimeChange.timeStamp) / currentStepLengthMs / currentStepsPerMeasure;
lastTimeChange = timeChange;
}
i++;
}
var remainingStepLengthMs:Float = (((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) * (4 / lastTimeChange.timeSignatureDen)) / Constants.STEPS_PER_BEAT;
var remainingStepsPerMeasure:Int = lastTimeChange.timeSignatureNum * Constants.STEPS_PER_BEAT;
var remainingFractionalMeasure:Float = (ms - lastTimeChange.timeStamp) / remainingStepLengthMs / remainingStepsPerMeasure;
resultMeasureTime += remainingFractionalMeasure;
return resultMeasureTime;
}
}
/**
* Given a time in measures and fractional measures, return a time in milliseconds.
* @param measureTime The time in measures.
* @return The time in milliseconds.
*/
public function getMeasureTimeInMs(measureTime:Float):Float
{
if (timeChanges.length == 0)
{
// Assume a constant BPM equal to the forced value.
return measureTime * stepLengthMs * stepsPerMeasure;
}
else
{
var resultMs:Float = 0;
measureTime = measureTime < 0 ? 0 : measureTime;
var lastTimeChange:SongTimeChange = timeChanges[0];
var i:Int = -1;
for (timeChange in timeChanges)
{
var currentTimeChangeMeasureTime:Float = getTimeInMeasures(timeChange.timeStamp);
if (measureTime >= currentTimeChangeMeasureTime)
{
// We do NOT want to add the current time change's MEASURE LENGTH to the result, same for if we're inside the song's last time change.
if (measureTime < currentTimeChangeMeasureTime || i == timeChanges.length - 1)
{
// However, we still want this for the ending calculation.
lastTimeChange = timeChange;
break;
}
var currentStepLengthMs:Float = (((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) * (4 / lastTimeChange.timeSignatureDen)) / Constants.STEPS_PER_BEAT;
var currentStepsPerMeasure:Int = lastTimeChange.timeSignatureNum * Constants.STEPS_PER_BEAT;
resultMs += (currentTimeChangeMeasureTime - getTimeInMeasures(lastTimeChange.timeStamp)) * currentStepLengthMs * currentStepsPerMeasure;
lastTimeChange = timeChange;
}
i++;
}
var remainingStepLengthMs:Float = (((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) * (4 / lastTimeChange.timeSignatureDen)) / Constants.STEPS_PER_BEAT;
var remainingStepsPerMeasure:Int = lastTimeChange.timeSignatureNum * Constants.STEPS_PER_BEAT;
var remainingFractionalMeasure:Float = (measureTime - getTimeInMeasures(lastTimeChange.timeStamp)) * remainingStepLengthMs * remainingStepsPerMeasure;
resultMs += remainingFractionalMeasure;
return resultMs;
}
}
/**
* Given a time in milliseconds, return a time in steps.
* @param ms The time in milliseconds.
@@ -598,23 +707,28 @@ class Conductor
else
{
var resultStep:Float = 0;
ms = ms < 0 ? 0 : ms;
var lastTimeChange:SongTimeChange = timeChanges[0];
var i:Int = -1;
for (timeChange in timeChanges)
{
if (ms >= timeChange.timeStamp)
{
// We do NOT want to add the current time change's STEP LENGTH to the result, same for if we're inside the song's last time change.
if (ms < timeChange.timeStamp || i == timeChanges.length - 1)
{
// However, we still want this for the ending calculation.
lastTimeChange = timeChange;
break;
}
resultStep += (timeChange.beatTime - lastTimeChange.beatTime) * Constants.STEPS_PER_BEAT;
lastTimeChange = timeChange;
resultStep = lastTimeChange.beatTime * Constants.STEPS_PER_BEAT;
}
else
{
// This time change is after the requested time.
break;
}
i++;
}
var lastStepLengthMs:Float = ((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) / timeSignatureNumerator;
var lastStepLengthMs:Float = (((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) * (4 / lastTimeChange.timeSignatureDen)) / Constants.STEPS_PER_BEAT;
var resultFractionalStep:Float = (ms - lastTimeChange.timeStamp) / lastStepLengthMs;
resultStep += resultFractionalStep;
@@ -637,23 +751,28 @@ class Conductor
else
{
var resultMs:Float = 0;
stepTime = stepTime < 0 ? 0 : stepTime;
var lastTimeChange:SongTimeChange = timeChanges[0];
var i:Int = -1;
for (timeChange in timeChanges)
{
if (stepTime >= timeChange.beatTime * Constants.STEPS_PER_BEAT)
{
// We do NOT want to add the current time change's TIME LENGTH to the result, same for if we're inside the song's last time change.
if (stepTime < (timeChange.beatTime * Constants.STEPS_PER_BEAT) || i == timeChanges.length - 1)
{
// However, we still want this for the ending calculation.
lastTimeChange = timeChange;
break;
}
resultMs += timeChange.timeStamp - lastTimeChange.timeStamp;
lastTimeChange = timeChange;
resultMs = lastTimeChange.timeStamp;
}
else
{
// This time change is after the requested time.
break;
}
i++;
}
var lastStepLengthMs:Float = ((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) / timeSignatureNumerator;
var lastStepLengthMs:Float = (((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) * (4 / lastTimeChange.timeSignatureDen)) / Constants.STEPS_PER_BEAT;
resultMs += (stepTime - lastTimeChange.beatTime * Constants.STEPS_PER_BEAT) * lastStepLengthMs;
return resultMs;
@@ -691,13 +810,69 @@ class Conductor
}
}
var lastStepLengthMs:Float = ((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) / timeSignatureNumerator;
var lastStepLengthMs:Float = (((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) * (4 / lastTimeChange.timeSignatureDen)) / Constants.STEPS_PER_BEAT;
resultMs += (beatTime - lastTimeChange.beatTime) * lastStepLengthMs * Constants.STEPS_PER_BEAT;
return resultMs;
}
}
/**
* Given a time in milliseconds, return the time change that time is inside of.
* @param ms The time in milliseconds.
* @return The resulting time change.
*/
public function getTimeChange(ms:Float):SongTimeChange
{
if (timeChanges.length == 0)
{
return new SongTimeChange(0, 100);
}
var i:Int = 0;
ms = ms < 0 ? 0 : ms;
for (timeChange in timeChanges)
{
i++;
if ((i == timeChanges.length && ms >= timeChange.timeStamp) || (ms >= timeChange.timeStamp && ms < timeChanges[i].timeStamp))
{
return timeChange;
}
}
return new SongTimeChange(0, 100);
}
/**
* An all-in-one function for getting either a step, beat, or measure's length in milliseconds from a given time change.
* @param ms The time in milliseconds. The time change is determined by this.
* @param type The type of length to return. Either "step", "beat", or "measure" works, along with their first character.
* @return The length of a step/beat/measure in milliseconds.
*/
public function getTypeLengthAtMs(ms:Float, type:String = "beat"):Float
{
if (timeChanges.length == 0) return 0;
var wantedTimeChange:SongTimeChange = timeChanges[0];
for (timeChange in timeChanges)
{
if (ms >= timeChange.timeStamp)
{
wantedTimeChange = timeChange;
}
else
{
// This time change is after the requested time.
break;
}
}
var wantedBeatLengthMs:Float = ((Constants.SECS_PER_MIN / wantedTimeChange.bpm) * Constants.MS_PER_SEC) * (4 / wantedTimeChange.timeSignatureDen);
return switch (type.toLowerCase())
{
case "measure", "m": wantedBeatLengthMs * wantedTimeChange.timeSignatureNum;
case "beat", "b": wantedBeatLengthMs;
case "step", "s": wantedBeatLengthMs / Constants.STEPS_PER_BEAT;
default: wantedBeatLengthMs;
}
}
/**
* Adds Conductor fields to the Flixel debugger variable display.
* @param conductorToUse The conductor to use. Defaults to `Conductor.instance`.
@@ -712,4 +887,9 @@ class Conductor
FlxG.watch.addQuick('currentBeatTime', target.currentBeatTime);
FlxG.watch.addQuick('currentStepTime', target.currentStepTime);
}
static function log(message:String):Void
{
trace(' CONDUCTOR '.bg_purple().bold() + ' ${message}');
}
}
+28 -10
View File
@@ -111,6 +111,16 @@ class FunkinMemory
///// TEXTURES /////
/**
* Determine whether the texture with the given key is cached.
* @param key The key of the texture to check.
* @return Whether the texture is cached.
*/
public static function isTextureCached(key:String):Bool
{
return FlxG.bitmap.get(key) != null;
}
/**
* Ensures a texture with the given key is cached.
* @param key The key of the texture to cache.
@@ -135,7 +145,7 @@ class FunkinMemory
return;
}
trace('Successfully cached graphic: $key');
log('Cached asset $key');
graphic.persist = true;
currentCachedTextures.set(key, graphic);
forceRender(graphic);
@@ -156,11 +166,11 @@ class FunkinMemory
return;
}
trace('Successfully cached graphic: $key');
log('Cached graphic $key');
graphic.persist = true;
permanentCachedTextures.set(key, graphic);
forceRender(graphic);
currentCachedTextures = permanentCachedTextures;
currentCachedTextures = permanentCachedTextures.copy();
}
/**
@@ -177,6 +187,7 @@ class FunkinMemory
var sprite = new flixel.FlxSprite();
sprite.loadGraphic(graphic);
sprite.draw(); // Draw sprite and load it into game's memory.
graphic.bitmap?.getTexture(FlxG.stage.context3D); // Just in case that didn't work...
sprite.destroy();
}
@@ -200,7 +211,7 @@ class FunkinMemory
*/
public inline static function preparePurgeTextureCache():Void
{
previousCachedTextures = currentCachedTextures;
previousCachedTextures = currentCachedTextures.copy();
for (graphicKey in previousCachedTextures.keys())
{
@@ -210,7 +221,7 @@ class FunkinMemory
}
}
currentCachedTextures = permanentCachedTextures;
currentCachedTextures = permanentCachedTextures.copy();
}
/**
@@ -232,6 +243,7 @@ class FunkinMemory
if (graphic != null)
{
FlxG.bitmap.remove(graphic);
graphic.persist = false;
graphic.destroy();
previousCachedTextures.remove(graphicKey);
Assets.cache.clear(graphicKey);
@@ -249,7 +261,7 @@ class FunkinMemory
{
var obj:Null<FlxGraphic> = FlxG.bitmap.get(key);
if (obj == null || obj.persist || permanentCachedTextures.exists(key) || key.contains("fonts"))
if (obj == null || (obj.persist && permanentCachedTextures.exists(key)) || key.contains("fonts"))
{
continue;
}
@@ -261,6 +273,7 @@ class FunkinMemory
if (key.contains(purgeEntry))
{
FlxG.bitmap.removeKey(key);
obj.persist = false;
obj.destroy();
}
}
@@ -345,7 +358,7 @@ class FunkinMemory
public static function preparePurgeSoundCache():Void
{
previousCachedSounds = currentCachedSounds;
previousCachedSounds = currentCachedSounds.copy();
for (key in previousCachedSounds.keys())
{
@@ -355,7 +368,7 @@ class FunkinMemory
}
}
currentCachedSounds = permanentCachedSounds;
currentCachedSounds = permanentCachedSounds.copy();
}
/**
@@ -408,7 +421,7 @@ class FunkinMemory
@:privateAccess
for (key in keysToRemove)
{
trace('Cleaning up $key');
log('Cleaning asset $key');
var obj:Null<FlxGraphic> = FlxG.bitmap.get(key);
if (obj != null)
{
@@ -439,7 +452,7 @@ class FunkinMemory
@:privateAccess
for (key in keysToRemove)
{
trace('Cleaning up $key');
log('Cleaning asset $key');
var obj:Null<FlxGraphic> = FlxG.bitmap.get(key);
if (obj != null)
{
@@ -450,4 +463,9 @@ class FunkinMemory
Assets.cache.clear(key);
}
}
static function log(message:String):Void
{
trace(' MEMORY '.bg_bright_lilac().bold() + ' ${message}');
}
}
+98 -16
View File
@@ -24,7 +24,7 @@ import funkin.data.event.SongEventRegistry;
import funkin.data.stage.StageRegistry;
import funkin.data.story.level.LevelRegistry;
import funkin.modding.module.ModuleHandler;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.data.character.CharacterData.CharacterDataParser;
import funkin.play.notes.notekind.NoteKindManager;
import funkin.play.PlayStatePlaylist;
import funkin.ui.debug.charting.ChartEditorState;
@@ -37,6 +37,7 @@ import funkin.util.macro.MacroUtil;
import funkin.util.TrackerUtil;
import funkin.util.WindowUtil;
import openfl.display.BitmapData;
import funkin.ui.debug.playtest.ChartPlaytestMenu;
#if FEATURE_DISCORD_RPC
import funkin.api.discord.DiscordClient;
#end
@@ -118,6 +119,11 @@ class InitState extends FlxState
funkin.mobile.util.InAppReviewUtil.init();
#end
#if FEATURE_MOBILE_WEBVIEW
// Setup WebView
funkin.mobile.util.WebViewUtil.init();
#end
#if android
// Setup Callback util.
funkin.external.android.CallbackUtil.init();
@@ -125,7 +131,12 @@ class InitState extends FlxState
#if ios
// Setup Audio session
funkin.external.ios.AudioSession.initialize();
funkin.external.apple.AudioSession.initialize();
#end
#if mobile
// Setup Mobile FNFC launcher.
funkin.mobile.util.FNFCUtil.init();
#end
// This ain't a pixel art game! (most of the time)
@@ -147,6 +158,11 @@ class InitState extends FlxState
// Set the game to a lower frame rate while it is in the background.
FlxG.game.focusLostFramerate = 30;
// Persist controls inputs between states.
// Without this, the game would release your keybinds when you switch states,
// and then act like you released and re-pressed them the frame after.
FlxG.inputs.resetOnStateSwitch = false;
// Makes Flixel use frame times instead of locked movements per frame for things like tweens
FlxG.fixedTimestep = false;
@@ -206,6 +222,10 @@ class InitState extends FlxState
});
#end
#if FEATURE_LOST_FOCUS_VOLUME
FlxG.signals.focusLost.add(onLostFocus);
FlxG.signals.focusGained.add(onGainFocus);
#end
//
// ANDROID SETUP
//
@@ -267,7 +287,7 @@ class InitState extends FlxState
// Move it to use a BaseRegistry.
CharacterDataParser.loadCharacterCache();
NoteKindManager.loadScripts();
NoteKindManager.initialize();
ModuleHandler.buildModuleCallbacks();
ModuleHandler.loadModuleCache();
@@ -281,6 +301,23 @@ class InitState extends FlxState
#end
}
#if FEATURE_LOST_FOCUS_VOLUME
@:noCompletion var _lastFocusVolume:Null<Float>;
function onLostFocus()
{
if (FlxG.sound.muted || FlxG.sound.volume == 0 || FlxG.autoPause) return;
_lastFocusVolume = FlxG.sound.volume;
FlxG.sound.volume *= Constants.LOST_FOCUS_VOLUME_MULTIPLIER;
}
function onGainFocus()
{
if (FlxG.sound.muted || FlxG.autoPause) return;
if (_lastFocusVolume != null) FlxG.sound.volume = _lastFocusVolume;
}
#end
/**
* Start the game.
*
@@ -374,22 +411,64 @@ class InitState extends FlxState
if (params.chart.shouldLoadChart)
{
#if FEATURE_CHART_EDITOR
FlxG.switchState(() -> new ChartEditorState(
{
fnfcTargetPath: params.chart.chartPath,
}));
#else
FlxG.switchState(() -> new TitleState());
#end
}
else if (params.stage.shouldLoadStage)
{
#if FEATURE_STAGE_EDITOR
FlxG.switchState(() -> new StageEditorState(
{
fnfsTargetPath: params.stage.stagePath,
}));
#else
FlxG.switchState(() -> new TitleState());
#end
}
else if (params.song.shouldLoadSong && params.song.songPath != null)
{
#if sys
FlxG.switchState(() -> new ChartPlaytestMenu(params.song.songPath));
#else
FlxG.switchState(() -> new TitleState());
#end
}
else
{
// FlxG.sound.cache(Paths.music('freakyMenu/freakyMenu'));
#if mobile
funkin.mobile.util.FNFCUtil.onFNFCOpen.add(function(fnfcFile:String) {
flixel.tweens.FlxTween.globalManager.clear();
flixel.util.FlxTimer.globalManager.clear();
@:nullSafety(Off)
if (FlxG.sound.music != null)
{
FlxG.sound.music.destroy();
FlxG.sound.music = null;
}
FlxG.switchState(() -> new ChartPlaytestMenu(fnfcFile));
});
final fnfcFile = funkin.mobile.util.FNFCUtil.queryFNFC();
if (fnfcFile != null)
{
trace('launching FNFC from $fnfcFile');
FlxG.switchState(() -> new ChartPlaytestMenu(fnfcFile));
}
else
{
FlxG.switchState(() -> new TitleState());
}
#else
FlxG.switchState(() -> new TitleState());
#end
}
}
@@ -510,19 +589,6 @@ class InitState extends FlxState
// Disable using ~ to open the console (we use that for the Editor menu)
FlxG.debugger.toggleKeys = [F2];
TrackerUtil.initTrackers();
// Adds an additional Close Debugger button.
// This big obnoxious white button is for MOBILE, so that you can press it
// easily with your finger when debug bullshit pops up during testing lol!
FlxG.debugger.addButton(LEFT, new BitmapData(200, 200), function() {
FlxG.debugger.visible = false;
// Make errors and warnings less annoying.
// Forcing this always since I have never been happy to have the debugger to pop up
LogStyle.ERROR.openConsole = false;
LogStyle.ERROR.errorSound = null;
LogStyle.WARNING.openConsole = false;
LogStyle.WARNING.errorSound = null;
});
// Adds a red button to the debugger.
// This pauses the game AND the music! This ensures the Conductor stops.
@@ -565,6 +631,22 @@ class InitState extends FlxState
FlxG.sound.music.pause();
FlxG.sound.music.time += FlxG.elapsed * 1000;
});
// Adds an additional Close Debugger button.
// This big obnoxious white button is for MOBILE, so that you can press it
// easily with your finger when debug bullshit pops up during testing lol!
#if mobile
FlxG.debugger.addButton(LEFT, new BitmapData(200, 200), function() {
FlxG.debugger.visible = false;
// Make errors and warnings less annoying.
// Forcing this always since I have never been happy to have the debugger to pop up
LogStyle.ERROR.openConsole = false;
LogStyle.ERROR.errorSound = null;
LogStyle.WARNING.openConsole = false;
LogStyle.WARNING.errorSound = null;
});
#end // mobile big butotn crap
#end
}
+60 -1
View File
@@ -1,13 +1,17 @@
package funkin;
import flixel.graphics.frames.FlxAtlasFrames;
import animate.FlxAnimateFrames;
import funkin.graphics.FunkinSprite.AtlasSpriteSettings;
import openfl.utils.AssetType;
import funkin.util.macro.ConsoleMacro;
import haxe.io.Path;
/**
* A core class which handles determining asset paths.
*/
@:nullSafety
class Paths
class Paths implements ConsoleClass
{
static var currentLevel:Null<String> = null;
@@ -103,6 +107,11 @@ class Paths
return getPath('data/$key.json', TEXT, library);
}
public static function srt(key:String, ?library:String, ?directory:String = "data/"):String
{
return getPath('$directory$key.srt', TEXT, library);
}
public static function sound(key:String, ?library:String):String
{
return getPath('sounds/$key.${Constants.EXT_SOUND}', SOUND, library);
@@ -120,6 +129,13 @@ class Paths
public static function videos(key:String, ?library:String):String
{
final path:Path = new Path(key);
if (path.ext != null)
{
return getPath('videos/${path.file}.${path.ext}', BINARY, library ?? 'videos');
}
return getPath('videos/$key.${Constants.EXT_VIDEO}', BINARY, library ?? 'videos');
}
@@ -163,6 +179,49 @@ class Paths
return FlxAtlasFrames.fromSparrow(image(key, library), file('images/$key.xml', library));
}
public static function getAnimateAtlas(key:String, ?library:String, settings:AtlasSpriteSettings):FlxAnimateFrames
{
var assetLibrary:String = library ?? "";
var graphicKey:String = "";
if (assetLibrary != "")
{
graphicKey = Paths.animateAtlas(key, assetLibrary);
}
else
{
graphicKey = Paths.animateAtlas(key);
}
var validatedSettings:AtlasSpriteSettings =
{
swfMode: settings?.swfMode ?? false,
cacheOnLoad: settings?.cacheOnLoad ?? false,
filterQuality: settings?.filterQuality ?? MEDIUM,
spritemaps: settings?.spritemaps ?? null,
metadataJson: settings?.metadataJson ?? null,
cacheKey: settings?.cacheKey ?? null,
uniqueInCache: settings?.uniqueInCache ?? false,
onSymbolCreate: settings?.onSymbolCreate ?? null,
applyStageMatrix: settings?.applyStageMatrix ?? false,
useRenderTexture: settings?.useRenderTexture ?? false
};
// Validate asset path.
if (!Assets.exists('${graphicKey}/Animation.json'))
{
throw 'No Animation.json file exists at the specified path (${graphicKey})';
}
return FlxAnimateFrames.fromAnimate(graphicKey, validatedSettings.spritemaps, validatedSettings.metadataJson, validatedSettings.cacheKey,
validatedSettings.uniqueInCache, {
swfMode: validatedSettings.swfMode,
cacheOnLoad: validatedSettings.cacheOnLoad,
filterQuality: validatedSettings.filterQuality,
onSymbolCreate: validatedSettings.onSymbolCreate
});
}
public static function getPackerAtlas(key:String, ?library:String):FlxAtlasFrames
{
return FlxAtlasFrames.fromSpriteSheetPacker(image(key, library), file('images/$key.txt', library));
+89 -57
View File
@@ -7,6 +7,7 @@ import funkin.mobile.util.InAppPurchasesUtil;
import funkin.save.Save;
import funkin.util.WindowUtil;
import funkin.util.HapticUtil.HapticsMode;
import funkin.ui.debug.FunkinDebugDisplay.DebugDisplayMode;
/**
* A core class which provides a store of user-configurable, globally relevant values.
@@ -43,7 +44,7 @@ class Preferences
#else
var save:Save = Save.instance;
save.options.framerate = value;
save.flush();
Save.system.flush();
FlxG.updateFramerate = value;
FlxG.drawFramerate = value;
return value;
@@ -73,7 +74,7 @@ class Preferences
var save:Save = Save.instance;
save.options.naughtyness = value;
save.flush();
Save.system.flush();
return value;
}
@@ -92,7 +93,7 @@ class Preferences
{
var save:Save = Save.instance;
save.options.downscroll = value;
save.flush();
Save.system.flush();
return value;
}
@@ -111,7 +112,7 @@ class Preferences
{
var save:Save = Save.instance;
save.options.flashingLights = value;
save.flush();
Save.system.flush();
return value;
}
@@ -130,35 +131,54 @@ class Preferences
{
var save:Save = Save.instance;
save.options.zoomCamera = value;
save.flush();
Save.system.flush();
return value;
}
/**
* If enabled, an FPS and memory counter will be displayed even if this is not a debug build.
* Always disabled on mobile.
* @default `false`
* Always disabled on release mobile builds.
* @default `Off`
*/
public static var debugDisplay(get, set):Bool;
public static var debugDisplay(get, set):DebugDisplayMode;
static function get_debugDisplay():Bool
static function get_debugDisplay():DebugDisplayMode
{
#if mobile
return false;
#if NO_FEATURE_DEBUG_DISPLAY
return DebugDisplayMode.Off;
#end
return Save?.instance?.options?.debugDisplay ?? false;
return Save?.instance?.options?.debugDisplay ?? 'Off';
}
static function set_debugDisplay(value:Bool):Bool
static function set_debugDisplay(value:DebugDisplayMode):DebugDisplayMode
{
if (value != Save.instance.options.debugDisplay)
{
toggleDebugDisplay(value);
}
if (value != Save.instance.options.debugDisplay) setDebugDisplayMode(value);
var save = Save.instance;
save.options.debugDisplay = value;
save.flush();
Save.system.flush();
return value;
}
/**
* Opacity of the debug display's background.
* @default `50`
*/
public static var debugDisplayBGOpacity(get, set):Int;
static function get_debugDisplayBGOpacity():Int
{
return Save?.instance?.options?.debugDisplayBGOpacity ?? 50;
}
static function set_debugDisplayBGOpacity(value:Int):Int
{
setDebugDisplayBGOpacity(value / 100);
var save:Save = Save.instance;
save.options.debugDisplayBGOpacity = value;
Save.system.flush();
return value;
}
@@ -199,7 +219,7 @@ class Preferences
var save:Save = Save.instance;
save.options.hapticsMode = string;
save.flush();
Save.system.flush();
return value;
}
@@ -218,7 +238,7 @@ class Preferences
{
var save:Save = Save.instance;
save.options.hapticsIntensityMultiplier = value;
save.flush();
Save.system.flush();
return value;
}
@@ -232,7 +252,7 @@ class Preferences
static function get_autoPause():Bool
{
#if mobile
return true;
return false;
#end
return Save?.instance?.options?.autoPause ?? true;
}
@@ -243,7 +263,7 @@ class Preferences
var save:Save = Save.instance;
save.options.autoPause = value;
save.flush();
Save.system.flush();
return value;
}
@@ -262,7 +282,7 @@ class Preferences
{
var save:Save = Save.instance;
save.options.autoFullscreen = value;
save.flush();
Save.system.flush();
return value;
}
@@ -282,7 +302,7 @@ class Preferences
{
var save:Save = Save.instance;
save.options.globalOffset = value;
save.flush();
Save.system.flush();
return value;
}
@@ -329,7 +349,7 @@ class Preferences
var save:Save = Save.instance;
save.options.vsyncMode = string;
save.flush();
Save.system.flush();
return value;
}
@@ -351,7 +371,7 @@ class Preferences
var save:Save = Save.instance;
save.options.unlockedFramerate = value;
save.flush();
Save.system.flush();
return value;
}
@@ -391,7 +411,7 @@ class Preferences
{
var save:Save = Save.instance;
save.options.strumlineBackgroundOpacity = value;
save.flush();
Save.system.flush();
return value;
}
@@ -410,7 +430,7 @@ class Preferences
{
var save:Save = Save.instance;
save.options.screenshot.shouldHideMouse = value;
save.flush();
Save.system.flush();
return value;
}
@@ -429,7 +449,7 @@ class Preferences
{
var save:Save = Save.instance;
save.options.screenshot.fancyPreview = value;
save.flush();
Save.system.flush();
return value;
}
@@ -448,7 +468,7 @@ class Preferences
{
var save:Save = Save.instance;
save.options.screenshot.previewOnSave = value;
save.flush();
Save.system.flush();
return value;
}
@@ -459,18 +479,15 @@ class Preferences
{
// Apply the autoPause setting (enables automatic pausing on focus lost).
FlxG.autoPause = Preferences.autoPause;
// WindowUtil.setVSyncMode(Preferences.vsyncMode);
// Apply the debugDisplay setting (enables the FPS and RAM display).
toggleDebugDisplay(Preferences.debugDisplay);
setDebugDisplayMode(Preferences.debugDisplay);
setDebugDisplayBGOpacity(Preferences.debugDisplayBGOpacity / 100);
#if web
toggleFramerateCap(Preferences.unlockedFramerate);
#end
#if desktop
// Apply the autoFullscreen setting (launches the game in fullscreen automatically)
FlxG.fullscreen = Preferences.autoFullscreen;
#end
#if mobile
// Apply the allowScreenTimeout setting.
lime.system.System.allowScreenTimeout = Preferences.screenTimeout;
@@ -485,26 +502,41 @@ class Preferences
#end
}
static function toggleDebugDisplay(show:Bool):Void
public static function setDebugDisplayMode(mode:DebugDisplayMode):Void
{
if (show)
{
// Enable the debug display.
FlxG.game.parent.addChild(Main.fpsCounter);
if (FlxG.game.parent.contains(Main.debugDisplay)) FlxG.game.parent.removeChild(Main.debugDisplay);
#if !html5
FlxG.game.parent.addChild(Main.memoryCounter);
#end
}
else
{
// Disable the debug display.
FlxG.game.parent.removeChild(Main.fpsCounter);
if (mode == DebugDisplayMode.Off) return;
#if !html5
FlxG.game.parent.removeChild(Main.memoryCounter);
#end
}
Main.debugDisplay.isAdvanced = (mode == DebugDisplayMode.Advanced);
FlxG.game.parent.addChild(Main.debugDisplay);
}
static function setDebugDisplayBGOpacity(value:Float):Void
{
if (Main.debugDisplay == null) return;
Main.debugDisplay.backgroundOpacity = value;
}
/**
* If enabled, subtitles will appear during some songs and cutscenes.
* @default `true`
*/
public static var subtitles(get, set):Bool;
static function get_subtitles():Bool
{
return Save?.instance?.options?.subtitles ?? true;
}
static function set_subtitles(value:Bool):Bool
{
var save:Save = Save.instance;
save.options.subtitles = value;
Save.system.flush();
return value;
}
#if mobile
@@ -525,7 +557,7 @@ class Preferences
var save:Save = Save.instance;
save.mobileOptions.screenTimeout = value;
save.flush();
Save.system.flush();
return value;
}
@@ -544,7 +576,7 @@ class Preferences
{
var save:Save = Save.instance;
save.mobileOptions.controlsScheme = value;
save.flush();
Save.system.flush();
return value;
}
@@ -569,7 +601,7 @@ class Preferences
{
var save:Save = Save.instance;
save.mobileOptions.noAds = value;
save.flush();
Save.system.flush();
return value;
}
#end
+20 -24
View File
@@ -1,6 +1,5 @@
package funkin.api.discord;
import funkin.util.macro.EnvironmentConfigMacro;
#if FEATURE_DISCORD_RPC
import hxdiscord_rpc.Discord;
import hxdiscord_rpc.Types.DiscordButton;
@@ -9,10 +8,12 @@ import hxdiscord_rpc.Types.DiscordRichPresence;
import hxdiscord_rpc.Types.DiscordUser;
import sys.thread.Thread;
@:build(funkin.util.macro.EnvironmentMacro.build())
@:nullSafety
class DiscordClient
{
static final CLIENT_ID:Null<String> = EnvironmentConfigMacro.environmentConfig?.get("DESKTOP_DISCORD_CLIENT_ID");
@:envField
static final DISCORD_CLIENT_ID:Null<String>;
public static var instance(get, never):DiscordClient;
static var _instance:Null<DiscordClient> = null;
@@ -28,9 +29,9 @@ class DiscordClient
private function new()
{
trace('[DISCORD] Initializing event handlers...');
trace(' DISCORD '.bold().bg_blue() + ' Initializing event handlers...');
handlers = DiscordEventHandlers.create();
handlers = new DiscordEventHandlers();
handlers.ready = cpp.Function.fromStaticFunction(onReady);
handlers.disconnected = cpp.Function.fromStaticFunction(onDisconnected);
@@ -39,7 +40,7 @@ class DiscordClient
public function init():Void
{
trace('[DISCORD] Initializing connection...');
trace(' DISCORD '.bold().bg_blue() + ' Initializing connection...');
if (!hasValidCredentials())
{
@@ -49,7 +50,7 @@ class DiscordClient
@:nullSafety(Off)
{
Discord.Initialize(CLIENT_ID, cpp.RawPointer.addressOf(handlers), 1, "");
Discord.Initialize(DISCORD_CLIENT_ID, cpp.RawPointer.addressOf(handlers), false, "");
}
createDaemon();
@@ -60,7 +61,7 @@ class DiscordClient
*/
static function hasValidCredentials():Bool
{
return !(CLIENT_ID == null || CLIENT_ID == "" || (CLIENT_ID != null && CLIENT_ID.contains(" ")));
return !(DISCORD_CLIENT_ID == null || DISCORD_CLIENT_ID == "" || (DISCORD_CLIENT_ID != null && DISCORD_CLIENT_ID.contains(" ")));
}
var daemon:Null<Thread> = null;
@@ -78,26 +79,21 @@ class DiscordClient
Discord.updateConnection();
#end
Discord.runCallbacks();
Discord.RunCallbacks();
Sys.sleep(2);
}
}
public function shutdown():Void
{
trace('[DISCORD] Shutting down...');
trace(' DISCORD '.bold().bg_blue() + ' Shutting down...');
Discord.shutdown();
Discord.Shutdown();
}
public function setPresence(params:DiscordClientPresenceParams):Void
{
Discord.updatePresence(buildPresence(params));
}
function buildPresence(params:DiscordClientPresenceParams):DiscordRichPresence
{
var presence = DiscordRichPresence.create();
var presence:DiscordRichPresence = new DiscordRichPresence();
// Presence should always be playing the game.
presence.type = DiscordActivityType_Playing;
@@ -128,23 +124,23 @@ class DiscordClient
// presence.startTimestamp = time - 10;
// presence.endTimestamp = time + 30;
final button1:DiscordButton = DiscordButton.create();
final button1:DiscordButton = new DiscordButton();
button1.label = "Play on Web";
button1.url = Constants.URL_NEWGROUNDS;
presence.buttons[0] = button1;
final button2:DiscordButton = DiscordButton.create();
final button2:DiscordButton = new DiscordButton();
button2.label = "Download";
button2.url = Constants.URL_ITCH;
presence.buttons[1] = button2;
return presence;
Discord.UpdatePresence(cpp.RawConstPointer.addressOf(presence));
}
// TODO: WHAT THE FUCK get this pointer bullfuckery out of here
private static function onReady(request:cpp.RawConstPointer<DiscordUser>):Void
{
trace('[DISCORD] Client has connected!');
trace(' DISCORD '.bold().bg_blue() + ' Client has connected!');
final username:String = request[0].username;
final globalName:String = request[0].username;
@@ -152,22 +148,22 @@ class DiscordClient
if (discriminator != null && discriminator != 0)
{
trace('[DISCORD] User: ${username}#${discriminator} (${globalName})');
trace(' DISCORD '.bold().bg_blue() + ' User: ${username}#${discriminator} (${globalName})');
}
else
{
trace('[DISCORD] User: @${username} (${globalName})');
trace(' DISCORD '.bold().bg_blue() + ' User: @${username} (${globalName})');
}
}
private static function onDisconnected(errorCode:Int, message:cpp.ConstCharStar):Void
{
trace('[DISCORD] Client has disconnected! ($errorCode) "${cast (message, String)}"');
trace(' DISCORD '.bold().bg_blue() + ' Client has disconnected! ($errorCode) "${cast (message, String)}"');
}
private static function onError(errorCode:Int, message:cpp.ConstCharStar):Void
{
trace('[DISCORD] Client has received an error! ($errorCode) "${cast (message, String)}"');
trace(' DISCORD '.bold().bg_blue() + ' Client has received an error! ($errorCode) "${cast (message, String)}"');
}
// public var type(get, set):DiscordActivityType;
+5 -5
View File
@@ -39,21 +39,21 @@ class Events
switch (outcome)
{
case SUCCESS(data):
trace('[NEWGROUNDS] Logged event: ${data.eventName}');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Logged event: ${data.eventName}');
case FAIL(outcome):
switch (outcome)
{
case HTTP(error):
trace('[NEWGROUNDS] HTTP error while logging event: ${error}');
trace(' NEWGROUNDS '.bold().bg_orange() + ' HTTP error while logging event: ${error}');
case RESPONSE(error):
trace('[NEWGROUNDS] Response error (${error.code}) while logging event: ${error.message}');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Response error (${error.code}) while logging event: ${error.message}');
case RESULT(error):
switch (error.code)
{
case 103: // Invalid custom event name
trace('[NEWGROUNDS] Invalid custom event name: ${eventName}');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Invalid custom event name: ${eventName}');
default:
trace('[NEWGROUNDS] Result error (${error.code}) while logging event: ${error.message}');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Result error (${error.code}) while logging event: ${error.message}');
}
}
}
+138 -91
View File
@@ -2,22 +2,29 @@ package funkin.api.newgrounds;
#if FEATURE_NEWGROUNDS
import io.newgrounds.Call.CallError;
import io.newgrounds.components.ScoreBoardComponent;
import io.newgrounds.components.ScoreBoardComponent.Period;
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;
/**
* Handles interactions with the leaderboards on the Newgrounds website.
*/
@:nullSafety
class Leaderboards
{
/**
* Retrieve the leaderboard data via the Newgrounds API.
* @return The leaderboard data.
*/
public static function listLeaderboardData():Map<Leaderboard, LeaderboardData>
{
var leaderboardList:Null<ScoreBoardList> = NewgroundsClient.instance.leaderboards;
if (leaderboardList == null)
{
trace('[NEWGROUNDS] Not logged in, cannot fetch medal data!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Not logged in, cannot fetch medal data!');
return [];
}
@@ -33,24 +40,25 @@ class Leaderboards
public static function submitScore(leaderboard:Leaderboard, score:Int, ?tag:String):Void
{
// Silently reject submissions for unknown leaderboards.
if (leaderboard == Leaderboard.Unknown) return;
if (leaderboard == Leaderboard.Unknown)
{
trace(' NEWGROUNDS '.bold().bg_orange() + ' Unknown leaderboard, skipping score submission...');
return;
}
if (NewgroundsClient.instance.isLoggedIn())
{
var leaderboardList = NewgroundsClient.instance.leaderboards;
if (leaderboardList == null) return;
var leaderboardData:Null<LeaderboardData> = leaderboardList.get(leaderboard.getId());
var leaderboardData:Null<LeaderboardData> = listLeaderboardData().get(leaderboard.getId());
if (leaderboardData != null)
{
leaderboardData.postScore(score, function(outcome:Outcome<CallError>):Void {
switch (outcome)
{
case SUCCESS:
trace('[NEWGROUNDS] Submitted score!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Submitted leaderboard score!');
case FAIL(error):
trace('[NEWGROUNDS] Failed to submit score!');
trace(error);
trace(' NEWGROUNDS '.bold().bg_orange() + ' ERROR '.error() + ' Failed to submit leaderboard score!');
trace(' ERROR '.error() + error);
}
});
}
@@ -67,10 +75,7 @@ class Leaderboards
// 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());
var leaderboardData:Null<LeaderboardData> = listLeaderboardData().get(leaderboard.getId());
if (leaderboardData == null) return;
var user:Null<User> = null;
@@ -81,12 +86,12 @@ class Leaderboards
switch (outcome)
{
case SUCCESS:
trace('[NEWGROUNDS] Fetched scores!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Fetched scores!');
if (params != null && params.onComplete != null) params.onComplete(leaderboardData.scores);
case FAIL(error):
trace('[NEWGROUNDS] Failed to fetch scores!');
trace(error);
trace(' NEWGROUNDS '.bold().bg_orange() + ' ERROR '.error() + ' Failed to fetch scores!');
trace(' ERROR '.error() + error);
if (params != null && params.onFail != null) params.onFail();
}
});
@@ -94,19 +99,29 @@ class Leaderboards
/**
* Submit a score for a Story Level to Newgrounds.
*
* @param levelId The ID for the story level.
* @param difficultyId The current difficulty.
* @param score The score to submit.
*/
public static function submitLevelScore(levelId:String, difficultyId:String, score:Int):Void
{
var tag = '${difficultyId}';
trace(' NEWGROUNDS '.bold().bg_orange() + 'Submitting score for level "${levelId}"...');
Leaderboards.submitScore(Leaderboard.getLeaderboardByLevel(levelId), score, tag);
}
/**
* Submit a score for a song to Newgrounds.
*
* @param songId The ID for the song.
* @param difficultyId The current difficulty.
* @param score The score to submit.
*/
public static function submitSongScore(songId:String, difficultyId:String, score:Int):Void
{
var tag = '${difficultyId}';
trace(' NEWGROUNDS '.bold().bg_orange() + 'Submitting score for song "${songId}" (${difficultyId})...');
Leaderboards.submitScore(Leaderboard.getLeaderboardBySong(songId, difficultyId), score, tag);
}
}
@@ -117,16 +132,33 @@ class Leaderboards
@:nullSafety
class LeaderboardsSandboxed
{
public static function getLeaderboardBySong(songId:String, difficultyId:String)
/**
* Get the leaderboard for a given song and difficulty.
*
* @param songId The ID for the song.
* @param difficultyId The current difficulty, suffixed with the variation, like `easy-pico` or `nightmare`.
* @return The Leaderboard ID for the given level and difficulty.
*/
public static function getLeaderboardBySong(songId:String, difficultyId:String):Leaderboard
{
return Leaderboard.getLeaderboardBySong(songId, difficultyId);
}
public static function getLeaderboardByLevel(levelId:String)
/**
* Get the leaderboard for a given story level and difficulty.
* @param levelId The ID for the story level.
* @return The Leaderboard ID for the given level and difficulty.
*/
public static function getLeaderboardByLevel(levelId:String):Leaderboard
{
return Leaderboard.getLeaderboardByLevel(levelId);
}
/**
* Request to receive scores from Newgrounds.
* @param leaderboard The leaderboard to fetch scores from.
* @param params Additional parameters for fetching the score.
*/
public function requestScores(leaderboard:Leaderboard, params:RequestScoresParams)
{
Leaderboards.requestScores(leaderboard, params);
@@ -180,106 +212,118 @@ typedef RequestScoresParams =
}
#end
/**
* An enumeration of Newgrounds leaderboards for the game's levels and songs.
*/
enum abstract Leaderboard(Int) from Int to Int
{
/**
* Represents an undefined or invalid leaderboard.
*/
var Unknown = -1;
public var Unknown = -1;
//
// STORY LEVELS
//
var StoryWeek1 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14239 #else 9615 #end;
var StoryWeek2 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14240 #else 9616 #end;
var StoryWeek3 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14242 #else 9767 #end;
var StoryWeek4 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14241 #else 9866 #end;
var StoryWeek5 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14243 #else 9956 #end;
var StoryWeek6 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14244 #else 9957 #end;
var StoryWeek7 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14245 #else 14682 #end;
var StoryWeekend1 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14237 #else 14683 #end;
// Tutorial only has one song.
public var StoryWeek1 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14239 #else 9615 #end;
public var StoryWeek2 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14240 #else 9616 #end;
public var StoryWeek3 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14242 #else 9767 #end;
public var StoryWeek4 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14241 #else 9866 #end;
public var StoryWeek5 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14243 #else 9956 #end;
public var StoryWeek6 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14244 #else 9957 #end;
public var StoryWeek7 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14245 #else 14682 #end;
public var StoryWeekend1 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14237 #else 14683 #end;
// Collab 1 only has one song.
//
// SONGS
//
// Tutorial
var Tutorial = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14249 #else 14684 #end;
public var Tutorial = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14249 #else 14684 #end;
// Week 1
var Bopeebo = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14246 #else 9603 #end;
var BopeeboErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14685 #end;
var BopeeboPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14686 #end;
var Fresh = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14247 #else 9602 #end;
var FreshErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14687 #end;
var FreshPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14688 #end;
var DadBattle = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14248 #else 9605 #end;
var DadBattleErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14689 #end;
var DadBattlePicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14690 #end;
public var Bopeebo = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14246 #else 9603 #end;
public var BopeeboErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14685 #end;
public var BopeeboPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14686 #end;
public var Fresh = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14247 #else 9602 #end;
public var FreshErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14687 #end;
public var FreshPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14688 #end;
public var DadBattle = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14248 #else 9605 #end;
public var DadBattleErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14689 #end;
public var DadBattlePicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14690 #end;
// Week 2
var Spookeez = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9604 #end;
var SpookeezErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14691 #end;
var SpookeezPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14692 #end;
var South = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9606 #end;
var SouthErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14693 #end;
var SouthPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14694 #end;
var Monster = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14703 #end;
public var Spookeez = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9604 #end;
public var SpookeezErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14691 #end;
public var SpookeezPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14692 #end;
public var South = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9606 #end;
public var SouthErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14693 #end;
public var SouthPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14694 #end;
public var Monster = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14703 #end;
// Week 3
var Pico = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9766 #end;
var PicoErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14695 #end;
var PicoPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14696 #end;
var PhillyNice = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9769 #end;
var PhillyNiceErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14697 #end;
var PhillyNicePicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14698 #end;
var Blammed = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9768 #end;
var BlammedErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14704 #end;
var BlammedPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14705 #end;
public var Pico = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9766 #end;
public var PicoErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14695 #end;
public var PicoPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14696 #end;
public var PhillyNice = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9769 #end;
public var PhillyNiceErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14697 #end;
public var PhillyNicePicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14698 #end;
public var Blammed = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9768 #end;
public var BlammedErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14704 #end;
public var BlammedPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14705 #end;
// Week 4
var SatinPanties = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9869 #end;
var SatinPantiesErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14701 #end;
var High = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9867 #end;
var HighErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14699 #end;
var MILF = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9868 #end;
public var SatinPanties = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9869 #end;
public var SatinPantiesErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14701 #end;
public var High = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9867 #end;
public var HighErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14699 #end;
public var MILF = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9868 #end;
// Week 5
var Cocoa = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14706 #end;
var CocoaErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14707 #end;
var CocoaPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14708 #end;
var Eggnog = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14709 #end;
var EggnogErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14711 #end;
var EggnogPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14710 #end;
var WinterHorrorland = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14712 #end;
public var Cocoa = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14706 #end;
public var CocoaErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14707 #end;
public var CocoaPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14708 #end;
public var Eggnog = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14709 #end;
public var EggnogErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14711 #end;
public var EggnogPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14710 #end;
public var WinterHorrorland = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14712 #end;
// Week 6
var Senpai = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9958 #end;
var SenpaiErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14713 #end;
var SenpaiPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14716 #end;
var Roses = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9959 #end;
var RosesErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14714 #end;
var RosesPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14717 #end;
var Thorns = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9960 #end;
var ThornsErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14715 #end;
public var Senpai = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9958 #end;
public var SenpaiErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14713 #end;
public var SenpaiPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14716 #end;
public var Roses = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9959 #end;
public var RosesErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14714 #end;
public var RosesPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14717 #end;
public var Thorns = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9960 #end;
public var ThornsErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14715 #end;
// Week 7
var Ugh = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14718 #end;
var UghErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14722 #end;
var UghPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14721 #end;
var Guns = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14719 #end;
var GunsPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14723 #end;
var Stress = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14720 #end;
var StressPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14724 #end;
public var Ugh = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14718 #end;
public var UghErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14722 #end;
public var UghPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14721 #end;
public var Guns = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14719 #end;
public var GunsPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14723 #end;
public var Stress = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14720 #end;
public var StressPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14724 #end;
// Weekend 1
var Darnell = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14725 #end;
var DarnellErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14727 #end;
var DarnellBFMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14726 #end;
var LitUp = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14728 #end;
var LitUpBFMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14729 #end;
var TwoHot = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14730 #end; // Variable names can't start with a number!
var Blazin = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14731 #end;
public var Darnell = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14725 #end;
public var DarnellErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14727 #end;
public var DarnellBFMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14726 #end;
public var LitUp = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14728 #end;
public var LitUpBFMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14729 #end;
public var TwoHot = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14730 #end; // Variable names can't start with a number!
public var Blazin = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14731 #end;
// Collab 1
public var Spaghetti = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 15448 #else 15451 #end;
/**
* Get the numeric ID for the leaderboard on the Newgrounds site.
* @return The numeric ID for the leaderboard.
*/
public function getId():Int
{
return this;
@@ -288,7 +332,6 @@ enum abstract Leaderboard(Int) from Int to Int
/**
* Get the leaderboard for a given level and difficulty.
* @param levelId The ID for the story level.
* @param difficulty The current difficulty.
* @return The Leaderboard ID for the given level and difficulty.
*/
public static function getLeaderboardByLevel(levelId:String):Leaderboard
@@ -311,14 +354,16 @@ enum abstract Leaderboard(Int) from Int to Int
return StoryWeek7;
case "weekend1":
return StoryWeekend1;
// Collab 1 has only one song.
default:
return Unknown;
}
}
/**
* Get the leaderboard for a given level and difficulty.
* @param levelId The ID for the story level.
* Get the leaderboard for a given song and difficulty.
*
* @param songId The ID for the song.
* @param difficulty The current difficulty, suffixed with the variation, like `easy-pico` or `nightmare`.
* @return The Leaderboard ID for the given level and difficulty.
*/
@@ -479,6 +524,8 @@ enum abstract Leaderboard(Int) from Int to Int
return TwoHot;
case "blazin":
return Blazin;
case "spaghetti":
return Spaghetti;
default:
return Unknown;
}
+95 -44
View File
@@ -5,45 +5,54 @@ import io.newgrounds.objects.Medal as MedalData;
import funkin.util.plugins.NewgroundsMedalPlugin;
import flixel.graphics.FlxGraphic;
import openfl.display.BitmapData;
import io.newgrounds.utils.MedalList;
import haxe.Json;
/**
* Handles interactions with the medals on the Newgrounds website.
*/
@:nullSafety
class Medals
{
/**
* A cache for the medal data fetched from the local `medals.json`.
*/
public static var medalJSON:Array<MedalJSON> = [];
/**
* Retrieve the leaderboard data via the Newgrounds API.
* @return The leaderboard data.
*/
public static function listMedalData():Map<Medal, MedalData>
{
var medalList = NewgroundsClient.instance.medals;
if (medalList == null)
{
trace('[NEWGROUNDS] Not logged in, cannot fetch medal data!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Not logged in, cannot fetch medal data!');
return [];
}
return @:privateAccess medalList._map?.copy() ?? [];
}
/**
* Awards the player a specific medal, updating their Newgrounds account and
* displaying the corresponding popup.
* @param medal The medal to be awarded.
*/
public static function award(medal:Medal):Void
{
if (NewgroundsClient.instance.isLoggedIn())
{
var medalList = NewgroundsClient.instance.medals;
@:privateAccess
if (medalList == null || medalList._map == null) return;
var medalData:Null<MedalData> = medalList.get(medal.getId());
var medalData:Null<MedalData> = listMedalData().get(medal.getId());
@:privateAccess
if (medalData == null || medalData._data == null)
{
trace('[NEWGROUNDS] Could not retrieve data for medal: ${medal}');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Could not retrieve data for medal: ${medal}');
return;
}
else if (!medalData.unlocked)
{
trace('[NEWGROUNDS] Awarding medal (${medal}).');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Awarding medal (${medal}).');
medalData.sendUnlock();
// Play the medal unlock animation, but only if the user has not already unlocked it.
@@ -85,15 +94,18 @@ class Medals
}
else
{
trace('[NEWGROUNDS] User already has medal (${medal}).');
trace(' NEWGROUNDS '.bold().bg_orange() + ' User already has medal (${medal}).');
}
}
else
{
trace('[NEWGROUNDS] Attempted to award medal (${medal}), but not logged into Newgrounds.');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Attempted to award medal (${medal}), but not logged into Newgrounds.');
}
}
/**
* Loads the full medal data from local JSON data.
*/
public static function loadMedalJSON():Void
{
var jsonPath = Paths.json('medals');
@@ -102,12 +114,12 @@ class Medals
var parser = new json2object.JsonParser<Array<MedalJSON>>();
parser.ignoreUnknownVariables = false;
trace('[NEWGROUNDS] Parsing local medal data...');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Parsing local medal data...');
parser.fromJson(jsonString, jsonPath);
if (parser.errors.length > 0)
{
trace('[NEWGROUNDS] Failed to parse local medal data!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Failed to parse local medal data!');
for (error in parser.errors)
funkin.data.DataError.printError(error);
medalJSON = [];
@@ -118,17 +130,18 @@ class Medals
}
}
/**
* Fetches the data for a medal from the Newgrounds API.
* @param medal The ID of the medal to fetch.
* @return The fetched medal data.
*/
public static function fetchMedalData(medal:Medal):Null<FetchedMedalData>
{
var medalList = NewgroundsClient.instance.medals;
@:privateAccess
if (medalList == null || medalList._map == null) return null;
var medalData:Null<MedalData> = medalList.get(medal.getId());
var medalData:Null<MedalData> = listMedalData().get(medal.getId());
@:privateAccess
if (medalData == null || medalData._data == null)
{
trace('[NEWGROUNDS] Could not retrieve data for medal: ${medal}');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Could not retrieve data for medal: ${medal}');
return null;
}
@@ -144,12 +157,16 @@ class Medals
}
}
/**
* Awards the player a medal for completing a specific story level.
* @param id The level ID for the story level that was completed.
*/
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}).');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Story level does not have a medal! (${id}).');
return;
}
Medals.award(medal);
@@ -161,16 +178,30 @@ class Medals
*/
class MedalsSandboxed
{
/**
* Fetches the data for a medal from the Newgrounds API.
* @param medal The ID of the medal to fetch.
* @return The fetched medal data.
*/
public static function fetchMedalData(medal:Medal):Null<FetchedMedalData>
{
return Medals.fetchMedalData(medal);
}
/**
* Retrieves the medal for completing a specific story level.
* @param id The ID for the story level that was completed.
* @return The medal to award.
*/
public static function getMedalByStoryLevel(id:String):Medal
{
return Medal.getMedalByStoryLevel(id);
}
/**
* Lists all medals aside from the `Unknown` one.
* @return An array of all medals.
*/
public static function getAllMedals():Array<Medal>
{
return Medal.getAllMedals();
@@ -219,140 +250,157 @@ typedef MedalJSON =
var icon:String;
}
/**
* An enumeration of all the game's medals.
*/
enum abstract Medal(Int) from Int to Int
{
/**
* Represents an undefined or invalid medal.
*/
var Unknown = -1;
public var Unknown = -1;
/**
* I Said Funkin'!
* Start the game for the first time.
*/
var StartGame = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80894 #else 60960 #end;
public var StartGame = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80894 #else 60960 #end;
/**
* That's How You Do It!
* Beat Tutoria l in Story Mode (on any difficulty).
* Beat Tutorial in Story Mode (on any difficulty).
*/
var StoryTutorial = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80906 #else 83647 #end;
public var StoryTutorial = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80906 #else 83647 #end;
/**
* More Like Daddy Queerest
* Beat Week 1 in Story Mode (on any difficulty).
*/
var StoryWeek1 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80899 #else 60961 #end;
public var StoryWeek1 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80899 #else 60961 #end;
/**
* IT IS THE SPOOKY MONTH
* Beat Week 2 in Story Mode (on any difficulty).
*/
var StoryWeek2 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80900 #else 83648 #end;
public var StoryWeek2 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80900 #else 83648 #end;
/**
* Pico Funny
* Beat Week 3 in Story Mode (on any difficulty).
*/
var StoryWeek3 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80901 #else 83649 #end;
public var StoryWeek3 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80901 #else 83649 #end;
/**
* Mommy Must Murder
* Beat Week 4 in Story Mode (on any difficulty).
*/
var StoryWeek4 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80902 #else 83650 #end;
public var StoryWeek4 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80902 #else 83650 #end;
/**
* Yule Tide Joy
* Beat Week 5 in Story Mode (on any difficulty).
*/
var StoryWeek5 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80903 #else 83651 #end;
public var StoryWeek5 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80903 #else 83651 #end;
/**
* A Visual Novelty
* Beat Week 6 in Story Mode (on any difficulty).
*/
var StoryWeek6 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80904 #else 83652 #end;
public var StoryWeek6 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80904 #else 83652 #end;
/**
* I <3 JohnnyUtah
* Beat Week 7 in Story Mode (on any difficulty).
*/
var StoryWeek7 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80905 #else 83653 #end;
public var StoryWeek7 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80905 #else 83653 #end;
/**
* Yo, Really Think So?
* Beat Weekend 1 in Story Mode (on any difficulty).
*/
var StoryWeekend1 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80907 #else 83654 #end;
public var StoryWeekend1 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80907 #else 83654 #end;
/**
* Eat It Up!
* Beat Collab 1 in Story Mode (on any difficulty).
*/
public var StoryCollab1 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 87697 #else 87714 #end;
/**
* Stay Funky
* Press TAB in Freeplay and unlock your first character.
*/
var CharSelect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 83633 #else 83655 #end;
public var CharSelect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 83633 #else 83655 #end;
/**
* A Challenger Appears
* Beat any Pico remix in Freeplay (on any difficulty).
*/
var FreeplayPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80910 #else 83656 #end;
public var FreeplayPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80910 #else 83656 #end;
/**
* De-Stressing
* Beat Stress (Pico Mix) in Freeplay (on Normal difficulty or higher).
*/
var FreeplayStressPico = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 83629 #else 83657 #end;
public var FreeplayStressPico = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 83629 #else 83657 #end;
/**
* L
* Earn a Loss rating on any song (on any difficulty).
*/
var LossRating = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80915 #else 83658 #end;
public var LossRating = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80915 #else 83658 #end;
/**
* Getting Freaky
* Earn a Perfect rating on any song on Hard difficulty or higher.
* NOTE: Should also be awarded for a Gold Perfect because otherwise that would be annoying.
*/
var PerfectRatingHard = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80908 #else 83659 #end;
public var PerfectRatingHard = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80908 #else 83659 #end;
/**
* You Should Drink More Water
* Earn a Golden Perfect rating on any song on Hard difficulty or higher.
*/
var GoldPerfectRatingHard = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80909 #else 83660 #end;
public var GoldPerfectRatingHard = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80909 #else 83660 #end;
/**
* Harder Than Hard
* Beat any Erect remix in Freeplay on Erect or Nightmare difficulty.
*/
var ErectDifficulty = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80911 #else 83661 #end;
public var ErectDifficulty = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80911 #else 83661 #end;
/**
* The Rap God
* Earn a Gold Perfect rating on any song on Nightmare difficulty.
*/
var GoldPerfectRatingNightmare = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80912 #else 83662 #end;
public var GoldPerfectRatingNightmare = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80912 #else 83662 #end;
/**
* Just like the game!
* Get freaky on a Friday.
* NOTE: You must beat at least one song on any difficulty.
*/
var FridayNight = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80913 #else 61034 #end;
public var FridayNight = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80913 #else 61034 #end;
/**
* Nice
* Earn a rating of EXACTLY 69% (good luck).
*/
var Nice = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80914 #else 83646 #end;
public var Nice = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80914 #else 83646 #end;
/**
* @return The internal ID for the medal.
*/
public function getId():Int
{
return this;
}
/**
* Retrieves the medal for completing a specific story level.
* @param levelId The ID for the story level that was completed.
* @return The medal to award.
*/
public static function getMedalByStoryLevel(levelId:String):Medal
{
switch (levelId)
@@ -375,6 +423,8 @@ enum abstract Medal(Int) from Int to Int
return StoryWeek7;
case "weekend1":
return StoryWeekend1;
case "sserafim":
return StoryCollab1;
default:
return Unknown;
}
@@ -382,8 +432,9 @@ enum abstract Medal(Int) from Int to Int
/**
* Lists all medals aside from the `Unknown` one.
* @return An array of all medals.
*/
public static function getAllMedals()
public static function getAllMedals():Array<Medal>
{
return [
StartGame,
+14 -19
View File
@@ -25,7 +25,7 @@ class NGSaveSlot
public static function loadInstance():NGSaveSlot
{
var loadedSave:NGSaveSlot = loadSlot(Save.BASE_SAVE_SLOT);
var loadedSave:NGSaveSlot = loadSlot(Constants.BASE_SAVE_SLOT);
if (_instance == null) _instance = loadedSave;
return loadedSave;
@@ -33,7 +33,7 @@ class NGSaveSlot
static function loadSlot(slot:Int):NGSaveSlot
{
trace('[NEWGROUNDS] Getting save slot from ID $slot');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Getting save slot from ID $slot');
var saveSlot:Null<SaveSlot> = NewgroundsClient.instance.saveSlots?.getById(slot);
@@ -46,11 +46,6 @@ class NGSaveSlot
public function new(?ngSaveSlot:Null<SaveSlot>)
{
this.ngSaveSlot = ngSaveSlot;
#if FLX_DEBUG
FlxG.console.registerClass(NGSaveSlot);
FlxG.console.registerClass(Save);
#end
}
/**
@@ -67,16 +62,16 @@ class NGSaveSlot
switch (outcome)
{
case SUCCESS:
trace('[NEWGROUNDS] Successfully saved save data to save slot!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Successfully saved save data to save slot!');
case FAIL(error):
trace('[NEWGROUNDS] Failed to save data to save slot!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Failed to save data to save slot!');
trace(error);
}
});
}
catch (error:String)
{
trace('[NEWGROUNDS] Failed to save data to save slot!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Failed to save data to save slot!');
trace(error);
}
}
@@ -89,7 +84,7 @@ class NGSaveSlot
switch (outcome)
{
case SUCCESS(value):
trace('[NEWGROUNDS] Loaded save slot with the ID of ${ngSaveSlot?.id}!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Loaded save slot with the ID of ${ngSaveSlot?.id}!');
#if FEATURE_DEBUG_FUNCTIONS
trace('Save Slot Data:');
trace(value);
@@ -101,7 +96,7 @@ class NGSaveSlot
onComplete(decodedData);
}
case FAIL(error):
trace('[NEWGROUNDS] Failed to load save slot with the ID of ${ngSaveSlot?.id}!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Failed to load save slot with the ID of ${ngSaveSlot?.id}!');
trace(error);
if (onError != null)
@@ -113,7 +108,7 @@ class NGSaveSlot
}
catch (error:String)
{
trace('[NEWGROUNDS] Failed to load save slot with the ID of ${ngSaveSlot?.id}!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Failed to load save slot with the ID of ${ngSaveSlot?.id}!');
trace(error);
if (onError != null)
@@ -131,26 +126,26 @@ class NGSaveSlot
switch (outcome)
{
case SUCCESS:
trace('[NEWGROUNDS] Successfully cleared save slot!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Successfully cleared save slot!');
case FAIL(error):
trace('[NEWGROUNDS] Failed to clear save slot!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Failed to clear save slot!');
trace(error);
}
});
}
catch (error:String)
{
trace('[NEWGROUNDS] Failed to clear save slot!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Failed to clear save slot!');
trace(error);
}
}
public function checkSlot():Void
{
trace('[NEWGROUNDS] Checking save slot with the ID of ${ngSaveSlot?.id}...');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Checking save slot with the ID of ${ngSaveSlot?.id}...');
trace(' Is null? ${ngSaveSlot == null}');
trace(' Is empty? ${ngSaveSlot?.isEmpty() ?? false}');
trace(' Is null? ${ngSaveSlot == null}');
trace(' Is empty? ${ngSaveSlot?.isEmpty() ?? false}');
}
}
#end
@@ -1,6 +1,5 @@
package funkin.api.newgrounds;
import funkin.util.macro.EnvironmentConfigMacro;
import funkin.save.Save;
import funkin.api.newgrounds.Medals.Medal;
#if FEATURE_NEWGROUNDS
@@ -14,12 +13,19 @@ import io.newgrounds.utils.MedalList;
import io.newgrounds.utils.SaveSlotList;
import io.newgrounds.utils.ScoreBoardList;
import io.newgrounds.objects.User;
#if FEATURE_MOBILE_WEBVIEW
import funkin.mobile.util.WebViewUtil;
#end
@:build(funkin.util.macro.EnvironmentMacro.build())
@:nullSafety
class NewgroundsClient
{
static final APP_ID:Null<String> = EnvironmentConfigMacro.environmentConfig?.get("API_NG_APP_ID");
static final ENCRYPTION_KEY:Null<String> = EnvironmentConfigMacro.environmentConfig?.get("API_NG_ENC_KEY");
@:envField
static final API_NG_APP_ID:Null<String>;
@:envField
static final API_NG_ENC_KEY:Null<String>;
public static var instance(get, never):NewgroundsClient;
@@ -39,11 +45,11 @@ class NewgroundsClient
private function new()
{
trace('[NEWGROUNDS] Initializing client...');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Initializing client...');
#if FEATURE_NEWGROUNDS_DEBUG
trace('[NEWGROUNDS] App ID: ${APP_ID}');
trace('[NEWGROUNDS] Encryption Key: ${ENCRYPTION_KEY}');
trace(' NEWGROUNDS '.bold().bg_orange() + ' App ID: ${API_NG_APP_ID}');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Encryption Key: ${API_NG_ENC_KEY}');
#end
if (!hasValidCredentials())
@@ -54,9 +60,9 @@ class NewgroundsClient
@:nullSafety(Off)
{
NG.create(APP_ID, getSessionId(), #if FEATURE_NEWGROUNDS_DEBUG true #else false #end, onLoginResolved);
NG.create(API_NG_APP_ID, getSessionId(), #if FEATURE_NEWGROUNDS_DEBUG true #else false #end, onLoginResolved);
NG.core.setupEncryption(ENCRYPTION_KEY);
NG.core.setupEncryption(API_NG_ENC_KEY);
}
}
@@ -64,7 +70,7 @@ class NewgroundsClient
{
if (NG.core == null) return;
trace('[NEWGROUNDS] Setting up connection...');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Setting up connection...');
#if FEATURE_NEWGROUNDS_DEBUG
NG.core.verbose = true;
@@ -75,16 +81,16 @@ class NewgroundsClient
if (NG.core.attemptingLogin)
{
// Session ID was valid and we should be logged in soon.
trace('[NEWGROUNDS] Waiting for existing login!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Waiting for existing login!');
}
else
{
#if FEATURE_NEWGROUNDS_AUTOLOGIN
// Attempt an automatic login.
trace('[NEWGROUNDS] Attempting new login immediately!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Attempting new login immediately!');
this.autoLogin();
#else
trace('[NEWGROUNDS] Not logged in, you have to login manually!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Not logged in, you have to login manually!');
#end
}
}
@@ -101,14 +107,39 @@ class NewgroundsClient
FlxG.log.warn("No Newgrounds client initialized! Are your credentials invalid?");
return;
}
if (NG.core.attemptingLogin)
{
trace(" NEWGROUNDS '.bold().bg_orange() + ' Login attempt ongoing, will not login until finished.");
return;
}
var passportHandler:String->Void = function(passportUrl:String) {
// This exists so we can create a popup on mobile but with a WebView instead.
#if FEATURE_MOBILE_WEBVIEW
if (passportUrl != null)
{
NG.core.logVerbose('Loading passport from WebView: ${passportUrl}');
WebViewUtil.openURL(passportUrl, function():Void {
NG.core.cancelLoginRequest();
});
NG.core.onPassportUrlOpen();
}
else
NG.core.logError("Cannot open passport");
#else
NG.core.openPassportUrl();
#end
};
if (onSuccess != null && onError != null)
{
NG.core.requestLogin(onLoginResolvedWithCallbacks.bind(_, onSuccess, onError));
NG.core.requestLogin(onLoginResolvedWithCallbacks.bind(_, onSuccess, onError), passportHandler);
}
else
{
NG.core.requestLogin(onLoginResolved);
NG.core.requestLogin(onLoginResolved, passportHandler);
}
}
@@ -156,7 +187,7 @@ class NewgroundsClient
}
}
Save.instance.ngSessionId = null;
Save.instance.ngSessionId.value = null;
}
/**
@@ -176,16 +207,20 @@ class NewgroundsClient
*/
static function hasValidCredentials():Bool
{
return !(APP_ID == null
|| APP_ID == ""
|| (APP_ID != null && APP_ID.contains(" "))
|| ENCRYPTION_KEY == null
|| ENCRYPTION_KEY == ""
|| (ENCRYPTION_KEY != null && ENCRYPTION_KEY.contains(" ")));
return !(API_NG_APP_ID == null
|| API_NG_APP_ID == ""
|| (API_NG_APP_ID != null && API_NG_APP_ID.contains(" "))
|| API_NG_ENC_KEY == null
|| API_NG_ENC_KEY == ""
|| (API_NG_ENC_KEY != null && API_NG_ENC_KEY.contains(" ")));
}
function onLoginResolved(outcome:LoginOutcome):Void
{
#if FEATURE_MOBILE_WEBVIEW
WebViewUtil.close();
#end
switch (outcome)
{
case SUCCESS:
@@ -236,17 +271,19 @@ class NewgroundsClient
{
if (NG.core == null) return;
trace('[NEWGROUNDS] Login successful!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Login successful!');
// Persist the session ID.
Save.instance.ngSessionId = NG.core.sessionId;
Save.instance.ngSessionId.value = NG.core.sessionId;
trace('[NEWGROUNDS] Submitting medal request...');
trace(NG.core.sessionId);
trace(' NEWGROUNDS '.bold().bg_orange() + ' Submitting medal request...');
NG.core.requestMedals(onFetchedMedals);
trace('[NEWGROUNDS] Submitting leaderboard request...');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Submitting leaderboard request...');
NG.core.scoreBoards.loadList(onFetchedLeaderboards);
trace('[NEWGROUNDS] Submitting save slot request...');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Submitting save slot request...');
NG.core.saveSlots.loadList(onFetchedSaveSlots);
}
@@ -258,32 +295,32 @@ class NewgroundsClient
switch (type)
{
case PASSPORT:
trace('[NEWGROUNDS] Login cancelled by passport website.');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Login cancelled by passport website.');
case MANUAL:
trace('[NEWGROUNDS] Login cancelled by application.');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Login cancelled by application.');
default:
trace('[NEWGROUNDS] Login cancelled by unknown source.');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Login cancelled by unknown source.');
}
case ERROR(error):
switch (error)
{
case HTTP(error):
trace('[NEWGROUNDS] Login failed due to HTTP error: ${error}');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Login failed due to HTTP error: ${error}');
case RESPONSE(error):
trace('[NEWGROUNDS] Login failed due to response error: ${error.message} (${error.code})');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Login failed due to response error: ${error.message} (${error.code})');
case RESULT(error):
trace('[NEWGROUNDS] Login failed due to result error: ${error.message} (${error.code})');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Login failed due to result error: ${error.message} (${error.code})');
default:
trace('[NEWGROUNDS] Login failed due to unknown error: ${error}');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Login failed due to unknown error: ${error}');
}
default:
trace('[NEWGROUNDS] Login failed due to unknown reason.');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Login failed due to unknown reason.');
}
}
function onLogoutSuccessful():Void
{
trace('[NEWGROUNDS] Logout successful!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Logout successful!');
}
function onLogoutFailed(result:CallError):Void
@@ -291,31 +328,31 @@ class NewgroundsClient
switch (result)
{
case HTTP(error):
trace('[NEWGROUNDS] Logout failed due to HTTP error: ${error}');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Logout failed due to HTTP error: ${error}');
case RESPONSE(error):
trace('[NEWGROUNDS] Logout failed due to response error: ${error.message} (${error.code})');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Logout failed due to response error: ${error.message} (${error.code})');
case RESULT(error):
trace('[NEWGROUNDS] Logout failed due to result error: ${error.message} (${error.code})');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Logout failed due to result error: ${error.message} (${error.code})');
default:
trace('[NEWGROUNDS] Logout failed due to unknown error: ${result}');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Logout failed due to unknown error: ${result}');
}
}
function onFetchedMedals(outcome:Outcome<CallError>):Void
{
trace('[NEWGROUNDS] Fetched medals!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Fetched medals!');
}
function onFetchedLeaderboards(outcome:Outcome<CallError>):Void
{
trace('[NEWGROUNDS] Fetched leaderboards!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Fetched leaderboards!');
// trace(funkin.api.newgrounds.Leaderboards.listLeaderboardData());
}
function onFetchedSaveSlots(outcome:Outcome<CallError>):Void
{
trace('[NEWGROUNDS] Fetched save slots!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Fetched save slots!');
NGSaveSlot.instance.checkSlot();
}
@@ -353,7 +390,7 @@ class NewgroundsClient
#end
// We have to fetch the session ID from the save file.
return Save.instance.ngSessionId;
return Save.instance.ngSessionId.value;
}
}
+13 -40
View File
@@ -124,11 +124,6 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
*/
var _label:String = "unknown";
/**
* Whether we received a focus lost event.
*/
var _lostFocus:Bool = false;
public function new()
{
super();
@@ -229,37 +224,6 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
return this;
}
/**
* Called when the user clicks to focus on the window.
*/
override function onFocus():Void
{
// Flixel can sometimes toss spurious `onFocus` events, e.g. if the Flixel debugger is toggled
// on and off. We only want to resume the sound if we actually lost focus, and if we weren't
// already paused before we lost focus.
if (_lostFocus && !_alreadyPaused)
{
// trace('Resuming audio (${this._label}) on focus!');
resume();
}
else
{
// trace('Not resuming audio (${this._label}) on focus!');
}
_lostFocus = false;
}
/**
* Called when the user tabs away from the window.
*/
override function onFocusLost():Void
{
// trace('Focus lost, pausing audio!');
_lostFocus = true;
_alreadyPaused = _paused;
pause();
}
public override function resume():FunkinSound
{
if (this._time < 0)
@@ -405,10 +369,7 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
var music = FunkinSound.load(pathToUse, params?.startingVolume ?? 1.0, params.loop ?? true, false, true, params.persist ?? false, params.onComplete);
if (music != null)
{
FlxG.sound.music = music;
// Prevent repeat update() and onFocus() calls.
FlxG.sound.list.remove(FlxG.sound.music);
setMusic(music);
if (FlxG.sound.music != null && params.onLoad != null) params.onLoad();
@@ -421,6 +382,18 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
}
}
/**
* Replaces the Flixel current music object with the given `FunkinSound` object.
* @param newMusic The new music to be set as the current music.
*/
public static function setMusic(newMusic:FunkinSound):Void
{
FlxG.sound.music = newMusic;
// Prevent repeat update() and onFocus() calls.
FlxG.sound.list.remove(FlxG.sound.music);
}
public static function emptyPartialQueue():Void
{
while (partialQueue.length > 0)
+4 -3
View File
@@ -14,6 +14,7 @@ class ABotVis extends FlxTypedSpriteGroup<FlxSprite>
{
// public var vis:VisShit;
var analyzer:Null<SpectralAnalyzer> = null;
var analyzerLevelsCache:Array<Bar> = new Array<Bar>();
var volumes:Array<Float> = [];
@@ -112,11 +113,11 @@ class ABotVis extends FlxTypedSpriteGroup<FlxSprite>
*/
function drawFFT():Void
{
var levels = (analyzer != null) ? analyzer.getLevels() : getDefaultLevels();
analyzerLevelsCache = (analyzer != null) ? analyzer.getLevels(analyzerLevelsCache) : getDefaultLevels();
for (i in 0...min(group.members.length, levels.length))
for (i in 0...min(group.members.length, analyzerLevelsCache.length))
{
var animFrame:Int = (FlxG.sound.volume == 0 || FlxG.sound.muted) ? 0 : Math.round(levels[i].value * 6);
var animFrame:Int = (FlxG.sound.volume == 0 || FlxG.sound.muted) ? 0 : Math.round(analyzerLevelsCache[i].value * 6);
// don't display if we're at 0 volume from the level
group.members[i].visible = animFrame > 0;
@@ -176,7 +176,7 @@ class SpectogramSprite extends FlxTypedSpriteGroup<FlxSprite>
for (i in 0...group.members.length)
{
// needs to be exponential growth / scaling
// still need to optmize the FFT to run better, gets only samples needed?
// still need to optimize the FFT to run better, gets only samples needed?
// not every frequency is built the same!
// 20hz to 40z is a LOT of subtle low ends, but somethin like 20,000hz to 20,020hz, the difference is NOT the same!
+1 -1
View File
@@ -166,7 +166,7 @@ class WaveformData
var ratio = newSamplesPerPoint / samplesPerPoint;
if (ratio == 1) return result;
if (ratio < 1) trace('[WARNING] Downsampling will result in a low precision.');
if (ratio < 1) trace(' WARNING '.warning() + ' Downsampling will result in a low precision.');
var inputSampleCount = this.lenSamples();
var outputSampleCount = Std.int(inputSampleCount * ratio);
+66 -14
View File
@@ -3,6 +3,10 @@ package funkin.audio.waveform;
import funkin.graphics.rendering.MeshRender;
import flixel.util.FlxColor;
/**
* A sprite which displays the waveform of audio data.
* Generate a WaveformData and provide it to this sprite.
*/
class WaveformSprite extends MeshRender
{
static final DEFAULT_COLOR:FlxColor = FlxColor.WHITE;
@@ -18,14 +22,17 @@ class WaveformSprite extends MeshRender
* Do this any time the data or drawable area of the waveform changes.
* This often (but not always) needs to be done every frame.
*/
var isWaveformDirty:Bool = true;
var isWaveformDirty:Bool;
/**
* If true, force the waveform to redraw every frame.
* Useful if the waveform's clipRect is constantly changing.
*/
public var forceUpdate:Bool = false;
public var forceUpdate:Bool;
/**
* The data to render the waveform with.
*/
public var waveformData(default, set):Null<WaveformData>;
function set_waveformData(value:Null<WaveformData>):Null<WaveformData>
@@ -52,6 +59,9 @@ class WaveformSprite extends MeshRender
return waveformColor;
}
/**
* Whether the Waveform is horizontal or vertical.
*/
public var orientation(default, set):WaveformOrientation;
function set_orientation(value:WaveformOrientation):WaveformOrientation
@@ -68,7 +78,7 @@ class WaveformSprite extends MeshRender
*/
public var time(default, set):Float;
function set_time(value:Float)
function set_time(value:Float):Float
{
if (time == value) return value;
@@ -77,13 +87,22 @@ class WaveformSprite extends MeshRender
return time;
}
override function set_visible(value:Bool):Bool
{
if (visible == value) return value;
visible = value;
isWaveformDirty = true;
return visible;
}
/**
* The duration, in seconds, that the waveform represents.
* The section of waveform from `time` to `time + duration` and `width` are used to determine how many samples each pixel represents.
*/
public var duration(default, set):Float;
function set_duration(value:Float)
function set_duration(value:Float):Float
{
if (duration == value) return value;
@@ -120,13 +139,13 @@ class WaveformSprite extends MeshRender
*
* NOTE: This is technically doubled since it's applied above and below the center of the waveform.
*/
public var minWaveformSize:Int = 1;
public var minWaveformSize:Int;
/**
* A multiplier on the size of the waveform.
* Still capped at the width and height set for the sprite.
*/
public var amplitude:Float = 1.0;
public var amplitude:Float;
public function new(?waveformData:WaveformData, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float)
{
@@ -135,6 +154,11 @@ class WaveformSprite extends MeshRender
this.width = DEFAULT_WIDTH;
this.height = DEFAULT_HEIGHT;
this.minWaveformSize = 1;
this.amplitude = 1.0;
this.isWaveformDirty = true;
this.forceUpdate = false;
this.waveformData = waveformData;
this.orientation = orientation ?? DEFAULT_ORIENTATION;
this.time = 0.0;
@@ -151,7 +175,7 @@ class WaveformSprite extends MeshRender
isWaveformDirty = true;
}
public override function update(elapsed:Float)
public override function update(elapsed:Float):Void
{
super.update(elapsed);
@@ -170,6 +194,11 @@ class WaveformSprite extends MeshRender
makeGraphic(1, 1, this.waveformColor);
}
public override function draw():Void
{
super.draw();
}
/**
* @param offsetX Horizontal offset to draw the waveform at, in samples.
*/
@@ -183,12 +212,12 @@ class WaveformSprite extends MeshRender
this.clear();
if (waveformData == null) return;
if (waveformData == null || !this.visible) return;
// Center point of the waveform. When horizontal this is half the height, when vertical this is half the width.
var waveformCenterPos:Int = orientation == HORIZONTAL ? Std.int(this.height / 2) : Std.int(this.width / 2);
var oneSecondInIndices:Int = waveformData.secondsToIndex(1);
// var oneSecondInIndices:Int = waveformData.secondsToIndex(1)
var startTime:Float = time;
var endTime:Float = time + duration;
@@ -223,7 +252,10 @@ class WaveformSprite extends MeshRender
var isBeforeClipRect:Bool = (clipRect != null) && ((orientation == HORIZONTAL) ? pixelPos < clipRect.x : pixelPos < clipRect.y);
if (isBeforeClipRect) continue;
if (isBeforeClipRect)
{
continue;
}
var isAfterClipRect:Bool = (clipRect != null)
&& ((orientation == HORIZONTAL) ? pixelPos > (clipRect.x + clipRect.width) : pixelPos > (clipRect.y + clipRect.height));
@@ -426,20 +458,40 @@ class WaveformSprite extends MeshRender
}
}
public static function buildFromWaveformData(data:WaveformData, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float)
/**
* Build a WaveformSprite from waveform data.
* @param data The data for the waveform to use.
* @param orientation Whether the waveform should be horizontal or vertical.
* @param color The color of the waveform.
* @param duration The width of the waveform, in seconds.
*
* @return The resulting WaveformSprite.
*/
public static function buildFromWaveformData(data:WaveformData, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float):WaveformSprite
{
return new WaveformSprite(data, orientation, color, duration);
}
public static function buildFromFunkinSound(sound:FunkinSound, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float)
/**
* Build a WaveformSprite from a FunkinSound's waveform data.
* @param sound The audio for the waveform to use.
* @param orientation Whether the waveform should be horizontal or vertical.
* @param color The color of the waveform.
* @param duration The width of the waveform, in seconds.
*
* @return The resulting WaveformSprite.
*/
public static function buildFromFunkinSound(sound:FunkinSound, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float):WaveformSprite
{
// TODO: Build waveform data from FunkinSound.
var data = null;
var data = WaveformDataParser.interpretFlxSound(sound);
return buildFromWaveformData(data, orientation, color, duration);
}
}
/**
* The possible orientations of a waveform.
*/
enum WaveformOrientation
{
HORIZONTAL;
+9 -9
View File
@@ -18,7 +18,7 @@ typedef EntryConstructorFunction = (String, ?Dynamic) -> Void;
*/
@:nullSafety
@:generic
@:autoBuild(funkin.util.macro.DataRegistryMacro.buildRegistry())
@:autoBuild(funkin.util.macro.RegistryMacro.buildRegistry())
abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructorFunction>), J, P>
{
/**
@@ -78,7 +78,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
// SCRIPTED ENTRIES
//
var scriptedEntryClassNames:Array<String> = getScriptedClassNames();
log('Parsing ${scriptedEntryClassNames.length} scripted entries...');
log(' INFO '.info() + 'Parsing ${scriptedEntryClassNames.length} scripted entries...');
for (entryCls in scriptedEntryClassNames)
{
@@ -112,7 +112,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
var unscriptedEntryIds:Array<String> = entryIdList.filter(function(entryId:String):Bool {
return !entries.exists(entryId);
});
log('Parsing ${unscriptedEntryIds.length} unscripted entries...');
log(' INFO '.info() + 'Parsing ${unscriptedEntryIds.length} unscripted entries...');
for (entryId in unscriptedEntryIds)
{
try
@@ -120,14 +120,14 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
var entry:Null<T> = createEntry(entryId);
if (entry != null)
{
trace(' Loaded entry data: ${entry}');
log('Loaded entry data: ${entry}');
entries.set(entry.id, entry);
}
}
catch (e)
{
// Print the error.
trace(' Failed to load entry data: ${entryId}');
log(' WARNING '.warning() + ' Failed to load entry data: ${entryId}');
trace(e);
continue;
}
@@ -157,7 +157,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
* @param id The ID of the entry.
* @return `true` if the entry has an attached script, `false` otherwise.
*/
public function isScriptedEntry(id:String):Bool
public function isScriptedEntry(id:String, ?params:Null<P>):Bool
{
return scriptedEntryIds.exists(id);
}
@@ -167,7 +167,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):Null<String>
public function getScriptedEntryClassName(id:String, ?params:Null<P>):Null<String>
{
return scriptedEntryIds.get(id);
}
@@ -224,7 +224,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
function log(message:String):Void
{
trace('[' + registryId + '] ' + message);
trace(' $registryId '.bold().bg_note_down() + ' $message');
}
function loadEntryFile(id:String):JsonFile
@@ -333,7 +333,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
function printErrors(errors:Array<json2object.Error>, id:String = ''):Void
{
trace('[${registryId}] Failed to parse entry data: ${id}');
trace(' $registryId '.bold().bg_note_down() + ' ERROR '.error() + 'Failed to parse entry data: ${id}');
for (error in errors)
{
+12 -12
View File
@@ -12,27 +12,27 @@ class DataError
switch (error)
{
case IncorrectType(vari, expected, pos):
trace(' Expected field "$vari" to be of type "$expected".');
trace(' ERROR '.error() + 'Expected field "$vari" to be of type "$expected".');
printPos(pos);
case IncorrectEnumValue(value, expected, pos):
trace(' Invalid enum value (expected "$expected", got "$value")');
trace(' ERROR '.error() + 'Invalid enum value (expected "$expected", got "$value")');
printPos(pos);
case InvalidEnumConstructor(value, expected, pos):
trace(' Invalid enum constructor (epxected "$expected", got "$value")');
trace(' ERROR '.error() + 'Invalid enum constructor (epxected "$expected", got "$value")');
printPos(pos);
case UninitializedVariable(vari, pos):
trace(' Uninitialized variable "$vari"');
trace(' ERROR '.error() + 'Uninitialized variable "$vari"');
printPos(pos);
case UnknownVariable(vari, pos):
trace(' Unknown variable "$vari"');
trace(' ERROR '.error() + 'Unknown variable "$vari"');
printPos(pos);
case ParserError(message, pos):
trace(' Parsing error: ${message}');
trace(' ERROR '.error() + 'Parsing error: ${message}');
printPos(pos);
case CustomFunctionException(e, pos):
if (Std.isOfType(e, String))
{
trace(' ${e}');
trace(' ERROR '.error() + '${e}');
}
else
{
@@ -49,11 +49,11 @@ class DataError
switch (Type.typeof(e))
{
case TClass(c):
trace(' [${Type.getClassName(c)}] ${e.toString()}');
trace(' ERROR '.error() + '(${Type.getClassName(c)}) ${e.toString()}');
case TEnum(c):
trace(' [${Type.getEnumName(c)}] ${e.toString()}');
trace(' ERROR '.error() + '(${Type.getEnumName(c)}) ${e.toString()}');
default:
trace(' [${Type.typeof(e)}] ${e.toString()}');
trace(' ERROR '.error() + '(${Type.typeof(e)}) ${e.toString()}');
}
}
@@ -66,11 +66,11 @@ class DataError
{
if (pos.lines[0].number == pos.lines[pos.lines.length - 1].number)
{
trace(' at ${(pos.file == '') ? 'line ' : '${pos.file}:'}${pos.lines[0].number}');
trace(' at ${(pos.file == '') ? 'line ' : '${pos.file}:'}${pos.lines[0].number}');
}
else
{
trace(' at ${(pos.file == '') ? 'line ' : '${pos.file}:'}${pos.lines[0].number}-${pos.lines[pos.lines.length - 1].number}');
trace(' at ${(pos.file == '') ? 'line ' : '${pos.file}:'}${pos.lines[0].number}-${pos.lines[pos.lines.length - 1].number}');
}
}
}
+10 -1
View File
@@ -83,7 +83,7 @@ typedef UnnamedAnimationData =
/**
* Optionally specify an asset path to use for this specific animation.
* ONLY for use by MultiSparrow characters.
* ONLY for use by MultiSparrow and MultiAnimateAtlas characters.
* @default The assetPath of the parent sprite
*/
@:optional
@@ -138,4 +138,13 @@ typedef UnnamedAnimationData =
@:default([])
@:optional
var frameIndices:Null<Array<Int>>;
/**
* The type of animation to use.
* Only available for texture atlases.
* Options: "framelabel", "symbol"
*/
@:default("framelabel")
@:optional
var animType:String;
}
@@ -1,4 +1,4 @@
package funkin.play.character;
package funkin.data.character;
import funkin.data.animation.AnimationData;
import funkin.modding.events.ScriptEvent;
@@ -6,8 +6,15 @@ import funkin.modding.events.ScriptEventDispatcher;
import funkin.play.character.ScriptedCharacter.ScriptedAnimateAtlasCharacter;
import funkin.play.character.ScriptedCharacter.ScriptedBaseCharacter;
import funkin.play.character.ScriptedCharacter.ScriptedMultiSparrowCharacter;
import funkin.play.character.ScriptedCharacter.ScriptedMultiAnimateAtlasCharacter;
import funkin.play.character.ScriptedCharacter.ScriptedPackerCharacter;
import funkin.play.character.ScriptedCharacter.ScriptedSparrowCharacter;
import funkin.play.character.AnimateAtlasCharacter;
import funkin.play.character.BaseCharacter;
import funkin.play.character.SparrowCharacter;
import funkin.play.character.MultiSparrowCharacter;
import funkin.play.character.MultiAnimateAtlasCharacter;
import funkin.play.character.PackerCharacter;
import funkin.util.assets.DataAssets;
import funkin.util.VersionUtil;
import haxe.Json;
@@ -44,7 +51,7 @@ class CharacterDataParser
{
// Clear any stages that are cached if there were any.
clearCharacterCache();
trace('[CHARACTER] Parsing all entries...');
log(' INFO '.info() + 'Parsing all entries...');
//
// UNSCRIPTED CHARACTERS
@@ -53,7 +60,7 @@ class CharacterDataParser
var unscriptedCharIds:Array<String> = charIdList.filter(function(charId:String):Bool {
return !characterCache.exists(charId);
});
trace(' Fetching data for ${unscriptedCharIds.length} characters...');
log('Fetching data for ${unscriptedCharIds.length} characters...');
for (charId in unscriptedCharIds)
{
try
@@ -61,7 +68,7 @@ class CharacterDataParser
var charData:Null<CharacterData> = parseCharacterData(charId);
if (charData != null)
{
trace(' Loaded character data: ${charId}');
log('Loaded character "${charId}"');
characterCache.set(charId, charData);
}
}
@@ -81,19 +88,19 @@ class CharacterDataParser
var scriptedCharClassNames1:Array<String> = ScriptedSparrowCharacter.listScriptClasses();
if (scriptedCharClassNames1.length > 0)
{
trace(' Instantiating ${scriptedCharClassNames1.length} (Sparrow) scripted characters...');
log('Instantiating ${scriptedCharClassNames1.length} (Sparrow) scripted characters...');
for (charCls in scriptedCharClassNames1)
{
try
{
var character:SparrowCharacter = ScriptedSparrowCharacter.init(charCls, DEFAULT_CHAR_ID);
trace(' Initialized character ${character.characterName}');
log('Loaded character ${character.characterName} (scripted: $charCls)');
characterScriptedClass.set(character.characterId, charCls);
}
catch (e)
{
trace(' FAILED to instantiate scripted Sparrow character: ${charCls}');
trace(e);
log(' ERROR '.error() + 'Failed to initialize scripted Sparrow character: $charCls');
log(' ERROR '.error() + '$e');
}
}
}
@@ -101,18 +108,19 @@ class CharacterDataParser
var scriptedCharClassNames2:Array<String> = ScriptedPackerCharacter.listScriptClasses();
if (scriptedCharClassNames2.length > 0)
{
trace(' Instantiating ${scriptedCharClassNames2.length} (Packer) scripted characters...');
log('Instantiating ${scriptedCharClassNames2.length} (Packer) scripted characters...');
for (charCls in scriptedCharClassNames2)
{
try
{
var character:PackerCharacter = ScriptedPackerCharacter.init(charCls, DEFAULT_CHAR_ID);
log('Loaded character ${character.characterName} (scripted: $charCls)');
characterScriptedClass.set(character.characterId, charCls);
}
catch (e)
{
trace(' FAILED to instantiate scripted Packer character: ${charCls}');
trace(e);
log(' ERROR '.error() + 'Failed to initialize scripted Packer character: $charCls');
log(' ERROR '.error() + '$e');
}
}
}
@@ -120,18 +128,19 @@ class CharacterDataParser
var scriptedCharClassNames3:Array<String> = ScriptedMultiSparrowCharacter.listScriptClasses();
if (scriptedCharClassNames3.length > 0)
{
trace(' Instantiating ${scriptedCharClassNames3.length} (Multi-Sparrow) scripted characters...');
log('Instantiating ${scriptedCharClassNames3.length} (Multi-Sparrow) scripted characters...');
for (charCls in scriptedCharClassNames3)
{
try
{
var character:MultiSparrowCharacter = ScriptedMultiSparrowCharacter.init(charCls, DEFAULT_CHAR_ID);
log('Loaded character ${character.characterName} (scripted: $charCls)');
characterScriptedClass.set(character.characterId, charCls);
}
catch (e)
{
trace(' FAILED to instantiate scripted Multi-Sparrow character: ${charCls}');
trace(e);
log(' ERROR '.error() + 'Failed to initialize scripted Multi-Sparrow character: $charCls');
log(' ERROR '.error() + '$e');
}
}
}
@@ -139,18 +148,39 @@ class CharacterDataParser
var scriptedCharClassNames4:Array<String> = ScriptedAnimateAtlasCharacter.listScriptClasses();
if (scriptedCharClassNames4.length > 0)
{
trace(' Instantiating ${scriptedCharClassNames4.length} (Animate Atlas) scripted characters...');
log('Instantiating ${scriptedCharClassNames4.length} (Animate Atlas) scripted characters...');
for (charCls in scriptedCharClassNames4)
{
try
{
var character:AnimateAtlasCharacter = ScriptedAnimateAtlasCharacter.init(charCls, DEFAULT_CHAR_ID);
log('Loaded character ${character.characterName} (scripted: $charCls)');
characterScriptedClass.set(character.characterId, charCls);
}
catch (e)
{
trace(' FAILED to instantiate scripted Animate Atlas character: ${charCls}');
trace(e);
log(' ERROR '.error() + 'Failed to initialize scripted Animate Atlas character: $charCls');
log(' ERROR '.error() + '$e');
}
}
}
var scriptedCharClassNames5:Array<String> = ScriptedMultiAnimateAtlasCharacter.listScriptClasses();
if (scriptedCharClassNames5.length > 0)
{
log('Instantiating ${scriptedCharClassNames5.length} (Multi-Animate Atlas) scripted characters...');
for (charCls in scriptedCharClassNames5)
{
try
{
var character:MultiAnimateAtlasCharacter = ScriptedMultiAnimateAtlasCharacter.init(charCls, DEFAULT_CHAR_ID);
log('Loaded character ${character.characterName} (scripted: $charCls)');
characterScriptedClass.set(character.characterId, charCls);
}
catch (e)
{
log(' ERROR '.error() + 'Failed to initialize scripted Multi-Animate Atlas character: $charCls');
log(' ERROR '.error() + '$e');
}
}
}
@@ -162,29 +192,30 @@ class CharacterDataParser
return !(scriptedCharClassNames1.contains(charCls)
|| scriptedCharClassNames2.contains(charCls)
|| scriptedCharClassNames3.contains(charCls)
|| scriptedCharClassNames4.contains(charCls));
|| scriptedCharClassNames4.contains(charCls)
|| scriptedCharClassNames5.contains(charCls));
});
if (scriptedCharClassNames.length > 0)
{
trace(' Instantiating ${scriptedCharClassNames.length} (Base) scripted characters...');
log('Instantiating ${scriptedCharClassNames.length} (Base) scripted characters...');
for (charCls in scriptedCharClassNames)
{
var character:BaseCharacter = ScriptedBaseCharacter.init(charCls, DEFAULT_CHAR_ID, Custom);
if (character == null)
{
trace(' Failed to instantiate scripted character: ${charCls}');
log(' ERROR '.error() + 'Failed to initialize scripted character: $charCls');
continue;
}
else
{
trace(' Successfully instantiated scripted character: ${charCls}');
log('Loaded character ${character.characterName} (scripted: $charCls)');
characterScriptedClass.set(character.characterId, charCls);
}
}
}
trace(' Successfully loaded ${Lambda.count(characterCache)} stages.');
log(' INFO '.info() + 'Successfully loaded ${characterCache.size()} stages.');
}
/**
@@ -221,6 +252,8 @@ class CharacterDataParser
char = ScriptedSparrowCharacter.init(charScriptClass, charId);
case CharacterRenderType.Packer:
char = ScriptedPackerCharacter.init(charScriptClass, charId);
case CharacterRenderType.MultiAnimateAtlas:
char = ScriptedMultiAnimateAtlasCharacter.init(charScriptClass, charId);
default:
// We're going to assume that the script class does the rendering.
char = ScriptedBaseCharacter.init(charScriptClass, charId, CharacterRenderType.Custom);
@@ -238,8 +271,10 @@ class CharacterDataParser
char = new SparrowCharacter(charId);
case CharacterRenderType.Packer:
char = new PackerCharacter(charId);
case CharacterRenderType.MultiAnimateAtlas:
char = new MultiAnimateAtlasCharacter(charId);
default:
trace('[WARN] Creating character with undefined renderType ${charData.renderType}');
trace(' WARNING '.warning() + ' Creating character with undefined renderType ${charData.renderType}');
char = new BaseCharacter(charId, CharacterRenderType.Custom);
}
}
@@ -307,7 +342,7 @@ class CharacterDataParser
if (!Assets.exists(Paths.image(charPath)))
{
trace('[WARN] Character ${char} has no freeplay icon.');
trace(' WARNING '.warning() + ' Character ${char} has no freeplay icon.');
return null;
}
@@ -324,7 +359,7 @@ class CharacterDataParser
if (idleFrame == null)
{
trace('[WARN] Character ${char} has no idle in their freeplay icon.');
trace(' WARNING '.warning() + ' Character ${char} has no idle in their freeplay icon.');
return null;
}
@@ -399,8 +434,8 @@ class CharacterDataParser
}
catch (e)
{
trace(' Error parsing data for character: ${charId}');
trace(' ${e}');
trace(' Error parsing data for character: ${charId}');
trace(' ${e}');
return null;
}
}
@@ -426,6 +461,16 @@ class CharacterDataParser
public static final DEFAULT_SCALE:Float = 1;
public static final DEFAULT_SCROLL:Array<Float> = [0, 0];
public static final DEFAULT_STARTINGANIM:String = 'idle';
public static final DEFAULT_APPLYSTAGEMATRIX:Bool = false;
public static final DEFAULT_ANIMTYPE:String = "framelabel";
public static final DEFAULT_ATLASSETTINGS:funkin.data.stage.StageData.TextureAtlasData =
{
swfMode: true,
cacheOnLoad: false,
filterQuality: 1,
applyStageMatrix: false,
useRenderTexture: false
};
/**
* Set unspecified parameters to their defaults.
@@ -554,6 +599,16 @@ class CharacterDataParser
input.flipX = DEFAULT_FLIPX;
}
if (input.applyStageMatrix == null)
{
input.applyStageMatrix = DEFAULT_APPLYSTAGEMATRIX;
}
if (input.atlasSettings == null)
{
input.atlasSettings = DEFAULT_ATLASSETTINGS;
}
if (input.animations.length == 0 && input.startingAnimation != null)
{
return null;
@@ -591,11 +646,21 @@ class CharacterDataParser
{
inputAnimation.flipY = DEFAULT_FLIPY;
}
if (inputAnimation.animType == null)
{
inputAnimation.animType = DEFAULT_ANIMTYPE;
}
}
// All good!
return input;
}
static function log(message:String):Void
{
trace(' CHARACTER '.bold().bg_note_down() + ' $message');
}
}
/**
@@ -619,10 +684,15 @@ enum abstract CharacterRenderType(String) from String to String
public var MultiSparrow = 'multisparrow';
/**
* Renders the character using a spritesheet of symbols and JSON data.
* Renders the character using a single spritesheet of symbols and JSON data.
*/
public var AnimateAtlas = 'animateatlas';
/**
* Renders the character using multiple spritesheets of symbols and JSON data.
*/
public var MultiAnimateAtlas = 'multianimateatlas';
/**
* Renders the character using a custom method.
*/
@@ -669,6 +739,9 @@ typedef CharacterData =
*/
var healthIcon:Null<HealthIconData>;
/**
* Optional data about the death animation for the character.
*/
var death:Null<DeathData>;
/**
@@ -729,6 +802,23 @@ typedef CharacterData =
* @default false
*/
var flipX:Null<Bool>;
/**
* NOTE: This only applies to animate atlas characters.
*
* Whether to apply the stage matrix, if it was exported from a symbol instance.
* Also positions the Texture Atlas as it displays in Animate.
* Turning this on is only recommended if you prepositioned the character in Animate.
* For other cases, it should be turned off to act similarly to a normal FlxSprite.
*/
var applyStageMatrix:Null<Bool>;
/**
* Various settings for the prop.
* Only available for texture atlases.
*/
@:optional
var atlasSettings:funkin.data.stage.StageData.TextureAtlasData;
};
/**
@@ -13,7 +13,7 @@ class SongEventRegistry
{
/**
* Every built-in event class must be added to this list.
* Thankfully, with the power of `SongEventMacro`, this is done automatically.
* Thankfully, with the power of `ClassMacro`, this is done automatically.
*/
static final BUILTIN_EVENTS:List<Class<SongEvent>> = ClassMacro.listSubclassesOf(SongEvent);
@@ -46,12 +46,12 @@ class SongEventRegistry
if (event != null)
{
trace(' Loaded built-in song event: ${event.id}');
trace(' Loaded built-in song event: ${event.id}');
eventCache.set(event.id, event);
}
else
{
trace(' Failed to load built-in song event: ${Type.getClassName(eventCls)}');
trace(' Failed to load built-in song event: ${Type.getClassName(eventCls)}');
}
}
}
@@ -68,12 +68,12 @@ class SongEventRegistry
if (event != null)
{
trace(' Loaded scripted song event: ${event.id}');
trace(' Loaded scripted song event: ${event.id}');
eventCache.set(event.id, event);
}
else
{
trace(' Failed to instantiate scripted song event class: ${eventCls}');
trace(' Failed to instantiate scripted song event class: ${eventCls}');
}
}
}
+137 -14
View File
@@ -1,5 +1,6 @@
package funkin.data.event;
@:nullSafety
@:forward(name, title, type, keys, min, max, step, units, defaultValue, iterator)
abstract SongEventSchema(SongEventSchemaRaw)
{
@@ -10,13 +11,22 @@ abstract SongEventSchema(SongEventSchemaRaw)
public function new(?fields:Array<SongEventSchemaField>)
{
this = fields;
this = fields ?? [];
}
/**
* Retrieve a SongEventSchemaField by name. This works even if the field is inside a Frame.
* You can use array access to call this function; `schema["field_name"]`
*
* @param name The name of the field to retreive.
* @return The retrieved field, or null if not found.
*/
@:arrayAccess
public function getByName(name:String):SongEventSchemaField
public function getByName(name:String):Null<SongEventSchemaField>
{
for (field in this)
var allFields = listAllFields(this);
for (field in allFields)
{
if (field.name == name) return field;
}
@@ -24,26 +34,70 @@ abstract SongEventSchema(SongEventSchemaRaw)
return null;
}
public function getFirstField():SongEventSchemaField
/**
* Return whether the field with the given name exists.
* @param name The name of the field to check.
* @return Whether the field exists.
*/
public function hasField(name:String):Bool
{
return abstract.getByName(name) != null;
}
/**
* Retrieve the first field in the schema.
* @return The first field.
*/
public function getFirstField():Null<SongEventSchemaField>
{
return this[0];
}
/**
* Retrieve a field from the schema by numeric index.
* @param key The index of the field to retrieve.
* @return The retrieved field.
*/
@:arrayAccess
public inline function get(key:Int)
public inline function get(key:Int):Null<SongEventSchemaField>
{
return this[key];
}
/**
* Write a field to the schema by numeric index.
* @param k The index of the field to write.
* @param v The new field value to write.
* @return The assigned value.
*/
@:arrayAccess
public inline function arrayWrite(k:Int, v:SongEventSchemaField):SongEventSchemaField
{
return this[k] = v;
}
/**
* For a given song event field, retrieve its default value.
* @param name The name of the field to retrieve.
* @return The default value of the field, or null if not found.
*/
public function getDefaultFieldValue(name:String):Null<Dynamic>
{
return getByName(name)?.defaultValue;
}
/**
* For a given song event field, convert the value into a string.
* This is particularly useful for ENUM fields.
*
* @param name The name of the field to display.
* @param value The value of the field to convert.
* @param addUnits Whether to add the units specified by the schema to the resulting string.
* @return The resulting string.
*/
public function stringifyFieldValue(name:String, value:Dynamic, addUnits:Bool = true):String
{
var field:SongEventSchemaField = getByName(name);
var field:Null<SongEventSchemaField> = getByName(name);
if (field == null) return 'Unknown';
switch (field.type)
@@ -62,10 +116,12 @@ abstract SongEventSchema(SongEventSchemaRaw)
return Std.string(value);
case SongEventFieldType.ENUM:
var valueString:String = Std.string(value);
for (key in field.keys.keys())
var fieldKeys:Array<String> = field.keys?.keyValues() ?? [];
for (key in fieldKeys)
{
var value:Null<Dynamic> = field.keys?.get(key) ?? null;
// Comparing these values as strings because comparing Dynamic variables is jank.
if (Std.string(field.keys.get(key)) == valueString) return key;
if (Std.string(value) == valueString) return key;
}
return valueString;
default:
@@ -73,7 +129,13 @@ abstract SongEventSchema(SongEventSchemaRaw)
}
}
function addUnitsToString(value:String, field:SongEventSchemaField)
/**
* Apply the song event field's specified units to the value.
* @param value The value to add the units to.
* @param field The field to get the units from.
* @return The resulting string.
*/
function addUnitsToString(value:String, field:SongEventSchemaField):String
{
if (field.units == null || field.units == '') return value;
@@ -81,10 +143,50 @@ abstract SongEventSchema(SongEventSchemaRaw)
return value + (NO_SPACE_UNITS.contains(unit) ? '' : ' ') + '${unit}';
}
/**
* Build a flat list of all the fields in the schema. Frames containing children are parsed recursively.
*
* @param schema The song event schema schema to parse.
* @return The array of fields, parsed recursively from the schema and its child frames.
*/
function listAllFields(schema:SongEventSchemaRaw):Array<SongEventSchemaField>
{
var result:Array<SongEventSchemaField> = [];
for (field in schema)
{
if (field.children == null)
{
result.push(field);
}
else
{
result = result.concat(field.children);
}
}
return result;
}
/**
* Get a list of all the field names in the schema, so they can be iterated over and retrieved.
* @return The list of field names.
*/
public function listAllFieldNames():Array<String>
{
return listAllFields(this).map((field:SongEventSchemaField) -> field.name);
}
}
/**
* The raw underlying data for a song event schema is an array of fields.
*/
typedef SongEventSchemaRaw = Array<SongEventSchemaField>;
/**
* The individual fields of a song event schema.
*/
typedef SongEventSchemaField =
{
/**
@@ -135,37 +237,58 @@ typedef SongEventSchemaField =
*/
?units:String,
/**
* Used for FRAME values.
* The child components that this frame contains.
*/
?children:SongEventSchemaRaw,
/**
* Used for FRAME values.
* Whether to make the frame be collapsible.
*/
?collapsible:Bool,
/**
* An optional default value for the field.
*/
?defaultValue:Dynamic,
}
/**
* The available field types for a song event schema.
*/
enum abstract SongEventFieldType(String) from String to String
{
/**
* The STRING type will display as a text field.
*/
var STRING = "string";
public var STRING = "string";
/**
* The INTEGER type will display as a text field that only accepts numbers.
*/
var INTEGER = "integer";
public var INTEGER = "integer";
/**
* The FLOAT type will display as a text field that only accepts numbers.
*/
var FLOAT = "float";
public var FLOAT = "float";
/**
* The BOOL type will display as a checkbox.
*/
var BOOL = "bool";
public var BOOL = "bool";
/**
* The ENUM type will display as a dropdown.
* Make sure to specify the `keys` field in the schema.
*/
var ENUM = "enum";
public var ENUM = "enum";
/**
* The FRAME type will display a frame with child components.
* Make sure to specify the `children` field in the schema.
*/
public var FRAME = "frame";
}
@@ -89,7 +89,7 @@ class PlayerData
updateVersionToLatest();
var writer = new json2object.JsonWriter<PlayerData>();
return writer.write(this, pretty ? ' ' : null);
return writer.write(this, pretty ? ' ' : null);
}
public function updateVersionToLatest():Void
@@ -103,6 +103,10 @@ class PlayerFreeplayDJData
var assetPath:String;
var animations:Array<AnimationData>;
@:optional
@:default(false)
var applyStageMatrix:Bool;
@:optional
@:default("BOYFRIEND")
var text1:String;
@@ -130,6 +134,14 @@ class PlayerFreeplayDJData
@:optional
var fistPump:Null<PlayerFreeplayDJFistPumpData>;
@:optional
@:default("animateatlas")
public var renderType:Null<String>;
@:optional
@:default("")
public var scriptClass:Null<String>;
public function new()
{
animationMap = new Map();
@@ -149,9 +161,15 @@ class PlayerFreeplayDJData
}
}
public function getAtlasPath():String
public inline function getAssetPath():String
return assetPath; // return assetPath;
public inline function getAnimationsList():Array<AnimationData>
return animations;
public function useApplyStageMatrix():Bool
{
return Paths.animateAtlas(assetPath);
return applyStageMatrix;
}
public function getFreeplayDJText(index:Int):String
@@ -334,6 +352,10 @@ typedef PlayerResultsAnimationData =
*/
var renderType:String;
@:optional
@:default(false)
var applyStageMatrix:Bool;
@:optional
var assetPath:Null<String>;
@@ -70,7 +70,7 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData, PlayerE
public function hasNewCharacter():Bool
{
#if (!UNLOCK_EVERYTHING)
var charactersSeen = Save.instance.charactersSeen.clone();
var charactersSeen = Save.instance.charactersSeen.value.clone();
for (charId in listEntryIds())
{
@@ -94,7 +94,7 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData, PlayerE
var result = [];
#if (!UNLOCK_EVERYTHING)
var charactersSeen = Save.instance.charactersSeen.clone();
var charactersSeen = Save.instance.charactersSeen.value.clone();
for (charId in listEntryIds())
{
var player = fetchEntry(charId);
@@ -143,7 +143,7 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData, PlayerE
#if UNLOCK_EVERYTHING
return true;
#else
return Save.instance.charactersSeen.contains(characterId);
return Save.instance.charactersSeen.value.contains(characterId);
#end
}
}
+206 -13
View File
@@ -126,7 +126,7 @@ class SongMetadata implements ICloneable<SongMetadata>
// I believe @:jignored should be ignored by the writer?
// var output = this.clone();
// output.variation = null; // Not sure how to make a field optional on the reader and ignored on the writer.
return writer.write(this, pretty ? ' ' : null);
return writer.write(this, pretty ? ' ' : null);
}
public function updateVersionToLatest():Void
@@ -664,7 +664,7 @@ class SongChartData implements ICloneable<SongChartData>
var ignoreNullOptionals = true;
var writer = new json2object.JsonWriter<SongChartData>(ignoreNullOptionals);
return writer.write(this, pretty ? ' ' : null);
return writer.write(this, pretty ? ' ' : null);
}
public function updateVersionToLatest():Void
@@ -700,6 +700,10 @@ class SongChartData implements ICloneable<SongChartData>
}
}
/**
* The raw underlying data for a song event.
* This value gets wrapped in an `abstract` to allow operators to be overloaded.
*/
class SongEventDataRaw implements ICloneable<SongEventDataRaw>
{
/**
@@ -735,7 +739,7 @@ class SongEventDataRaw implements ICloneable<SongEventDataRaw>
/**
* Whether this event has been activated.
* This is only used internally by the game. It should not be serialized.
* This is only used internally by the game during gameplay. It should not be serialized.
*/
@:jignored
public var activated:Bool = false;
@@ -747,9 +751,19 @@ class SongEventDataRaw implements ICloneable<SongEventDataRaw>
this.value = value;
}
/**
* The cached step time of the event. Should not be serialized in case the BPM of the song changes.
*/
@:jignored
var _stepTime:Null<Float> = null;
/**
* Get the position of the event in the song, in steps.
* This value is cached for performance. Use `force` to require a recalculation.
*
* @param force Force the value to be recalculated.
* @return The position of the event in the song, in steps.
*/
public function getStepTime(force:Bool = false):Float
{
if (_stepTime != null && !force) return _stepTime;
@@ -757,6 +771,10 @@ class SongEventDataRaw implements ICloneable<SongEventDataRaw>
return _stepTime = Conductor.instance.getTimeInSteps(this.time);
}
/**
* Clone this event data, creating an independent instance with identical data.
* @return The newly created event data.
*/
public function clone():SongEventDataRaw
{
return new SongEventDataRaw(this.time, this.eventKind, this.value);
@@ -784,26 +802,49 @@ class SongEventDataRaw implements ICloneable<SongEventDataRaw>
}
}
/**
* Retrieve the SongEvent handler class for this event.
* @return The handler class, or `null` if not found.
*/
public function getHandler():Null<SongEvent>
{
return SongEventRegistry.getEvent(this.eventKind);
}
/**
* Retrieve the SongEventSchema for this event.
* @return The schema, or `null` if not found.
*/
public function getSchema():Null<SongEventSchema>
{
return SongEventRegistry.getEventSchema(this.eventKind);
}
/**
* Retrieve a field from this event's data, as a `Dynamic` value.
* @param key The name of the field to retrieve.
* @return The field value, or `null` if not provided.
*/
public function getDynamic(key:String):Null<Dynamic>
{
return this.value == null ? null : Reflect.field(this.value, key);
}
/**
* Retrieve a field from this event's data, as a `Bool` value.
* @param key The name of the field to retrieve.
* @return The field value, or `null` if not provided.
*/
public function getBool(key:String):Null<Bool>
{
return this.value == null ? null : cast Reflect.field(this.value, key);
}
/**
* Retrieve a field from this event's data, as an `Int` value.
* @param key The name of the field to retrieve.
* @return The field value, or `null` if not provided.
*/
public function getInt(key:String):Null<Int>
{
if (this.value == null) return null;
@@ -814,6 +855,11 @@ class SongEventDataRaw implements ICloneable<SongEventDataRaw>
return cast result;
}
/**
* Retrieve a field from this event's data, as a `Float` value.
* @param key The name of the field to retrieve.
* @return The field value, or `null` if not provided.
*/
public function getFloat(key:String):Null<Float>
{
if (this.value == null) return null;
@@ -824,21 +870,60 @@ class SongEventDataRaw implements ICloneable<SongEventDataRaw>
return cast result;
}
/**
* Retrieve a field from this event's data, as a `String` value.
* @param key The name of the field to retrieve.
* @return The field value, or `null` if not provided.
*/
public function getString(key:String):String
{
return this.value == null ? null : cast Reflect.field(this.value, key);
}
/**
* Retrieve a field from this event's data, as an `Array<Dynamic>` value.
* @param key The name of the field to retrieve.
* @return The field value, or `null` if not provided.
*/
public function getArray(key:String):Array<Dynamic>
{
return this.value == null ? null : cast Reflect.field(this.value, key);
}
/**
* Retrieve a field from this event's data, as an `Array<Bool>` value.
* @param key The name of the field to retrieve.
* @return The field value, or `null` if not provided.
*/
public function getBoolArray(key:String):Array<Bool>
{
return this.value == null ? null : cast Reflect.field(this.value, key);
}
/**
* Retrieve a field from this event's data, as an `Array<Float>` value.
* @param key The name of the field to retrieve.
* @return The field value, or `null` if not provided.
*/
public function getFloatArray(key:String):Array<Float>
{
return this.value == null ? null : cast Reflect.field(this.value, key);
}
/**
* Retrieve a field from this event's data, as an `Array<String>` value.
* @param key The name of the field to retrieve.
* @return The field value, or `null` if not provided.
*/
public function getStringArray(key:String):Array<Float>
{
return this.value == null ? null : cast Reflect.field(this.value, key);
}
/**
* Build the tooltip string for this particular instance of the song event.
* @return A tooltip string, containing readable values for the event's data.
*/
public function buildTooltip():String
{
var eventHandler = getHandler();
@@ -851,15 +936,26 @@ class SongEventDataRaw implements ICloneable<SongEventDataRaw>
var defaultKey = eventSchema.getFirstField()?.name;
var valueStruct:haxe.DynamicAccess<Dynamic> = valueAsStruct(defaultKey);
for (fieldName in eventSchema.listAllFieldNames())
{
var fieldValue:Dynamic = valueStruct.exists(fieldName) ? valueStruct.get(fieldName) : eventSchema.getDefaultFieldValue(fieldName);
var title:String = eventSchema.getByName(fieldName)?.title ?? 'UnknownField';
var valueStr:String = eventSchema.stringifyFieldValue(fieldName, fieldValue) ?? 'UnknownValue';
result += '\n- ${title}: ${valueStr}';
}
for (pair in valueStruct.keyValueIterator())
{
var key = pair.key;
var value = pair.value;
var fieldName:String = pair.key;
var fieldValue:Dynamic = pair.value;
var title = eventSchema.getByName(key)?.title ?? 'UnknownField';
// Already populated above.
if (eventSchema.hasField(fieldName)) continue;
// if (eventSchema.stringifyFieldValue(key, value) != null) trace(eventSchema.stringifyFieldValue(key, value));
var valueStr = eventSchema.stringifyFieldValue(key, value) ?? 'UnknownValue';
var title:String = eventSchema.getByName(fieldName)?.title ?? 'Unknown Field ($fieldName)';
var valueStr:String = eventSchema.stringifyFieldValue(fieldName, fieldValue) ?? 'Unknown Value ($fieldValue)';
result += '\n- ${title}: ${valueStr}';
}
@@ -880,41 +976,87 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
this = new SongEventDataRaw(time, eventKind, value);
}
/**
* Create an independent copy of this event with the same underlying data.
* @return The newly created event.
*/
public function clone():SongEventData
{
return new SongEventData(this.time, this.eventKind, this.value);
}
/**
* Check whether this event is equal to another event.
* Ex. `event1 == event2`
*
* @param other The other event to compare.
* @return Whether the events have the same time, kind, and data.
*/
@:op(A == B)
public function op_equals(other:SongEventData):Bool
{
return this.time == other.time && this.eventKind == other.eventKind && this.value == other.value;
}
/**
* Check whether this event is not equal to another event.
* Ex. `event1 != event2`
*
* @param other The other event to compare.
* @return Whether the events have a different same time, kind, or data.
*/
@:op(A != B)
public function op_notEquals(other:SongEventData):Bool
{
return this.time != other.time || this.eventKind != other.eventKind || this.value != other.value;
}
/**
* Check whether this event is later than another event.
* Ex. `event1 > event2`
*
* @param other The other event to compare.
* @return Bool
*/
@:op(A > B)
public function op_greaterThan(other:SongEventData):Bool
{
return this.time > other.time;
}
/**
* Check whether this event is earlier than another event.
* Ex. `event1 < event2`
*
* @param other The other event to compare.
* @return Whether this event is earlier than the other event.
*/
@:op(A < B)
public function op_lessThan(other:SongEventData):Bool
{
return this.time < other.time;
}
/**
* Check whether this event is later than or at the same time as another event.
* Ex. `event1 >= event2`
*
* @param other The other event to compare.
* @return Whether this event is later than or at the same time as the other event.
*/
@:op(A >= B)
public function op_greaterThanOrEquals(other:SongEventData):Bool
{
return this.time >= other.time;
}
/**
* Check whether this event is earlier than or at the same time as another event.
* Ex. `event1 <= event2`
*
* @param other The other event to compare.
* @return Whether this event is earlier than or at the same time as the other event.
*/
@:op(A <= B)
public function op_lessThanOrEquals(other:SongEventData):Bool
{
@@ -922,7 +1064,7 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
}
/**
* Produces a string representation suitable for debugging.
* Produces a string representation of this event, suitable for debugging.
*/
public function toString():String
{
@@ -930,6 +1072,9 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
}
}
/**
* The underlying data for a single note.
*/
class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
{
/**
@@ -991,6 +1136,10 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
return this.kind = value;
}
/**
* The parameters for the note.
* Used for custom behavior on custom note kinds. Defaults to an empty array.
*/
@:alias("p")
@:default([])
@:optional
@@ -1010,12 +1159,22 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
* Strips the strumline index from the data.
*
* 0 = left, 1 = down, 2 = up, 3 = right
*
* @param strumlineSize The size of the strumline for the current song. Defaults to 4.
* @return The direction of the note as an index.
*/
public inline function getDirection(strumlineSize:Int = 4):Int
{
return this.data % strumlineSize;
}
/**
* The name of the direction of the note, if applicable.
* Strips the strumline index from the data.
*
* @param strumlineSize The size of the strumline for the current song. Defaults to 4.
* @return The direction of the note as a name.
*/
public function getDirectionName(strumlineSize:Int = 4):String
{
return SongNoteData.buildDirectionName(this.data, strumlineSize);
@@ -1026,6 +1185,9 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
* Strips the direction from the data.
*
* 0 = player, 1 = opponent, etc.
*
* @param strumlineSize The size of the strumline for the current song. Defaults to 4.
* @return The strumline index of the note.
*/
public function getStrumlineIndex(strumlineSize:Int = 4):Int
{
@@ -1043,6 +1205,11 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
return getStrumlineIndex(strumlineSize) == 0;
}
/**
* The position of the note in the song, in steps.
* Calculated from the time and the BPM.
* Cached for performance. Set to `null` to recalculate.
*/
@:jignored
var _stepTime:Null<Float> = null;
@@ -1078,6 +1245,10 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
return _stepLength = Conductor.instance.getTimeInSteps(this.time + this.length) - getStepTime();
}
/**
* Set the length of the hold note, in steps.
* @param value The desired length of the hold note, in steps. Use `0` to make this a tap note.
*/
public function setStepLength(value:Float):Void
{
if (value <= 0)
@@ -1097,16 +1268,24 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
_stepLength = null;
}
/**
* Clone the `params` data for this node, creating a new independent instance.
* @return A new array of cloned `NoteParamData` instances.
*/
public function cloneParams():Array<NoteParamData>
{
var params:Array<NoteParamData> = [];
var newParams:Array<NoteParamData> = [];
for (param in this.params)
{
params.push(param.clone());
newParams.push(param.clone());
}
return params;
return newParams;
}
/**
* Clone this song note data, creating a new independent instance with identical data.
* @return The newly created song note data.
*/
public function clone():SongNoteDataRaw
{
return new SongNoteDataRaw(this.time, this.data, this.length, this.kind, cloneParams());
@@ -1118,6 +1297,10 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
+ (this.kind != '' ? ' [kind: ${this.kind}])' : ')');
}
/**
* Build a tooltip string for this note, as seen when hovering in the Chart Editor.
* @return The tooltip string to display.
*/
public function buildTooltip():String
{
if ((this.kind?.length ?? 0) == 0) return "";
@@ -1164,14 +1347,24 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
}
}
/**
* Whether this note is a hold note.
*/
@:jignored
public var isHoldNote(get, never):Bool;
public function get_isHoldNote():Bool
function get_isHoldNote():Bool
{
return this.length > 0;
}
/**
* Determine if two notes are equal.
* Ex. `note1 == note2`
*
* @param other The other note to compare.
* @return Whether the two notes have equal data.
*/
@:op(A == B)
public function op_equals(other:SongNoteData):Bool
{
+1 -1
View File
@@ -212,7 +212,7 @@ class SongDataUtils
{
var ignoreNullOptionals = true;
var writer = new json2object.JsonWriter<SongClipboardItems>(ignoreNullOptionals);
var dataString:String = writer.write(data, ' ');
var dataString:String = writer.write(data, ' ');
ClipboardUtil.setClipboard(dataString);
+33 -9
View File
@@ -56,7 +56,7 @@ using funkin.data.song.migrator.SongDataMigrator;
// SCRIPTED ENTRIES
//
var scriptedEntryClassNames:Array<String> = getScriptedClassNames();
log('Parsing ${scriptedEntryClassNames.length} scripted entries...');
log(' INFO '.info() + 'Parsing ${scriptedEntryClassNames.length} scripted entries...');
for (entryCls in scriptedEntryClassNames)
{
@@ -99,15 +99,15 @@ using funkin.data.song.migrator.SongDataMigrator;
var entry:Null<Song> = createEntry(entryId);
if (entry != null)
{
trace(' Loaded entry data: ${entry}');
log('Loaded entry data: ${entry}');
entries.set(entry.id, entry);
}
}
catch (e:Dynamic)
{
// Print the error.
trace(' Failed to load entry data: ${entryId}');
trace(e);
log(' ERROR '.error() + 'Failed to load entry data: ${entryId}');
log(' ERROR '.error() + e);
continue;
}
}
@@ -129,6 +129,29 @@ using funkin.data.song.migrator.SongDataMigrator;
return parseEntryMetadataRaw(contents);
}
public override function isScriptedEntry(id:String, ?params:Null<SongEntryParams>)
{
var variation:String = params?.variation ?? Constants.DEFAULT_VARIATION;
if (variation != Constants.DEFAULT_VARIATION)
{
return scriptedSongVariations.exists('${id}:${variation}');
}
return super.isScriptedEntry(id, params);
}
public override function getScriptedEntryClassName(id:String, ?params:Null<SongEntryParams>):Null<String>
{
var variation:String = params?.variation ?? Constants.DEFAULT_VARIATION;
if (variation != Constants.DEFAULT_VARIATION)
{
final variationSongId:ScriptedSong = cast scriptedSongVariations.get('${id}:${variation}');
@:privateAccess
var path:String = variationSongId._asc._c.name;
return path;
}
return super.getScriptedEntryClassName(id, params);
}
/**
* We override `fetchEntry` to handle song variations!
*/
@@ -426,12 +449,13 @@ using funkin.data.song.migrator.SongDataMigrator;
}
}
public function parseEntryChartDataRawWithMigration(contents:String, ?fileName:String = 'raw', version:thx.semver.Version):Null<SongChartData>
public function parseEntryChartDataRawWithMigration(contents:String, ?fileName:String = 'raw', version:thx.semver.Version,
?variation:String):Null<SongChartData>
{
// If a version rule is not specified, do not check against it.
if (SONG_CHART_DATA_VERSION_RULE == null || VersionUtil.validateVersion(version, SONG_CHART_DATA_VERSION_RULE))
{
return parseEntryChartDataRaw(contents, fileName);
return parseEntryChartDataRaw(contents, fileName, variation);
}
else
{
@@ -445,7 +469,7 @@ using funkin.data.song.migrator.SongDataMigrator;
var entryFilePath:String = Paths.json('$dataFilePath/$id/$id-metadata${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}');
if (!openfl.Assets.exists(entryFilePath))
{
trace(' [WARN] Could not locate file $entryFilePath');
trace(' WARNING '.bold().bg_yellow() + ' Could not locate file $entryFilePath');
return null;
}
var rawJson:Null<String> = openfl.Assets.getText(entryFilePath);
@@ -523,7 +547,7 @@ using funkin.data.song.migrator.SongDataMigrator;
if (character == null)
{
trace(' [WARN] Could not locate character $characterId');
trace(' WARNING '.bold().bg_yellow() + ' Could not locate character $characterId');
return allDifficulties;
}
@@ -541,7 +565,7 @@ using funkin.data.song.migrator.SongDataMigrator;
if (allDifficulties.length == 0)
{
trace(' [WARN] No difficulties found. Returning default difficulty list.');
trace(' WARNING '.bold().bg_yellow() + ' No difficulties found. Returning default difficulty list.');
allDifficulties = Constants.DEFAULT_DIFFICULTY_LIST.copy();
}
@@ -22,6 +22,7 @@ class ChartManifestData
* The metadata and chart data file names are derived from this.
*/
public var songId(default, set):String;
public function set_songId(value:String):String
{
return songId = invalidIdRegex.replace(value.trim(), '');
@@ -71,7 +72,7 @@ class ChartManifestData
updateVersionToLatest();
var writer = new json2object.JsonWriter<ChartManifestData>();
return writer.write(this, pretty ? ' ' : null);
return writer.write(this, pretty ? ' ' : null);
}
public function updateVersionToLatest():Void
@@ -0,0 +1,82 @@
package funkin.data.song.importer;
/**
* Structure of a parsed Osu!Mania .osu file
* Stuctured like a INI file format by CSV for HitObjects and more
*/
typedef OsuManiaData =
{
var General:
{
var PreviewTime:Int;
};
var Editor:
{
var DistanceSpacing:Float;
var BeatDivisor:Int;
var GridSize:Int;
};
var Metadata:
{
var Title:String;
var TitleUnicode:String;
var Artist:String;
var ArtistUnicode:String;
var Creator:String;
var Version:String;
};
var Difficulty:
{
var OverallDifficulty:Int;
var SliderMultiplier:Float;
var SliderTickRate:Float;
var CircleSize:Int;
};
var HitObjects:Array<ManiaHitObject>;
var TimingPoints:Array<TimingPoint>;
var Events:Array<Any>;
}
class TimingPoint
{
public var time:Float;
public var beatLength:Float;
public var meter:Int;
public var sampleSet:Int;
public var sampleIndex:Int;
public var volume:Int;
public var uninherited:Int;
public var effects:Int;
public var bpm:Null<Float>;
public var sv:Null<Float>;
public function new(time:Float, beatLength:Float, meter:Int, sampleSet:Int, sampleIndex:Int, volume:Int, uninherited:Int, effects:Int)
{
this.time = time;
this.beatLength = beatLength;
this.meter = meter;
this.sampleSet = sampleSet;
this.sampleIndex = sampleIndex;
this.volume = volume;
this.uninherited = uninherited;
this.effects = effects;
// Derived values
this.bpm = (uninherited == 1) ? (Math.round((60000 / beatLength) * 10) / 10) : null;
this.sv = (uninherited == 0) ? (beatLength / 100) : null; // Just incase someone wants to add Scroll Velocity Support
}
}
class ManiaHitObject
{
public var time:Int;
public var column:Int;
public var holdDuration:Int;
public function new(time:Int, column:Int, holdDuration:Int)
{
this.time = time;
this.column = column;
this.holdDuration = holdDuration;
}
}
@@ -0,0 +1,199 @@
package funkin.data.song.importer;
import funkin.data.song.SongData.SongMetadata;
import funkin.data.song.SongData.SongChartData;
import funkin.data.song.SongData.SongCharacterData;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongTimeChange;
import funkin.data.song.importer.OsuManiaData;
import funkin.data.song.importer.OsuManiaData.TimingPoint;
import funkin.data.song.importer.OsuManiaData.ManiaHitObject;
class OsuManiaImporter
{
public static function parseOsuFile(osuContent:String):OsuManiaData
{
var lines:Array<String> = osuContent.split("\n");
var result:Dynamic = {};
var currentSection:String = null;
var nonCSVLikeSections = ["General", "Editor", "Metadata", "Difficulty"];
for (line in lines)
{
line = StringTools.trim(line);
if (line == "" || StringTools.startsWith(line, "//")) continue;
// Section header like [General]
var sectionRegex = ~/^\[(.+)\]$/;
if (sectionRegex.match(line))
{
currentSection = sectionRegex.matched(1);
if (nonCSVLikeSections.contains(currentSection))
{
Reflect.setField(result, currentSection, {});
}
else
{
Reflect.setField(result, currentSection, []);
}
continue;
}
// Key-value pairs (INI style)
if (currentSection != null && nonCSVLikeSections.contains(currentSection))
{
var parts:Array<String> = line.split(":");
var key:String = StringTools.trim(parts.shift());
var value:String = StringTools.trim(parts.join(":"));
if (Reflect.field(result, currentSection) == null) Reflect.setField(result, currentSection, {});
Reflect.setField(Reflect.field(result, currentSection), key, value);
}
// For CSV-like sections
else if (currentSection != null)
{
var theArray:Array<String> = cast Reflect.field(result, currentSection);
theArray.push(line);
Reflect.setField(result, currentSection, theArray);
}
}
return result;
}
/**
* @param songData The raw parsed JSON data to migrate, as a Dynamic.
* @param difficulty The difficulty name to assign to the migrated chart.
* @return SongMetadata
*/
public static function migrateMetadata(songData:OsuManiaData, difficulty:String = 'normal'):SongMetadata
{
trace('Migrating song metadata from Osu!Mania.');
var songMetadata:SongMetadata = new SongMetadata('Import', songData.Metadata.ArtistUnicode ?? songData.Metadata.Artist ?? Constants.DEFAULT_ARTIST,
songData.Metadata.Creator ?? Constants.DEFAULT_CHARTER, Constants.DEFAULT_VARIATION);
// Set generatedBy string for debugging.
songMetadata.generatedBy = 'Chart Editor Import (Osu!Mania)';
songMetadata.playData.stage = 'mainStage';
songMetadata.songName = songData.Metadata.TitleUnicode ?? songData.Metadata.Title ?? 'Import';
songMetadata.playData.difficulties = [difficulty];
songMetadata.playData.songVariations = [];
songMetadata.timeChanges = rebuildTimeChanges(songData);
songMetadata.playData.characters = new SongCharacterData('bf', 'gf', 'dad');
songMetadata.playData.ratings.set(difficulty, songData.Difficulty.OverallDifficulty ?? 0);
return songMetadata;
}
static function rebuildTimeChanges(songData:OsuManiaData):Array<SongTimeChange>
{
var timings:Array<TimingPoint> = parseTimingPoints(cast songData.TimingPoints);
var bpmPoints:Array<TimingPoint> = timings.filter((tp) -> tp.uninherited == 1);
var result:Array<SongTimeChange> = [];
if (bpmPoints.length >= 1)
{
result.push(new SongTimeChange(0, bpmPoints[0].bpm ?? Constants.DEFAULT_BPM));
for (i in 1...bpmPoints.length)
{
var bpmPoint:TimingPoint = bpmPoints[i];
result.push(new SongTimeChange(bpmPoint.time, bpmPoint.bpm ?? Constants.DEFAULT_BPM));
}
}
if (result.length == 0)
{
result.push(new SongTimeChange(0, Constants.DEFAULT_BPM));
trace("[WARN] No BPM points found, resulting to default BPM...");
}
return result;
}
/**
* @param songData The raw parsed JSON data to migrate, as a Dynamic.
* @param difficulty The difficulty name to assign to the migrated chart.
* @return SongChartData
*/
public static function migrateChartData(songData:OsuManiaData, difficulty:String = 'normal'):SongChartData
{
trace('Migrating song chart data from Osu!Mania.');
// Osu!Mania doesn't have a scroll speed variable as its controlled by the player
var songChartData:SongChartData = new SongChartData([difficulty => Constants.DEFAULT_SCROLLSPEED], [], [difficulty => []]);
// songData.HitObjects is a Array<String> here so im casting it so haxe stops yelling at me
var osuNotes:Array<ManiaHitObject> = parseManiaHitObjects(cast songData.HitObjects, songData.Difficulty.CircleSize);
songChartData.notes.set(difficulty, convertNotes(osuNotes, songData.Difficulty.CircleSize));
songChartData.events = [];
return songChartData;
}
static final STRUMLINE_SIZE = 4;
static function convertNotes(hitObjects:Array<ManiaHitObject>, keyCount:Int):Array<SongNoteData>
{
var result:Array<SongNoteData> = [];
for (hitObject in hitObjects)
{
var wrappedColumn:Int = hitObject.column % (keyCount * 2); // wrap overflow for 9K+
if (keyCount <= 4) // if its 4K or less, flip to BF side
{
wrappedColumn -= 4;
var noteOffset:Int = Std.int(Math.abs(keyCount - STRUMLINE_SIZE));
var flippedNoteData:Int = wrappedColumn + keyCount + noteOffset;
result.push(new SongNoteData(hitObject.time, flippedNoteData, hitObject.holdDuration ?? 0, ''));
}
else
result.push(new SongNoteData(hitObject.time, wrappedColumn, hitObject.holdDuration ?? 0, ''));
}
return result;
}
static function parseTimingPoints(timingLines:Array<String>):Array<TimingPoint>
{
return timingLines.map(function(line:String):TimingPoint {
var parts = line.split(",");
var time = Std.parseFloat(parts[0]);
var beatLength = Std.parseFloat(parts[1]);
var meter = Std.parseInt(parts[2]);
var sampleSet = Std.parseInt(parts[3]);
var sampleIndex = Std.parseInt(parts[4]);
var volume = Std.parseInt(parts[5]);
var uninherited = Std.parseInt(parts[6]);
var effects = Std.parseInt(parts[7]);
return new TimingPoint(time, beatLength, meter, sampleSet, sampleIndex, volume, uninherited, effects);
});
}
static function parseManiaHitObjects(hitObjectsLines:Array<String>, ?columns:Int = 4):Array<ManiaHitObject>
{
return hitObjectsLines.map(function(line:String):ManiaHitObject {
var parts = line.split(",");
var x:Int = Std.parseInt(parts[0]);
var time:Int = Std.parseInt(parts[2]);
var type:Int = Std.parseInt(parts[3]);
var hasHold:Bool = (type & 128) == 128;
var noteD:Int = Std.int(x / (512 / columns));
var holdEndTime:Null<Int> = hasHold ? Std.parseInt(parts[5].split(":")[0]) : null;
var holdDuration:Int = (holdEndTime != null) ? (holdEndTime - time) : 0;
return new ManiaHitObject(time, noteD, holdDuration);
});
}
}
@@ -0,0 +1,123 @@
package funkin.data.song.importer;
typedef StepManiaData =
{
var Metadata:
{
var Title:String;
var Artist:String;
var Genre:String;
var Credit:String;
var Banner:String;
var Background:String;
var Offset:Float;
var SampleStart:Float;
};
var TimingPoints:Array<StepTimingPoint>;
var Stops:Array<StepStop>;
var Difficulties:Array<StepDifficulty>;
}
enum StepManiaChartType {
DanceSingle;
DanceDouble;
Unknown;
}
enum StepManiaNoteType {
Tap;
Head;
Tail;
Roll;
Mine;
Fake;
}
class StepNote
{
public var beat:Float;
public var column:Int;
public var type:StepManiaNoteType;
public function new(t:String, beat:Float, column:Int)
{
this.beat = beat;
this.column = column;
switch (t) {
case "2":
this.type = StepManiaNoteType.Head;
case "3":
this.type = StepManiaNoteType.Tail;
case "4":
this.type = StepManiaNoteType.Roll;
case "M":
this.type = StepManiaNoteType.Mine;
case "F":
this.type = StepManiaNoteType.Fake;
default:
this.type = StepManiaNoteType.Tap;
}
}
}
class StepDifficulty
{
public var name:String;
public var charter:String;
public var difficultyRating:Int;
public var type:StepManiaChartType;
public var notes:Array<StepNote>;
public function parseChartType(chartTypeStr:String):StepManiaChartType
{
switch (chartTypeStr) {
case "dance-single":
return StepManiaChartType.DanceSingle;
case "dance-double":
return StepManiaChartType.DanceDouble;
default:
return StepManiaChartType.Unknown;
}
}
public function new(name:String, charter:String, difficultyRating:Int, type:String)
{
this.name = name;
this.charter = charter;
this.difficultyRating = difficultyRating;
this.type = parseChartType(type);
}
}
class StepTimingPoint
{
public var bpm:Float;
public var startBeat:Float = Math.NEGATIVE_INFINITY;
public var endBeat:Float = Math.POSITIVE_INFINITY;
public var startTimestamp:Float = 0;
public var endTimestamp:Float = 0;
public function new(bpm:Float, startBeat:Float)
{
this.bpm = bpm;
this.startBeat = startBeat;
}
}
// Not implemented, but if any chart uses them then the chart will break.
// IE this messes with the timing of the notes.
class StepStop
{
public var startBeat:Float = Math.NEGATIVE_INFINITY;
public var duration:Float = 0;
public var startTimestamp:Float = Math.NEGATIVE_INFINITY;
public function new(startBeat:Float, duration:Float)
{
this.startBeat = startBeat;
this.duration = duration;
}
}
@@ -0,0 +1,613 @@
package funkin.data.song.importer;
import funkin.data.song.SongData.SongMetadata;
import funkin.data.song.SongData.SongChartData;
import funkin.data.song.SongData.SongCharacterData;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongTimeChange;
import funkin.data.song.importer.StepManiaData.StepTimingPoint;
import funkin.data.song.importer.StepManiaData.StepDifficulty;
import funkin.data.song.importer.StepManiaData.StepManiaChartType;
import funkin.data.song.importer.StepManiaData.StepNote;
import funkin.data.song.importer.StepManiaData.StepStop;
import funkin.data.song.importer.StepManiaData.StepManiaNoteType;
enum StepStateEnum
{
Metadata;
TimingPoints;
Stops;
Notes;
}
class StepManiaImporter
{
private static var readDiffMetadata:Map<String, String> = new Map<String, String>();
static function parseMetadataLine(line:String, result:StepManiaData):StepManiaData
{
var parts:Array<String> = line.split(":");
if (parts.length != 2) return result;
var key:String = StringTools.trim(parts[0]);
var value:String = StringTools.trim(parts[1]);
// remove trailing ; on value
if (StringTools.endsWith(value, ";")) value = value.substr(0, value.length - 1);
switch (key)
{
case "TITLE":
result.Metadata.Title = value;
case "ARTIST":
result.Metadata.Artist = value;
case "GENRE":
result.Metadata.Genre = value;
case "CREDIT":
if (result.Metadata.Credit == "") result.Metadata.Credit = value;
else // .ssc
readDiffMetadata.set("CREDIT", value);
case "BANNER":
result.Metadata.Banner = value;
case "BACKGROUND":
result.Metadata.Background = value;
case "OFFSET":
result.Metadata.Offset = Std.parseFloat(value);
case "SAMPLESTART":
result.Metadata.SampleStart = Std.parseFloat(value);
// .ssc
case "STEPSTYPE":
readDiffMetadata.set("STEPSTYPE", value);
case "DESCRIPTION":
readDiffMetadata.set("DESCRIPTION", value);
case "DIFFICULTY":
readDiffMetadata.set("DIFFICULTY", value);
}
return result;
}
static function parseTimingPointLine(line:String):Array<StepTimingPoint>
{
// Remove #BPMS: prefix if present
if (StringTools.startsWith(line, "#BPMS:")) line = line.substr(6);
var parts:Array<String> = line.split(",");
var stepTimingPoints:Array<StepTimingPoint> = [];
for (i in 0...parts.length)
{
// Split the =
var split = parts[i].split("=");
if (split.length != 2) continue;
var beat:Float = Std.parseFloat(StringTools.trim(split[0]));
// Check if split[i + 1] has ; and remove it
var bpmSplit = split[1];
if (StringTools.endsWith(bpmSplit, ";")) bpmSplit = bpmSplit.substr(0, bpmSplit.length - 1);
var tp:StepTimingPoint = new StepTimingPoint(Std.parseFloat(bpmSplit), beat);
stepTimingPoints.push(tp);
}
return stepTimingPoints;
}
static function parseStopsLine(line:String):Array<StepStop>
{
// Remove #STOPS: prefix if present
if (StringTools.startsWith(line, "#STOPS:")) line = line.substr(7);
// Same as timing points
var parts:Array<String> = line.split(",");
var stepStopPoints:Array<StepStop> = [];
for (i in 0...parts.length)
{
// Split the =
var split = parts[i].split("=");
if (split.length != 2) continue;
var beat:Float = Std.parseFloat(StringTools.trim(split[0]));
// Check if split[i + 1] has ; and remove it
var durSplit = split[1];
if (StringTools.endsWith(durSplit, ";")) durSplit = durSplit.substr(0, durSplit.length - 1);
var tp:StepStop = new StepStop(beat, Std.parseFloat(durSplit));
stepStopPoints.push(tp);
}
return stepStopPoints;
}
static function parseMeasure(lines:Array<String>, measureIndex:Int):Array<StepNote>
{
var lengthInRows:Float = 192.0 / lines.length;
var rowIndex:Int = 0;
var beat:Float = 0;
var stepNotes:Array<StepNote> = [];
for (i in 0...lines.length)
{
var stepNoteRow:Float = measureIndex * 192 + (lengthInRows * rowIndex);
beat = stepNoteRow / 48.0; // 48 rows per beat
for (j in 0...lines[i].length)
{
var char:String = lines[i].charAt(j);
if (char != "0")
{
var stepNote:StepNote = new StepNote(char, beat, j);
stepNotes.push(stepNote);
}
}
rowIndex++;
}
return stepNotes;
}
static function synchronizeStepTimingPoints(stepStopPoints:Array<StepStop>, stepTimingPoints:Array<StepTimingPoint>):Array<StepTimingPoint>
{
// Initialize startTimestamp/endTimestamp and endBeat for timing points
for (tpIndex in 0...stepTimingPoints.length)
{
var tp = stepTimingPoints[tpIndex];
// ensure fields exist
if (tp.startTimestamp == Math.NEGATIVE_INFINITY) tp.startTimestamp = 0.0;
}
var tpIndex:Int = 1;
var spIndex:Int = 0;
// if we have more than one timing point or any StepStops, synchronize
if (stepTimingPoints.length > 1 || stepStopPoints.length > 0)
{
if (stepStopPoints.length > 1)
{
while (tpIndex < stepTimingPoints.length || spIndex < stepStopPoints.length)
{
var prevTp:StepTimingPoint = stepTimingPoints[tpIndex - 1];
if (tpIndex == stepTimingPoints.length)
{
var cts:Float = 0;
while (spIndex < stepStopPoints.length)
{
var sp = stepStopPoints[spIndex];
sp.startTimestamp = (prevTp.startTimestamp + (sp.startBeat - prevTp.startBeat) / (prevTp.bpm / 60)) + cts;
cts += sp.duration;
spIndex++;
}
continue;
}
if (spIndex == stepStopPoints.length)
{
while (tpIndex < stepTimingPoints.length)
{
var tp = stepTimingPoints[tpIndex];
prevTp = stepTimingPoints[tpIndex - 1];
prevTp.endBeat = tp.startBeat;
prevTp.endTimestamp += (prevTp.endBeat - prevTp.startBeat) / (prevTp.bpm / 60);
tp.startTimestamp = prevTp.endTimestamp;
tp.endTimestamp = tp.startTimestamp;
tpIndex++;
if (tpIndex == stepTimingPoints.length)
{
tp.endTimestamp = Math.POSITIVE_INFINITY;
tp.endBeat = Math.POSITIVE_INFINITY;
}
}
continue;
}
var tp:StepTimingPoint = stepTimingPoints[tpIndex];
var sp:StepStop = stepStopPoints[spIndex];
prevTp.endBeat = tp.startBeat;
if (sp.startBeat < prevTp.endBeat)
{
sp.startTimestamp = prevTp.endTimestamp + (sp.startBeat - prevTp.startBeat) / (prevTp.bpm / 60);
prevTp.endTimestamp += sp.duration;
spIndex++;
}
else
{
prevTp.endTimestamp += (prevTp.endBeat - prevTp.startBeat) / (prevTp.bpm / 60);
tp.startTimestamp = prevTp.endTimestamp;
tp.endTimestamp = tp.startTimestamp;
tpIndex++;
}
}
}
else
{
while (tpIndex < stepTimingPoints.length)
{
var prevTp:StepTimingPoint = stepTimingPoints[tpIndex - 1];
var tp:StepTimingPoint = stepTimingPoints[tpIndex];
prevTp.endBeat = tp.startBeat;
prevTp.endTimestamp = prevTp.startTimestamp + (prevTp.endBeat - prevTp.startBeat) / (prevTp.bpm / 60);
tp.startTimestamp = prevTp.endTimestamp;
tpIndex++;
if (tpIndex == stepTimingPoints.length)
{
tp.endTimestamp = Math.POSITIVE_INFINITY;
tp.endBeat = Math.POSITIVE_INFINITY;
}
}
}
}
return stepTimingPoints;
}
static function pushWorking(workingDiff:StepDifficulty, result:StepManiaData):StepManiaData
{
if (workingDiff != null && workingDiff.notes.length > 0)
{
// Apply .ssc metadata if any
for (metaKey in readDiffMetadata.keys())
{
var metaValue = readDiffMetadata.get(metaKey);
switch (metaKey)
{
case "STEPSTYPE":
workingDiff.type = workingDiff.parseChartType(metaValue);
case "DIFFICULTY":
workingDiff.name = metaValue;
case "CREDIT":
workingDiff.charter = metaValue;
// DESCRIPTION is ignored for now
}
}
result.Difficulties.push(workingDiff);
}
readDiffMetadata.clear();
return result;
}
static function parseBPMS(line:String, result:StepManiaData):StepManiaData
{
var tps = parseTimingPointLine(line);
if (tps != null)
{
for (tp in tps)
result.TimingPoints.push(tp);
}
return result;
}
static function parseStops(line:String, result:StepManiaData):StepManiaData
{
var sps = parseStopsLine(line);
if (sps != null)
{
for (sp in sps)
result.Stops.push(sp);
}
return result;
}
/**
* Parses a StepMania file content into StepManiaData structure.
* @param stepContent The content of the StepMania file as a string.
* @return StepManiaData The parsed StepMania data.
*/
public static function parseStepManiaFile(stepContent:String):StepManiaData
{
readDiffMetadata = new Map<String, String>();
var lines:Array<String> = stepContent.split("\n");
var currentMeasure:Int = 0;
var measure:Array<String> = [];
var workingDiff:StepDifficulty = null;
var state:StepStateEnum = StepStateEnum.Metadata;
var headerLines:Int = 0;
var result:StepManiaData = {
Metadata: {
Title: "",
Artist: "",
Genre: "",
Credit: "",
Banner: "",
Background: "",
Offset: 0,
SampleStart: 0
},
TimingPoints: [],
Stops: [],
Difficulties: []
};
// Parsing metadata
for (line in lines)
{
line = StringTools.trim(line);
if (line == "") continue;
if (StringTools.startsWith(line, "//")) continue; // Comment line
switch (state)
{
case StepStateEnum.Metadata:
if (StringTools.startsWith(line, "#BPMS:"))
{
state = StepStateEnum.TimingPoints;
result = parseBPMS(line, result);
if (StringTools.endsWith(line, ";")) state = StepStateEnum.Metadata;
}
else if (StringTools.startsWith(line, "#STOPS:"))
{
state = StepStateEnum.Stops;
result = parseStops(line, result);
if (StringTools.endsWith(line, ";")) state = StepStateEnum.Metadata;
}
else if (StringTools.startsWith(line, "#NOTES:"))
{
if (workingDiff != null)
{
// Save previous difficulty
result = pushWorking(workingDiff, result);
}
// Start new difficulty
workingDiff = new StepDifficulty("", "", 0, "");
workingDiff.notes = [];
headerLines = 0;
currentMeasure = 0;
measure = [];
state = StepStateEnum.Notes;
}
else if (StringTools.startsWith(line, "#")) result = parseMetadataLine(line.substr(1), result);
case StepStateEnum.TimingPoints:
if (line == ";") state = StepStateEnum.Metadata;
else
{
result = parseBPMS(line, result);
if (StringTools.endsWith(line, ";")) state = StepStateEnum.Metadata;
}
case StepStateEnum.Stops:
if (line == ";") state = StepStateEnum.Metadata;
else
{
result = parseStops(line, result);
if (StringTools.endsWith(line, ";")) state = StepStateEnum.Metadata;
}
case StepStateEnum.Notes:
if (line == "#NOTES:") continue;
// remove comments in line (if any) ie, "0000 // some comment"
var commentIndex = line.indexOf("//");
if (commentIndex != -1) line = StringTools.trim(line.substr(0, commentIndex));
if (StringTools.contains(line, ":")) // header
{
switch (headerLines)
{
case 0:
// First line is chart type
var chartTypeStr = StringTools.trim(line).replace(":", "");
workingDiff.type = workingDiff.parseChartType(chartTypeStr);
case 1:
// Second line is charter
workingDiff.charter = StringTools.trim(line).replace(":", "");
case 2:
// Third line is difficulty name
workingDiff.name = StringTools.trim(line).replace(":", "");
case 3:
// Fourth line is difficulty rating
workingDiff.difficultyRating = Std.parseInt(StringTools.trim(line).replace(":", ""));
// we dont care about the rest lol
}
headerLines++;
continue;
}
// we're reading StepNote data now
// start gathering measures until we hit a ,
if (line == "," || line == ";")
{
// end of measure
var stepNotesInMeasure = parseMeasure(measure, currentMeasure);
for (stepNote in stepNotesInMeasure)
workingDiff.notes.push(stepNote);
measure = [];
currentMeasure++;
// end of StepNotes section
if (line == ";") state = StepStateEnum.Metadata;
}
else
{
measure.push(line);
}
}
}
if (workingDiff != null && workingDiff.notes.length > 0)
{
// Save last difficulty
result = pushWorking(workingDiff, result);
}
// Syncronize timing points with StepStops
result.TimingPoints = synchronizeStepTimingPoints(result.Stops, result.TimingPoints);
return result;
}
static function getStepStopAtBeat(beat:Float, stepStops:Array<StepStop>):StepStop
{
var stepStop:StepStop = null;
for (s in stepStops)
{
if (s.startBeat < beat) stepStop = s;
}
return stepStop;
}
static function beatToTime(beat:Float, offset:Float, stepTimingPoints:Array<StepTimingPoint>, stepStops:Array<StepStop>):Float
{
var time:Float = 0;
for (tp in stepTimingPoints)
{
if (tp.startBeat <= beat && tp.endBeat > beat)
{
var stepStop:StepStop = getStepStopAtBeat(beat, stepStops);
var b:Float = tp.startBeat;
var startTime:Float = tp.startTimestamp;
if (stepStop != null)
{
if (stepStop.startBeat > tp.startBeat)
{
b = stepStop.startBeat;
startTime = stepStop.startTimestamp + stepStop.duration;
}
}
var nb:Float = (beat - b) / (tp.bpm / 60);
time = startTime + nb;
}
}
return (time * 1000) - (offset * 1000); // convert to ms
}
static function convertStepNotes(offset:Float, type:StepManiaChartType, stepNotes:Array<StepNote>,
stepTimingPoints:Array<StepTimingPoint>, stepStops:Array<StepStop>):Array<SongNoteData>
{
var result:Array<SongNoteData> = [];
var holdArray:Array<Float> = [];
if (type == StepManiaChartType.DanceSingle)
{
holdArray = [-1, -1, -1, -1];
}
else if (type == StepManiaChartType.DanceDouble)
{
holdArray = [-1, -1, -1, -1, -1, -1, -1, -1];
}
else
{
trace("[WARN] Unknown StepMania chart type when converting notes.");
return result;
}
for (stepNote in stepNotes)
{
var time = beatToTime(stepNote.beat, offset, stepTimingPoints, stepStops);
if (stepNote.type == StepManiaNoteType.Head || stepNote.type == StepManiaNoteType.Roll)
{
holdArray[stepNote.column] = time;
continue;
}
var snd:SongNoteData = new SongNoteData(time, stepNote.column, 0);
if (stepNote.type == StepManiaNoteType.Mine) snd.kind = "mine";
else if (stepNote.type == StepManiaNoteType.Fake) snd.kind = "fake";
if (stepNote.type == StepManiaNoteType.Tail)
{
var length:Float = 0;
if (holdArray[stepNote.column] != -1) length = time - holdArray[stepNote.column];
snd.time = holdArray[stepNote.column];
snd.length = length;
holdArray[stepNote.column] = -1;
}
if (type != StepManiaChartType.DanceSingle)
{
if (stepNote.column < 4) snd.data = stepNote.column + 4;
else
snd.data = stepNote.column - 4;
}
else
{
if (snd.data >= 4) snd.data -= 4;
}
result.push(snd);
}
return result;
}
/**
* Migrates StepManiaData to SongMetadata.
* @param songData The StepManiaData to migrate.
* @return SongMetadata The migrated SongMetadata.
*/
public static function migrateChartMetadata(songData:StepManiaData):SongMetadata
{
var metadata:SongMetadata = new SongMetadata(songData.Metadata.Title, songData.Metadata.Artist);
metadata.playData.stage = 'mainStage';
metadata.playData.characters = new SongCharacterData('bf', 'gf', 'dad');
metadata.generatedBy = 'Chart Editor Import (StepMania)';
metadata.playData.songVariations = [];
// Difficulties
var difficulties:Array<String> = [];
for (diff in songData.Difficulties)
{
if (diff.type == StepManiaChartType.Unknown)
{
trace("[WARN] Skipping unknown StepMania chart type. Name: " + diff.name);
continue; // skip unknown chart types
}
difficulties.push(diff.name);
metadata.playData.ratings.set(diff.name, diff.difficultyRating);
}
metadata.playData.difficulties = difficulties;
metadata.charter = songData.Metadata.Credit != "" ? songData.Metadata.Credit : null;
// TimeChanges
metadata.timeChanges = [];
for (tp in songData.TimingPoints)
{
var timeChange = new SongTimeChange(tp.startTimestamp * 1000, tp.bpm);
timeChange.beatTime = tp.startBeat;
metadata.timeChanges.push(timeChange);
}
return metadata;
}
/**
* Migrates StepManiaData to SongChartData.
* @param songData The StepManiaData to migrate.
* @return SongChartData The migrated SongChartData.
*/
public static function migrateChartData(songData:StepManiaData):SongChartData {
var scrollsMap:Map<String, Float> = new Map<String, Float>();
var stepNoteMap:Map<String, Array<SongNoteData>> = new Map<String, Array<SongNoteData>>();
for (diff in songData.Difficulties)
{
if (diff.type == StepManiaChartType.Unknown)
{
trace("[WARN] Skipping unknown StepMania chart type. Name: " + diff.name);
continue; // skip unknown chart types
}
scrollsMap.set(diff.name, Constants.DEFAULT_SCROLLSPEED);
stepNoteMap.set(diff.name, convertStepNotes(songData.Metadata.Offset, diff.type, diff.notes, songData.TimingPoints, songData.Stops));
}
var songChartData:SongChartData = new SongChartData(scrollsMap, [], stepNoteMap);
return songChartData;
}
}
+59 -2
View File
@@ -66,7 +66,7 @@ class StageData
updateVersionToLatest();
var writer = new json2object.JsonWriter<StageData>();
return writer.write(this, pretty ? ' ' : null);
return writer.write(this, pretty ? ' ' : null);
}
public function updateVersionToLatest():Void
@@ -196,7 +196,7 @@ typedef StageDataProp =
/**
* The animation type to use.
* Options: "sparrow", "packer"
* Options: "sparrow", "packer", "animateatlas"
* @default "sparrow"
*/
@:default("sparrow")
@@ -228,6 +228,63 @@ typedef StageDataProp =
@:default("#FFFFFF")
@:optional
var color:String;
/**
* Various settings for the prop.
* Only available for texture atlases.
*/
@:optional
var atlasSettings:TextureAtlasData;
};
typedef TextureAtlasData =
{
/**
* If true, the texture atlas will behave as if it was exported as an SWF file.
* Notably, this allows MovieClip symbols to play.
*/
@:optional
var swfMode:Bool;
/**
* If true, filters and masks will be cached when the atlas is loaded, instead of during runtime.
*/
@:optional
var cacheOnLoad:Bool;
/**
* The filter quality.
* Available values are: HIGH, MEDIUM, LOW, and RUDY.
*
* If you're making an atlas sprite in HScript, you pass an Int instead:
*
* HIGH - 0
* MEDIUM - 1
* LOW - 2
* RUDY - 3
*/
@:optional
var filterQuality:Int;
/**
* Whether to apply the stage matrix, if it was exported from a symbol instance.
* Also positions the Texture Atlas as it displays in Animate.
* Turning this on is only recommended if you prepositioned the character in Animate.
* For other cases, it should be turned off to act similarly to a normal FlxSprite.
*/
@:optional
var applyStageMatrix:Bool;
/**
* If enabled, the sprite will render as one texture instead of rendering multiple limbs.
* This is useful for stuff like changing alpha, and shaders that require the whole sprite.
*
* Only enable this if your sprite either:
* - Changes alpha to something other than 1.0
* - Has a shader or blend mode
*/
@:optional
var useRenderTexture:Bool;
};
typedef StageDataCharacter =
@@ -114,4 +114,19 @@ typedef LevelPropData =
@:default([])
@:optional
var animations:Array<AnimationData>;
/**
* If animations are used, this is the name of the animation to play first.
* @default idle
*/
@:default('')
@:optional
var startingAnimation:String;
/**
* Flips the sprite on X axis.
*/
@:default(false)
@:optional
var flipX:Null<Bool>;
}
@@ -40,7 +40,8 @@ class LevelRegistry extends BaseRegistry<Level, LevelData, LevelEntryParams> imp
'week5',
'week6',
'week7',
'weekend1'
'weekend1',
'sserafim'
];
}
+51 -27
View File
@@ -3,26 +3,18 @@ package funkin.external.android;
#if android
import lime.system.JNI;
import flixel.util.FlxSignal;
import haxe.ds.Map;
/**
* A Utility class to handle Android API level callbacks and events.
*/
@:unreflective
class CallbackUtil
class CallbackUtil #if (lime >= "8.0.0") implements JNISafety #end
{
/**
* The result code for `DATA_FOLDER_CLOSED` activity.
*/
public static var DATA_FOLDER_CLOSED(get, never):Int;
@:noCompletion
static function get_DATA_FOLDER_CLOSED():Int
{
final field:Null<Dynamic> = JNIUtil.createStaticField('funkin/extensions/CallbackUtil', 'DATA_FOLDER_CLOSED', 'I');
return field != null ? field.get() : 0;
}
/**
* Signal triggered when an activity result is received.
*
@@ -30,6 +22,13 @@ class CallbackUtil
*/
public static var onActivityResult:FlxTypedSignal<Int->Int->Void> = new FlxTypedSignal<Int->Int->Void>();
/**
* Signal triggered when the user opens a FNFC file with the game in runtime.
*
* First argument is the FNFC file path.
*/
public static var onFNFCOpen:FlxTypedSignal<String->Void> = new FlxTypedSignal<String->Void>();
/**
* Initializes the callback utility.
*/
@@ -39,32 +38,57 @@ class CallbackUtil
if (initCallBackJNI != null)
{
initCallBackJNI(new CallbackHandler());
initCallBackJNI(new CallbackUtil());
}
}
}
/**
* Internal class to handle native callback events.
*/
class CallbackHandler #if (lime >= "8.0.0") implements JNISafety #end
{
@:allow(funkin.external.android.CallbackUtil)
function new():Void {}
@:noCompletion
static function get_DATA_FOLDER_CLOSED():Int
{
final field:Null<Dynamic> = JNIUtil.createStaticField('funkin/extensions/CallbackUtil', 'DATA_FOLDER_CLOSED', 'I');
/**
* Handles the activity result callback from native code.
*
* @param requestCode The request code of the acitivty.
* @param resultCode The result code of the acitivty.
*/
return field != null ? field.get() : 0;
}
@:noCompletion
private static var __staticFields:Array<Dynamic> = null;
@:noCompletion
public static var __callbacksFields:Map<String, Dynamic> = new Map<String, Dynamic>();
@:noCompletion
private static function listStaticFields():Array<Dynamic>
{
if (__staticFields != null) return __staticFields;
__staticFields = Type.getClassFields(CallbackUtil);
__staticFields = __staticFields.filter((field) -> Std.isOfType(Reflect.field(CallbackUtil, field), IFlxSignal));
return __staticFields;
}
@:noCompletion
private function new() {}
@:noCompletion
@:keep
#if (lime >= "8.0.0")
@:runOnMainThread
#end
public function onActivityResult(requestCode:Int, resultCode:Int):Void
private function dispatchCallback(callbackName:String, arguments:Array<Dynamic>)
{
if (CallbackUtil.onActivityResult != null) CallbackUtil.onActivityResult.dispatch(requestCode, resultCode);
if (listStaticFields().contains(callbackName))
{
final field = Reflect.field(CallbackUtil, callbackName);
if (!__callbacksFields.exists(callbackName))
{
__callbacksFields.set(callbackName, Reflect.field(field, "dispatch"));
}
Reflect.callMethod(field, __callbacksFields.get(callbackName), arguments);
}
else
{
trace('[CALLBACK] Callback "${callbackName}" not found.');
}
}
}
#end
-1
View File
@@ -4,7 +4,6 @@ package funkin.external.android;
/**
* A Utility class to manage the Application's Data folder on Android.
*/
@:unreflective
class DataFolderUtil
{
/**
+26
View File
@@ -0,0 +1,26 @@
package funkin.external.android;
#if android
import lime.system.JNI;
/**
* Utility class for keyboard detection.
*/
class KeyboardUtil
{
/**
* Returns `true` if a keyboard is currently connected to the device.
*/
public static var keyboardConnected(get, never):Bool;
@:noCompletion
static function get_keyboardConnected():Bool
{
final method:Null<Dynamic> = JNIUtil.createStaticMethod('funkin/util/KeyboardUtil', 'isKeyboardConnected', '()Z');
if (method == null) return false;
return inline JNI.callStatic(method, []);
}
}
#end
-1
View File
@@ -7,7 +7,6 @@ import lime.system.JNI;
/**
* A Utility class to get Android screen related informations.
*/
@:unreflective
class ScreenUtil
{
/**
@@ -0,0 +1,37 @@
package funkin.extensions;
import android.content.Context;
import android.media.AudioManager;
import android.os.Bundle;
import org.haxe.extension.Extension;
public class AudioSession extends Extension {
private AudioManager audioManager;
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
audioManager = (AudioManager) Extension.mainContext.getSystemService(Context.AUDIO_SERVICE);
requestAudioFocus();
}
@Override
public void onPause() {
super.onPause();
if (audioManager != null) {
audioManager.abandonAudioFocus(null);
}
}
@Override
public void onResume() {
super.onResume();
requestAudioFocus();
}
private void requestAudioFocus() {
if (audioManager != null) {
audioManager.requestAudioFocus(null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
}
}
}
@@ -25,11 +25,80 @@ public class CallbackUtil extends Extension
CallbackUtil.haxeObject = haxeObject;
}
public static void callMethod(String methodName)
{
if (haxeObject != null)
{
Object[] args = new Object[2];
args[0] = methodName;
args[1] = new Object[0];
haxeObject.call("dispatchCallback", args);
}
}
public static void callMethod(String methodName, Object arg1)
{
if (haxeObject != null)
{
Object[] _args = {arg1};
Object[] args = new Object[2];
args[0] = methodName;
args[1] = _args;
haxeObject.call("dispatchCallback", args);
}
}
public static void callMethod(String methodName, Object arg1, Object arg2)
{
if (haxeObject != null)
{
Object[] _args = {arg1, arg2};
Object[] args = new Object[2];
args[0] = methodName;
args[1] = _args;
haxeObject.call("dispatchCallback", args);
}
}
public static void callMethod(String methodName, Object arg1, Object arg2, Object arg3)
{
if (haxeObject != null)
{
Object[] _args = {arg1, arg2, arg3};
Object[] args = new Object[2];
args[0] = methodName;
args[1] = _args;
haxeObject.call("dispatchCallback", args);
}
}
public static void callMethod(String methodName, Object arg1, Object arg2, Object arg3, Object arg4)
{
if (haxeObject != null)
{
Object[] _args = {arg1, arg2, arg3, arg4};
Object[] args = new Object[2];
args[0] = methodName;
args[1] = _args;
haxeObject.call("dispatchCallback", args);
}
}
public static void callMethod(String methodName, Object[] args)
{
if (haxeObject != null)
{
Object[] _args = new Object[2];
args[0] = methodName;
args[1] = args;
haxeObject.call("dispatchCallback", _args);
}
}
@Override
public boolean onActivityResult(int requestCode, int resultCode, Intent data)
{
if (haxeObject != null)
haxeObject.call2("onActivityResult", requestCode, resultCode);
callMethod("onActivityResult", requestCode, resultCode);
return true;
}
@@ -0,0 +1,129 @@
package funkin.extensions;
import android.content.Intent;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.os.Bundle;
import android.util.Log;
import funkin.extensions.CallbackUtil;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.haxe.extension.Extension;
import org.haxe.lime.HaxeObject;
public class FNFCExtension extends Extension
{
public static final String LOG_TAG = "FNFCExtension";
public static String lastFNFC = null;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = mainActivity.getIntent();
if (intent != null && intent.getData() != null) {
handleIntent(intent, false);
}
}
@Override
public void onNewIntent(Intent intent) {
super.onNewIntent(intent);
if (intent != null && intent.getData() != null) {
handleIntent(intent, true);
}
}
private static void handleIntent(Intent intent, boolean doCallback)
{
if (Intent.ACTION_VIEW.equals(intent.getAction()))
{
Uri uri = intent.getData();
if (uri != null)
{
try
{
lastFNFC = copyFNFCToCache(uri);
if (doCallback)
{
CallbackUtil.callMethod("onFNFCOpen", lastFNFC);
}
}
catch (IOException e)
{
Log.e(LOG_TAG, e.getMessage());
}
}
}
}
public static String copyFNFCToCache(Uri uri) throws IOException
{
if (uri != null)
{
String fileName = new File(uri.getPath()).getName();
// Clean some Uri leftovers
if (fileName.contains(":"))
fileName = fileName.split(":")[1];
File cacheFNFC = new File(mainContext.getCacheDir(), "fnfc");
File output = new File(cacheFNFC, fileName);
if (!cacheFNFC.exists())
cacheFNFC.mkdir();
if (output.exists())
output.delete();
ParcelFileDescriptor parcelFileDescriptor = null;
FileInputStream fileInputStream = null;
OutputStream out = null;
try
{
parcelFileDescriptor = mainContext.getContentResolver().openFileDescriptor(uri, "r");
fileInputStream = new FileInputStream(parcelFileDescriptor.getFileDescriptor());
byte[] fileBytes = new byte[(int) parcelFileDescriptor.getStatSize()];
fileInputStream.read(fileBytes);
out = new FileOutputStream(output);
out.write(fileBytes);
if (output.exists())
output.deleteOnExit(); // makes the file get deleted when the app closes ig?
}
catch (IOException e)
{
Log.e(LOG_TAG, e.getMessage());
}
finally
{
if (fileInputStream != null)
fileInputStream.close();
if (parcelFileDescriptor != null)
parcelFileDescriptor.close();
if (out != null)
out.close();
}
return output.getAbsolutePath();
}
return null;
}
}
@@ -0,0 +1,14 @@
package funkin.util;
import org.haxe.extension.Extension;
public class KeyboardUtil
{
public static boolean isKeyboardConnected()
{
if (Extension.mainContext == null) return false;
// KEYBOARD_UNDEFINED = 0, KEYBOARD_NOKEYS = 1
return Extension.mainContext.getResources().getConfiguration().keyboard > 1;
}
}
+31
View File
@@ -0,0 +1,31 @@
package funkin.external.apple;
#if ((ios || macos) && cpp)
/**
* A utility class to manage the audio session on apple devices.
*/
@:build(funkin.util.macro.LinkerMacro.xml('project/Build.xml'))
@:include('AudioSession.hpp')
@:unreflective
extern class AudioSession
{
/**
* Initializes the audio session for playback.
*
* This function configures the shared AVAudioSession instance with the following settings:
* - Sets the audio session category to Playback, allowing Bluetooth A2DP and mixing with spoken audio interruptions.
* - For iOS 17.0 and above, disables interruption on route disconnect.
* - For iOS 14.5 and above, prefers no interruptions from system alerts.
*/
@:native('Apple_AudioSession_Initialize')
static function initialize():Void;
/**
* Sets the active state of the shared AVAudioSession.
*
* @param active Whether to activate or deactivate the audio session.
*/
@:native('Apple_AudioSession_SetActive')
static function setActive(active:Bool):Void;
}
#end
+21
View File
@@ -0,0 +1,21 @@
package funkin.external.apple;
#if ((ios || macos) && cpp)
/**
* A utility class for FNFC loading on apple devices.
*/
@:build(funkin.util.macro.LinkerMacro.xml('project/Build.xml'))
@:include('FNFCUtil.hpp')
@:unreflective
extern class FNFCUtil
{
/**
* Copies the FNFC resource from the specified URL into the cache.
*
* @param url The URL of the FNFC resource to copy into the cache.
* @param callback A function to be called when the copy operation completes.
*/
@:native('Apple_FNFCUtil_CopyFNFCIntoCache')
static function copyFNFCIntoCache(url:cpp.ConstCharStar, callback:cpp.Callable<(event:cpp.ConstCharStar, value:cpp.ConstCharStar) -> Void>):Void;
}
#end
+20
View File
@@ -0,0 +1,20 @@
package funkin.external.apple;
#if ((ios || macos) && cpp)
/**
* A utility class to get information about the mem usage.
*/
@:build(funkin.util.macro.LinkerMacro.xml('project/Build.xml'))
@:include('MemoryUtil.hpp')
@:unreflective
extern class MemoryUtil
{
/**
* Retrieves the current process's resident set size (RSS) in bytes on Apple platforms.
*
* @return The resident set size (RSS) in bytes if successful; otherwise, returns 0 on failure.
*/
@:native('Apple_MemoryUtil_GetCurrentProcessRss')
static function getCurrentProcessRss():cpp.SizeT;
}
#end
+36
View File
@@ -0,0 +1,36 @@
package funkin.external.apple;
#if ((ios || macos) && cpp)
/**
* A utility class to get screen related informations on apple devices.
*/
@:build(funkin.util.macro.LinkerMacro.xml('project/Build.xml'))
@:include('ScreenUtil.hpp')
@:unreflective
extern class ScreenUtil
{
/**
* Retrieves the safe area insets for the current screen on iOS devices.
*
* This function populates the provided pointers with the safe area insets (in pixels) for the top, bottom, left, and right edges of the screen.
* On iOS 11 and later, it uses the main window's safeAreaInsets and converts them to pixel values using the screen scale.
* On earlier iOS versions, all insets are set to 0.0.
*
* @param top Pointer to a double to receive the top safe area inset (in pixels).
* @param bottom Pointer to a double to receive the bottom safe area inset (in pixels).
* @param left Pointer to a double to receive the left safe area inset (in pixels).
* @param right Pointer to a double to receive the right safe area inset (in pixels).
*/
@:native('Apple_ScreenUtil_GetSafeAreaInsets')
static function getSafeAreaInsets(top:cpp.RawPointer<Float>, bottom:cpp.RawPointer<Float>, left:cpp.RawPointer<Float>, right:cpp.RawPointer<Float>):Void;
/**
* Retrieves the size of the main screen in pixels.
*
* @param width Pointer to a double where the screen width (in pixels) will be stored.
* @param height Pointer to a double where the screen height (in pixels) will be stored.
*/
@:native('Apple_ScreenUtil_GetScreenSize')
static function getScreenSize(width:cpp.RawPointer<Float>, height:cpp.RawPointer<Float>):Void;
}
#end
+29
View File
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<xml>
<pragma once="true" />
<files id="haxe">
<compilerflag value="-I${this_dir}/include" />
</files>
<files id="__main__">
<compilerflag value="-I${this_dir}/include" />
</files>
<files id="external-apple">
<compilerflag value="-I${this_dir}/include" />
<file name="${this_dir}/src/AudioSession.mm" />
<file name="${this_dir}/src/MemoryUtil.mm" />
<file name="${this_dir}/src/ScreenUtil.mm" />
<file name="${this_dir}/src/FNFCUtil.mm" />
</files>
<target id="haxe">
<vflag name="-framework" value="UIKit" unless="macos" />
<vflag name="-framework" value="Foundation" />
<vflag name="-framework" value="AVFAudio" />
<files id="external-apple" />
</target>
</xml>
@@ -0,0 +1,18 @@
#pragma once
/**
* Initializes the audio session for playback.
*
* This function configures the shared AVAudioSession instance with the following settings:
* - Sets the audio session category to Playback, allowing Bluetooth A2DP and mixing with spoken audio interruptions.
* - For iOS 17.0 and above, disables interruption on route disconnect.
* - For iOS 14.5 and above, prefers no interruptions from system alerts.
*/
void Apple_AudioSession_Initialize();
/**
* Sets the active state of the shared AVAudioSession.
*
* @param active Whether to activate or deactivate the audio session.
*/
void Apple_AudioSession_SetActive(bool active);
@@ -0,0 +1,17 @@
#pragma once
/**
* Function pointer type for handling events with associated string values.
*
* @param event The name of the event as a null-terminated string.
* @param value The value associated with the event as a null-terminated string.
*/
typedef void (*FNFCCallback)(const char* event, const char* value);
/**
* Copies the FNFC resource from the specified URL into the cache.
*
* @param url The URL of the FNFC resource to copy into the cache.
* @param callback A function to be called when the copy operation completes.
*/
void Apple_FNFCUtil_CopyFNFCIntoCache(const char *url, FNFCCallback callback);
@@ -0,0 +1,10 @@
#pragma once
#include <cstddef>
/**
* Retrieves the current process's resident set size (RSS) in bytes on Apple platforms.
*
* @return The resident set size (RSS) in bytes if successful; otherwise, returns 0 on failure.
*/
size_t Apple_MemoryUtil_GetCurrentProcessRss();
@@ -0,0 +1,23 @@
#pragma once
/**
* Retrieves the safe area insets for the current screen on iOS devices.
*
* This function populates the provided pointers with the safe area insets (in pixels) for the top, bottom, left, and right edges of the screen.
* On iOS 11 and later, it uses the main window's safeAreaInsets and converts them to pixel values using the screen scale.
* On earlier iOS versions, all insets are set to 0.0.
*
* @param top Pointer to a double to receive the top safe area inset (in pixels).
* @param bottom Pointer to a double to receive the bottom safe area inset (in pixels).
* @param left Pointer to a double to receive the left safe area inset (in pixels).
* @param right Pointer to a double to receive the right safe area inset (in pixels).
*/
void Apple_ScreenUtil_GetSafeAreaInsets(double* top, double* bottom, double* left, double* right);
/**
* Retrieves the size of the main screen in pixels.
*
* @param width Pointer to a double where the screen width (in pixels) will be stored.
* @param height Pointer to a double where the screen height (in pixels) will be stored.
*/
void Apple_ScreenUtil_GetScreenSize(double* width, double* height);
@@ -0,0 +1,49 @@
#import "AudioSession.hpp"
#import <TargetConditionals.h>
#import <Foundation/Foundation.h>
#import <AVFAudio/AVFAudio.h>
#if TARGET_OS_IOS
#import <UIKit/UIKit.h>
#endif
void Apple_AudioSession_Initialize()
{
#if TARGET_OS_IOS
AVAudioSession *session = [AVAudioSession sharedInstance];
NSError *error;
[session setCategory:AVAudioSessionCategoryPlayback mode:AVAudioSessionModeDefault options:AVAudioSessionCategoryOptionAllowBluetoothA2DP error:&error];
if (@available(iOS 17.0, *))
{
[session setPrefersInterruptionOnRouteDisconnect:false error:nil];
}
if (@available(iOS 14.5, *))
{
[session setPrefersNoInterruptionsFromSystemAlerts:true error:nil];
}
if (error)
NSLog(@"Unable to set category of audio session: %@", error);
else
[session setActive:YES error:nil];
#endif
}
void Apple_AudioSession_SetActive(bool active)
{
#if TARGET_OS_IOS
AVAudioSession *session = [AVAudioSession sharedInstance];
NSError *error;
[session setActive:active error:&error];
if (error)
NSLog(@"Unable to set active of audio session: %@", error);
#endif
}
+132
View File
@@ -0,0 +1,132 @@
#include "FNFCUtil.hpp"
#import <Foundation/Foundation.h>
static FNFCCallback fnfcCallback = nullptr;
void Apple_FNFCUtil_CopyFNFCIntoCache(const char *url, FNFCCallback callback)
{
#if TARGET_OS_IOS
if (!fnfcCallback)
{
fnfcCallback = callback;
}
if (!url)
{
if (fnfcCallback)
{
dispatch_async(dispatch_get_main_queue(), ^{
fnfcCallback("FNFC_URL_NIL", "The URL passed to the conversion was null.");
});
}
return;
}
NSString *urlString = [NSString stringWithUTF8String:url];
NSURL *fileURL = [NSURL URLWithString:urlString];
if (!fileURL || !fileURL.isFileURL)
{
fileURL = [NSURL fileURLWithPath:urlString];
}
if (!fileURL || !fileURL.isFileURL)
{
if (fnfcCallback)
{
dispatch_async(dispatch_get_main_queue(), ^{
char *msg = strdup([[NSString stringWithFormat:@"The passed URL '%@' doesn't point at a FNFC file.", urlString] UTF8String]);
fnfcCallback("FNFC_FILE_MISMATC", msg);
free(msg);
});
}
return;
}
BOOL accessed = [fileURL startAccessingSecurityScopedResource];
if (!accessed)
{
if (fnfcCallback)
{
dispatch_async(dispatch_get_main_queue(), ^{
char *msg = strdup([[NSString stringWithFormat:@"Failed to get access to URL '%@'.", urlString] UTF8String]);
fnfcCallback("FNFC_FILE_ACCESS_ERR", msg);
free(msg);
});
}
}
NSString *sourcePath = [fileURL path];
NSString *filename = [sourcePath lastPathComponent];
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *cacheDir = [[paths firstObject] stringByAppendingPathComponent:@"fnfc"];
NSFileManager *fm = [NSFileManager defaultManager];
NSError *dirError = nil;
if (![fm fileExistsAtPath:cacheDir])
{
if (![fm createDirectoryAtPath:cacheDir withIntermediateDirectories:YES attributes:nil error:&dirError])
{
if (fnfcCallback)
{
dispatch_async(dispatch_get_main_queue(), ^{
char *msg = strdup([[NSString stringWithFormat:@"Failed to create directory %@: %@", cacheDir, dirError] UTF8String]);
fnfcCallback("FNFC_DIR_CREATE_ERR", msg);
free(msg);
});
}
}
}
NSString *destPath = [cacheDir stringByAppendingPathComponent:filename];
NSError *error = nil;
if ([fm fileExistsAtPath:destPath])
{
[fm removeItemAtPath:destPath error:nil];
}
if (![fm copyItemAtPath:sourcePath toPath:destPath error:&error])
{
if (fnfcCallback)
{
dispatch_async(dispatch_get_main_queue(), ^{
char *msg = strdup([[NSString stringWithFormat:@"Failed to copy FNFC from URL %@ to directory %@: %@", sourcePath, destPath, error] UTF8String]);
fnfcCallback("FNFC_COPY_ERR", msg);
free(msg);
});
}
if (accessed)
{
[fileURL stopAccessingSecurityScopedResource];
}
return;
}
if (accessed)
{
[fileURL stopAccessingSecurityScopedResource];
}
if (fnfcCallback)
{
dispatch_async(dispatch_get_main_queue(), ^{
char *val = strdup([destPath UTF8String]);
fnfcCallback("FNFC_RESULTS", val);
free(val);
});
}
#endif
}
+15
View File
@@ -0,0 +1,15 @@
#import "MemoryUtil.hpp"
#include <mach/mach.h>
size_t Apple_MemoryUtil_GetCurrentProcessRss()
{
struct task_basic_info info;
mach_msg_type_number_t count = TASK_BASIC_INFO_COUNT;
if (task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)&info, &count) != KERN_SUCCESS)
return 0;
return info.resident_size;
}
+45
View File
@@ -0,0 +1,45 @@
#import "ScreenUtil.hpp"
#import <TargetConditionals.h>
#if TARGET_OS_IOS
#import <UIKit/UIKit.h>
#endif
#import <Foundation/Foundation.h>
void Apple_ScreenUtil_GetSafeAreaInsets(double* top, double* bottom, double* left, double* right)
{
#if TARGET_OS_IOS
if (@available(iOS 11, *))
{
UIWindow* window = [UIApplication sharedApplication].windows[0];
UIEdgeInsets safeAreaInsets = window.safeAreaInsets;
float scale = [UIScreen mainScreen].scale;
(*top) = safeAreaInsets.top * scale;
(*bottom) = safeAreaInsets.bottom * scale;
(*left) = safeAreaInsets.left * scale;
(*right) = safeAreaInsets.right * scale;
return;
}
#endif
(*top) = 0.0;
(*bottom) = 0.0;
(*left) = 0.0;
(*right) = 0.0;
}
void Apple_ScreenUtil_GetScreenSize(double* width, double* height)
{
#if TARGET_OS_IOS
CGRect screenRect = [[UIScreen mainScreen] bounds];
float scale = [UIScreen mainScreen].scale;
(*width) = (double)screenRect.size.width * scale;
(*height) = (double)screenRect.size.height * scale;
#else
(*width) = 0.0;
(*width) = 0.0;
#endif
}
-17
View File
@@ -1,17 +0,0 @@
package funkin.external.ios;
#if ios
/**
* A Utility class to manage iOS audio.
*/
@:build(funkin.util.macro.LinkerMacro.xml('project/Build.xml'))
@:include('AudioSession.hpp')
@:unreflective
extern class AudioSession
{
@:native('initialize')
static function initialize():Void;
@:native('setActive')
static function setActive(active:Bool):Void;
}
#end
-18
View File
@@ -1,18 +0,0 @@
package funkin.external.ios;
#if ios
/**
* A Utility class to get iOS screen related informations.
*/
@:build(funkin.util.macro.LinkerMacro.xml('project/Build.xml'))
@:include('ScreenUtil.hpp')
@:unreflective
extern class ScreenUtil
{
@:native('getSafeAreaInsets')
static function getSafeAreaInsets(top:cpp.RawPointer<Float>, bottom:cpp.RawPointer<Float>, left:cpp.RawPointer<Float>, right:cpp.RawPointer<Float>):Void;
@:native('getScreenSize')
static function getScreenSize(width:cpp.RawPointer<Float>, height:cpp.RawPointer<Float>):Void;
}
#end
-29
View File
@@ -1,29 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<xml>
<pragma once="true" />
<files id="haxe">
<compilerflag value="-I${this_dir}/ios/include" if="iphoneos || iphonesim" />
</files>
<files id="__main__">
<compilerflag value="-I${this_dir}/ios/include" if="iphoneos || iphonesim" />
</files>
<files id="external-ios" dir="${this_dir}/ios" if="iphoneos || iphonesim">
<compilerflag value="-I${this_dir}/ios/include" />
<file name="src/ScreenUtil.mm" />
<file name="src/AudioSession.mm" />
</files>
<target id="haxe">
<section if="iphoneos || iphonesim">
<vflag name="-framework" value="UIKit" />
<vflag name="-framework" value="Foundation" />
<vflag name="-framework" value="AVFAudio" />
</section>
<files id="external-ios" if="iphoneos || iphonesim" />
</target>
</xml>
@@ -1,4 +0,0 @@
#pragma once
void initialize();
void setActive(bool active);
@@ -1,4 +0,0 @@
#pragma once
void getSafeAreaInsets(double* top, double* bottom, double* left, double* right);
void getScreenSize(double* width, double* height);
@@ -1,41 +0,0 @@
#import <Foundation/Foundation.h>
#import <AVFAudio/AVFAudio.h>
void initialize()
{
AVAudioSession *session = [AVAudioSession sharedInstance];
NSError *error;
[session setCategory:AVAudioSessionCategoryPlayback
mode:AVAudioSessionModeDefault
options:AVAudioSessionCategoryOptionAllowBluetoothA2DP|AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers
error:&error];
if (@available(iOS 17.0, *))
{
[session setPrefersInterruptionOnRouteDisconnect:false error:nil];
}
if (@available(iOS 14.5, *))
{
[session setPrefersNoInterruptionsFromSystemAlerts:true error:nil];
}
if (error)
{
NSLog(@"Unable to set category of audio session: %@", error);
}
}
void setActive(bool active)
{
AVAudioSession *session = [AVAudioSession sharedInstance];
NSError *error;
[session setActive:YES error:&error];
if (error)
{
NSLog(@"Unable to set active of audio session: %@", error);
}
}
@@ -1,36 +0,0 @@
#import "ScreenUtil.hpp"
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
void getSafeAreaInsets(double* top, double* bottom, double* left, double* right)
{
if (@available(iOS 11, *))
{
UIWindow* window = [UIApplication sharedApplication].windows[0];
UIEdgeInsets safeAreaInsets = window.safeAreaInsets;
float scale = [UIScreen mainScreen].scale;
(*top) = safeAreaInsets.top * scale;
(*bottom) = safeAreaInsets.bottom * scale;
(*left) = safeAreaInsets.left * scale;
(*right) = safeAreaInsets.right * scale;
return;
}
(*top) = 0.0;
(*bottom) = 0.0;
(*left) = 0.0;
(*right) = 0.0;
}
void getScreenSize(double* width, double* height)
{
CGRect screenRect = [[UIScreen mainScreen] bounds];
float scale = [UIScreen mainScreen].scale;
(*width) = (double)screenRect.size.width * scale;
(*height) = (double)screenRect.size.height * scale;
}
+17 -14
View File
@@ -11,38 +11,32 @@ extern class WinAPI
/**
* Shows a message box with an error icon.
*
* @param handle A handle to the parent window.
* @param message The message to display.
* @param title The title of the message box.
*/
@:native('WINAPI_ShowError')
static function showError(message:cpp.ConstCharStar, title:cpp.ConstCharStar):Void;
static function showError(handle:cpp.RawPointer<cpp.Void>, message:cpp.ConstCharStar, title:cpp.ConstCharStar):Void;
/**
* Shows a message box with a warning icon.
*
* @param handle A handle to the parent window.
* @param message The message to display.
* @param title The title of the message box.
*/
@:native('WINAPI_ShowWarning')
static function showWarning(message:cpp.ConstCharStar, title:cpp.ConstCharStar):Void;
static function showWarning(handle:cpp.RawPointer<cpp.Void>, message:cpp.ConstCharStar, title:cpp.ConstCharStar):Void;
/**
* Shows a message box with an information icon.
*
* @param handle A handle to the parent window.
* @param message The message to display.
* @param title The title of the message box.
*/
@:native('WINAPI_ShowInformation')
static function showInformation(message:cpp.ConstCharStar, title:cpp.ConstCharStar):Void;
/**
* Shows a message box with a question icon.
*
* @param message The message to display.
* @param title The title of the message box.
*/
@:native('WINAPI_ShowQuestion')
static function showQuestion(message:cpp.ConstCharStar, title:cpp.ConstCharStar):Void;
static function showInformation(handle:cpp.RawPointer<cpp.Void>, message:cpp.ConstCharStar, title:cpp.ConstCharStar):Void;
/**
* Disables the "Report to Microsoft" dialog that appears when the application crashes.
@@ -56,13 +50,22 @@ extern class WinAPI
@:native('WINAPI_DisableWindowsGhosting')
static function disableWindowsGhosting():Void;
/**
* Retrieves the current working set size (in bytes) of the process.
*
* @return The size of the working set memory used by the process.
*/
@:native('WINAPI_GetProcessMemoryWorkingSetSize')
static function getProcessMemoryWorkingSetSize():cpp.SizeT;
/**
* Sets the dark mode for the active window.
*
* @param enable A boolean value indicating whether to enable (true) or disable (false) dark mode.
* @param handle A handle to the parent window.
* @param enable Whether to enable or disable dark mode.
*/
@:native('WINAPI_SetDarkMode')
static function setDarkMode(enable:Bool):Void;
static function setDarkMode(handle:cpp.RawPointer<cpp.Void>, enable:Bool):Void;
/**
* Checks if the system is using dark mode.
+3 -3
View File
@@ -10,7 +10,7 @@
<compilerflag value="-I${this_dir}/include" />
</files>
<files id="winapi">
<files id="external-windows">
<compilerflag value="-I${this_dir}/include" />
<file name="${this_dir}/src/winapi.cpp" />
@@ -24,13 +24,13 @@
<lib name="-ladvapi32" />
</section>
<section if="windows" unless="mingw">
<section unless="mingw">
<lib name="user32.lib" />
<lib name="kernel32.lib" />
<lib name="dwmapi.lib" />
<lib name="advapi32.lib" />
</section>
<files id="winapi" />
<files id="external-windows" />
</target>
</xml>
+30 -19
View File
@@ -1,51 +1,62 @@
#pragma once
/**
* @brief Shows an error message box
* Shows an error message box.
*
* @param handle A handle to the parent window
* @param message The error message to display
* @param title The title of the message box
*/
void WINAPI_ShowError(const char *message, const char *title);
void WINAPI_ShowError(void* handle, const char *message, const char *title);
/**
* @brief Shows a warning message box
* Shows a warning message box.
*
* @param handle A handle to the parent window
* @param message The warning message to display
* @param title The title of the message box
*/
void WINAPI_ShowWarning(const char *message, const char *title);
void WINAPI_ShowWarning(void* handle, const char *message, const char *title);
/**
* @brief Shows an information message box
* Shows an information message box.
*
* @param handle A handle to the parent window
* @param message The information message to display
* @param title The title of the message box
*/
void WINAPI_ShowInformation(const char *message, const char *title);
void WINAPI_ShowInformation(void* handle, const char *message, const char *title);
/**
* @brief Shows a question message box with OK/Cancel buttons
* @param message The question message to display
* @param title The title of the message box
*/
void WINAPI_ShowQuestion(const char *message, const char *title);
/**
* @brief Disables Windows error reporting dialogs
* Disables Windows error reporting dialogs.
*/
void WINAPI_DisableErrorReporting();
/**
* @brief Disables Windows ghosting for the current process
* Disables Windows ghosting for the current process.
*/
void WINAPI_DisableWindowsGhosting();
/**
* @brief Sets dark mode for the active window
* @param enable True to enable dark mode, false to disable
* Retrieves the current working set size (in bytes) of the calling process.
*
* This function queries the operating system for the amount of physical memory currently allocated to the process (its working set).
*
* @return The working set size in bytes. Returns 0 if the query fails.
*/
void WINAPI_SetDarkMode(bool enable);
size_t WINAPI_GetProcessMemoryWorkingSetSize();
/**
* @brief Checks if the system is using dark mode
* Sets dark mode on a window
* @param handle A handle to the parent window.
*
* @param enable True to enable dark mode, false to disable
*/
void WINAPI_SetDarkMode(void* handle, bool enable);
/**
* Checks if the system is using dark mode.
*
* @return True if system is in dark mode, false otherwise
*/
bool WINAPI_IsSystemDarkMode();
+39 -15
View File
@@ -11,25 +11,36 @@
#include <stdint.h>
#include <stdio.h>
void WINAPI_ShowError(const char *message, const char *title)
void WINAPI_ShowError(void* handle, const char *message, const char *title)
{
MessageBox(GetActiveWindow(), message, title, MB_OK | MB_ICONERROR);
HWND hwnd = (HWND)handle;
if (!IsWindow(hwnd))
hwnd = nullptr;
MessageBox(hwnd, message, title, MB_OK | MB_ICONERROR);
}
void WINAPI_ShowWarning(const char *message, const char *title)
void WINAPI_ShowWarning(void* handle, const char *message, const char *title)
{
MessageBox(GetActiveWindow(), message, title, MB_OK | MB_ICONWARNING);
HWND hwnd = (HWND)handle;
if (!IsWindow(hwnd))
hwnd = nullptr;
MessageBox(hwnd, message, title, MB_OK | MB_ICONWARNING);
}
void WINAPI_ShowInformation(const char *message, const char *title)
void WINAPI_ShowInformation(void* handle, const char *message, const char *title)
{
MessageBox(GetActiveWindow(), message, title, MB_OK | MB_ICONINFORMATION);
HWND hwnd = (HWND)handle;
if (!IsWindow(hwnd))
hwnd = nullptr;
MessageBox(hwnd, message, title, MB_OK | MB_ICONINFORMATION);
}
void WINAPI_ShowQuestion(const char *message, const char *title)
{
MessageBox(GetActiveWindow(), message, title, MB_OKCANCEL | MB_ICONQUESTION);
}
void WINAPI_DisableErrorReporting()
{
@@ -41,16 +52,29 @@ void WINAPI_DisableWindowsGhosting()
DisableProcessWindowsGhosting();
}
void WINAPI_SetDarkMode(bool enable)
size_t WINAPI_GetProcessMemoryWorkingSetSize()
{
HWND window = GetActiveWindow();
PROCESS_MEMORY_COUNTERS_EX pmc;
if (GetProcessMemoryInfo(GetCurrentProcess(), (PROCESS_MEMORY_COUNTERS*)&pmc, sizeof(pmc)))
return pmc.WorkingSetSize;
return 0;
}
void WINAPI_SetDarkMode(void* handle, bool enable)
{
HWND hwnd = (HWND)handle;
if (!IsWindow(hwnd))
return;
int darkMode = enable ? 1 : 0;
if (DwmSetWindowAttribute(window, 20, &darkMode, sizeof(darkMode)) != S_OK)
DwmSetWindowAttribute(window, 19, &darkMode, sizeof(darkMode));
if (DwmSetWindowAttribute(hwnd, 20, &darkMode, sizeof(darkMode)) != S_OK)
DwmSetWindowAttribute(hwnd, 19, &darkMode, sizeof(darkMode));
UpdateWindow(window);
UpdateWindow(hwnd);
}
bool WINAPI_IsSystemDarkMode()
@@ -0,0 +1,35 @@
package funkin.graphics;
import funkin.graphics.FunkinSprite;
import animate.FlxAnimateController;
class FunkinAnimationController extends FlxAnimateController
{
/**
* The sprite that this animation controller is attached to.
*/
var _parentSprite:FunkinSprite;
public function new(sprite:FunkinSprite)
{
super(sprite);
_parentSprite = sprite;
}
/**
* We override `FlxAnimationController`'s `play` method to account for texture atlases.
*/
public override function play(animName:String, force = false, reversed = false, frame = 0):Void
{
if (animName == null || animName == '') animName = _parentSprite.getDefaultSymbol();
if (!_parentSprite.hasAnimation(animName))
{
// Skip if the animation doesn't exist
trace('Animation ${animName} does not exist!');
return;
}
super.play(animName, force, reversed, frame);
}
}
-1
View File
@@ -254,7 +254,6 @@ class FunkinCamera extends FlxCamera
function disposeTextures():Void
{
trace('disposing textures');
for (bitmap in grabbed)
{
bitmap.dispose();
+505 -136
View File
@@ -9,28 +9,158 @@ import funkin.graphics.framebuffer.FixedBitmapData;
import openfl.display.BitmapData;
import flixel.math.FlxRect;
import flixel.math.FlxPoint;
import flixel.graphics.frames.FlxFrame.FlxFrameAngle;
import flixel.math.FlxMatrix;
import flixel.graphics.frames.FlxFrame;
import flixel.FlxCamera;
import openfl.system.System;
import flixel.system.FlxAssets.FlxGraphicAsset;
import funkin.FunkinMemory;
import animate.internal.SymbolItem;
import animate.internal.elements.Element;
import animate.internal.elements.AtlasInstance;
import animate.internal.elements.SymbolInstance;
import animate.FlxAnimate;
import animate.FlxAnimateFrames;
import haxe.io.Path;
using StringTools;
typedef AtlasSpriteSettings =
{
/**
* If true, the texture atlas will behave as if it was exported as an SWF file.
* Notably, this allows MovieClip symbols to play.
*/
@:optional
var swfMode:Bool;
/**
* If true, filters and masks will be cached when the atlas is loaded, instead of during runtime.
*/
@:optional
var cacheOnLoad:Bool;
/**
* The filter quality.
* Available values are: HIGH, MEDIUM, LOW, and RUDY.
*
* If you're making an atlas sprite in HScript, you pass an Int instead:
*
* HIGH - 0
* MEDIUM - 1
* LOW - 2
* RUDY - 3
*/
@:optional
var filterQuality:FilterQuality;
/**
* Optional, an array of spritemaps for the atlas to load.
*/
@:optional
var spritemaps:Array<SpritemapInput>;
/**
* Optional, string of the metadata.json contents.
*/
@:optional
var metadataJson:String;
/**
* Optional, force the cache to use a specific key to index the texture atlas.
*/
@:optional
var cacheKey:String;
/**
* If true, the texture atlas will use a new slot in the cache.
*/
@:optional
var uniqueInCache:Bool;
/**
* Optional callback for when a symbol is created.
*/
@:optional
var onSymbolCreate:animate.internal.SymbolItem->Void;
/**
* Whether to apply the stage matrix, if it was exported from a symbol instance.
* Also positions the Texture Atlas as it displays in Animate.
* Turning this on is only recommended if you prepositioned the character in Animate.
* For other cases, it should be turned off to act similarly to a normal FlxSprite.
*/
@:optional
var applyStageMatrix:Bool;
/**
* If enabled, the sprite will render as one texture instead of rendering multiple limbs.
* This is useful for stuff like changing alpha, and shaders that require the whole sprite.
*
* Only enable this if your sprite either:
* - Changes alpha to something other than 1.0
* - Has a shader or blend mode
*/
@:optional
var useRenderTexture:Bool;
}
/**
* An FlxSprite with additional functionality.
* - A more efficient method for creating solid color sprites.
* - TODO: Better cache handling for textures.
*/
@:nullSafety
class FunkinSprite extends FlxSprite
class FunkinSprite extends FlxAnimate
{
/**
* @param x Starting X position
* @param y Starting Y position
* @param path The asset path for the graphic
* @param atlasSettings The optional settings for the texture atlas
*/
public function new(?x:Float = 0, ?y:Float = 0)
public function new(?x:Float = 0, ?y:Float = 0, ?path:String, ?atlasSettings:AtlasSpriteSettings)
{
super(x, y);
if (path != null)
{
var ext:String = Path.extension(path);
switch (ext)
{
case 'png':
this.loadGraphic(path);
case '':
// Do the opposite of Paths.animateAtlas since that function is called in loadTextureAtlas.
var lib:String = Paths.getLibrary(path);
if (lib == 'preload')
{
path = path.replace('assets/images/', '');
}
else
{
path = path.replace('$lib:assets/$lib/images/', '');
}
this.loadTextureAtlas(path, lib, atlasSettings);
default:
FlxG.log.warn('Texture path $path is not a valid path. Make sure the path points to either an image or a folder with the texture atlas files!');
}
}
}
override function initVars():Void
{
super.initVars();
var newController:FunkinAnimationController = new FunkinAnimationController(this);
animation = newController;
anim = newController;
}
/**
@@ -75,6 +205,20 @@ class FunkinSprite extends FlxSprite
return sprite;
}
/**
* Create a new FunkinSprite with an Adobe Animate texture atlas.
* @param x The starting X position.
* @param y The starting Y position.
* @param key The key of the texture to load.
* @return The new FunkinSprite.
*/
public static function createTextureAtlas(x:Float = 0.0, y:Float = 0.0, key:String, ?assetLibrary:Null<String>, ?settings:AtlasSpriteSettings):FunkinSprite
{
var sprite:FunkinSprite = new FunkinSprite(x, y);
sprite.loadTextureAtlas(key, assetLibrary ?? "", settings);
return sprite;
}
/**
* Load a static image as the sprite's texture.
* @param key The key of the texture to load.
@@ -83,7 +227,17 @@ class FunkinSprite extends FlxSprite
public function loadTexture(key:String):FunkinSprite
{
var graphicKey:String = Paths.image(key);
if (!isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
if (!Assets.exists(graphicKey, IMAGE))
{
FlxG.log.error('Texture not found, check your path! $graphicKey');
return this;
}
if (!FunkinMemory.isTextureCached(graphicKey))
{
FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
}
loadGraphic(graphicKey);
@@ -163,6 +317,32 @@ class FunkinSprite extends FlxSprite
return loadBitmapData(inputBitmap);
}
/**
* Loads an Adobe Animate texture atlas as the sprite's texture.
* @param key The key of the texture to load.
* @param settings Additional settings for loading the atlas.
* @return This sprite, for chaining.
*/
public function loadTextureAtlas(key:Null<String>, ?assetLibrary:Null<String>, ?settings:AtlasSpriteSettings):FunkinSprite
{
if (key == null)
{
throw 'Null path specified for loadTextureAtlas()!';
}
if (settings == null)
{
settings = getDefaultAtlasSettings();
}
this.applyStageMatrix = settings.applyStageMatrix ?? false;
this.useRenderTexture = settings.useRenderTexture ?? false;
frames = Paths.getAnimateAtlas(key, assetLibrary, settings);
return this;
}
/**
* Load an animated texture (Sparrow atlas spritesheet) as the sprite's texture.
* @param key The key of the texture to load.
@@ -171,7 +351,7 @@ class FunkinSprite extends FlxSprite
public function loadSparrow(key:String):FunkinSprite
{
var graphicKey:String = Paths.image(key);
if (!isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
if (!FunkinMemory.isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
this.frames = Paths.getSparrowAtlas(key);
@@ -186,73 +366,13 @@ class FunkinSprite extends FlxSprite
public function loadPacker(key:String):FunkinSprite
{
var graphicKey:String = Paths.image(key);
if (!isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
if (!FunkinMemory.isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
this.frames = Paths.getPackerAtlas(key);
return this;
}
/**
* Determine whether the texture with the given key is cached.
* @param key The key of the texture to check.
* @return Whether the texture is cached.
*/
public static function isTextureCached(key:String):Bool
{
return FlxG.bitmap.get(key) != null;
}
@:deprecated("Use FunkinMemory.cacheTexture() instead")
public static function cacheTexture(key:String):Void
{
FunkinMemory.cacheTexture(Paths.image(key));
}
@:deprecated("Use FunkinMemory.permanentCacheTexture() instead")
public static function permanentCacheTexture(key:String):Void
{
@:privateAccess FunkinMemory.permanentCacheTexture(Paths.image(key));
}
@:deprecated("Use FunkinMemory.cacheTexture() instead")
public static function cacheSparrow(key:String):Void
{
FunkinMemory.cacheTexture(Paths.image(key));
}
@:deprecated("Use FunkinMemory.cacheTexture() instead")
public static function cachePacker(key:String):Void
{
FunkinMemory.cacheTexture(Paths.image(key));
}
@:deprecated("Use FunkinMemory.preparePurgeTextureCache() instead")
public static function preparePurgeCache():Void
{
FunkinMemory.preparePurgeTextureCache();
}
@:deprecated("Use FunkinMemory.purgeCache() instead")
public static function purgeCache():Void
{
FunkinMemory.purgeCache();
}
static function isGraphicCached(graphic:FlxGraphic):Bool
{
var result = null;
if (graphic == null) return false;
result = FlxG.bitmap.get(graphic.key);
if (result == null) return false;
if (result != graphic)
{
FlxG.log.warn('Cached graphic does not match original: ${graphic.key}');
return false;
}
return true;
}
/**
* @param id The animation ID to check.
* @return Whether the animation is dynamic (has multiple frames). `false` for static, one-frame animations.
@@ -266,6 +386,107 @@ class FunkinSprite extends FlxSprite
return animData.numFrames > 1;
}
/**
* Whether or not this sprite has an animation with the given ID.
* @param id The ID of the animation to check.
*/
public function hasAnimation(id:String):Bool
{
var animationList:Array<String> = this.animation?.getNameList() ?? [];
if (animationList.contains(id))
{
return true;
}
else if (this.isAnimate && !animationList.contains(id))
{
return addAnimationIfMissing(id);
}
return false;
}
/**
* Adds an animation if it doesn't exist.
* @param id The animation ID to check.
*/
function addAnimationIfMissing(id:String):Bool
{
@:privateAccess
var symbols:Array<String> = this.library.dictionary.keys().array();
var frameLabels:Array<String> = listAnimations();
if (frameLabels.contains(id))
{
// Animation exists as a frame label but wasn't added, so we add it
anim.addByFrameLabel(id, id, this.library.frameRate, false);
return true;
}
else if (symbols.contains(id))
{
// Animation exists as a symbol but wasn't added, so we add it
anim.addBySymbol(id, id, this.library.frameRate, false);
return true;
}
return false;
}
/**
* Gets every frame on every symbol that starts with the given keyword.
* @param keyword The keyword to search for.
* @return An array of frames.
*/
public function getFramesWithKeyword(keyword:String):Array<animate.internal.Frame>
{
if (!this.isAnimate)
{
trace('WARNING: getFramesWithKeyword() only works texture atlases!');
return [];
}
var symbolItems:Array<animate.internal.SymbolItem> = [];
var frames:Array<animate.internal.Frame> = [];
@:privateAccess
for (symbol in this.library.dictionary.keys())
{
var symbolItem:Null<animate.internal.SymbolItem> = this.library.getSymbol(symbol);
if (symbolItem == null) continue;
if (symbolItem.name.contains(keyword))
{
symbolItems.push(symbolItem);
}
}
for (symbolItem in symbolItems)
{
symbolItem.timeline.forEachLayer((layer) -> {
layer.forEachFrame((frame) -> {
frames.push(frame);
});
});
}
return frames;
}
/**
* Gets the current animation ID.
*/
public function getCurrentAnimation():String
{
return this.animation?.curAnim?.name ?? '';
}
/**
* Whether or not the current animation is finished.
*/
public function isAnimationFinished():Bool
{
return this.animation?.finished ?? false;
}
/**
* Acts similarly to `makeGraphic`, but with improved memory usage,
* at the expense of not being able to paint onto the resulting sprite.
@@ -286,6 +507,221 @@ class FunkinSprite extends FlxSprite
return this;
}
/**
* @return A list of all the animations this sprite has available.
*/
public function listAnimations():Array<String>
{
var frameLabels:Array<String> = getFrameLabelList();
var animationList:Array<String> = this.animation?.getNameList() ?? [];
return frameLabels.concat(animationList);
}
/**
* TEXTURE ATLAS-EXCLUSIVE FUNCTIONS
* These functions only work if the sprite's texture is an Adobe Animate texture atlas.
* Calling these functions on non-texture atlases will do nothing.
*/
/**
* Gets a list of frame labels from the default timeline.
*/
public function getFrameLabelList():Array<String>
{
if (!this.isAnimate)
{
trace('WARNING: getFrameLabelList() only works texture atlases!');
return [];
}
var foundLabels:Array<String> = [];
var mainTimeline:Null<animate.internal.Timeline> = this.library.timeline;
for (layer in mainTimeline.layers)
{
@:nullSafety(Off)
for (frame in layer.frames)
{
if (frame.name.rtrim() != '')
{
foundLabels.push(frame.name);
}
}
}
return foundLabels;
}
/**
* Gets a frame label by its name.
* @param name The name of the frame label to retrieve.
* @return The frame label, or null if it doesn't exist.
*/
public function getFrameLabel(name:String):Null<animate.internal.Frame>
{
if (!this.isAnimate)
{
trace('WARNING: getFrameLabel() only works texture atlases!');
return null;
}
for (layer in this.timeline.layers)
{
@:nullSafety(Off)
for (frame in layer.frames)
{
if (frame.name == name)
{
return frame;
}
}
}
return null;
}
/**
* Returns the default symbol in the atlas.
*/
public function getDefaultSymbol():String
{
if (!this.isAnimate)
{
trace('WARNING: getDefaultSymbol() only works texture atlases!');
return '';
}
return library.timeline.name;
}
/**
* Replaces the graphic of a symbol in the atlas.
* @param symbol The symbol to replace.
* @param graphic The new graphic to use.
* @param adjustScale Whether to adjust the scale of new frame to match the old one.
*/
public function replaceSymbolGraphic(symbol:String, ?graphic:Null<FlxGraphicAsset>, ?adjustScale:Bool = true):Void
{
if (!this.isAnimate)
{
trace('WARNING: replaceSymbolGraphic() only works texture atlases!');
return;
}
var elements:Array<Element> = getSymbolElements(symbol);
for (element in elements)
{
var atlasInstance:AtlasInstance = element.toAtlasInstance();
var frame:Null<FlxFrame> = graphic != null ? FlxG.bitmap.add(graphic).imageFrame.frame : null;
atlasInstance.replaceFrame(frame, adjustScale);
element = atlasInstance;
}
}
/**
* Returns the first element of a symbol in the atlas.
* @param symbol The symbol to get elements from.
* @return The first element of the symbol. WARNING: Can be null.
*/
public function getFirstElement(symbol:String):Null<Element>
{
if (!this.isAnimate)
{
trace('WARNING: getFirstElement() only works texture atlases!');
return null;
}
var symbolElements:Array<Element> = getSymbolElements(symbol);
return symbolElements.length > 0 ? symbolElements[0] : null;
}
/**
* Returns the elements of a symbol in the atlas.
* @param symbol The symbol to get elements from.
*/
public function getSymbolElements(symbol:String):Array<Element>
{
if (!this.isAnimate)
{
trace('WARNING: getSymbolElements() only works texture atlases!');
return [];
}
var symbolInstance:Null<SymbolItem> = this.library.getSymbol(symbol);
if (symbolInstance == null)
{
throw 'Symbol not found in atlas: ${symbol}';
return [];
}
var elements:Array<Element> = symbolInstance.timeline.getElementsAtIndex(0);
if (elements?.length == 0)
{
trace('WARNING: No Atlas Elements found for "$symbol" symbol.');
}
return elements ?? [];
}
/**
* Scales an element by a certain multiplier.
* @param element The element to scale.
* @param scale The scale multiplier.
* @param positionOffset The offset to apply to `tx` and `ty` after scaling.
* (Or in other words, the position of the element.)
*/
public function scaleElement(element:Element, scale:Float, positionOffset:Float = 0, scaleEverything:Bool = false):Void
{
if (!this.isAnimate)
{
trace('WARNING: scaleElement() only works texture atlases!');
return;
}
var elementMatrix:FlxMatrix = element.matrix;
if (scaleEverything)
{
elementMatrix.scale(scale, scale);
return;
}
var symbolInstance:SymbolInstance = element.parentFrame.convertToSymbol(0, 1);
var transformPoint:FlxPoint = symbolInstance.transformationPoint;
elementMatrix.a += scale;
elementMatrix.d += scale;
elementMatrix.tx -= transformPoint.x * scale;
elementMatrix.ty -= transformPoint.y * scale;
elementMatrix.tx -= positionOffset;
elementMatrix.ty -= positionOffset;
}
/**
* Gets the default settings for a texture atlas sprite.
* @return The default settings for a texture atlas sprite.
*/
public function getDefaultAtlasSettings():AtlasSpriteSettings
{
return {
swfMode: false,
cacheOnLoad: false,
filterQuality: MEDIUM,
spritemaps: null,
metadataJson: null,
cacheKey: null,
uniqueInCache: false,
onSymbolCreate: null,
applyStageMatrix: false,
useRenderTexture: false
};
}
/**
* Ensure scale is applied when cloning a sprite.R
* The default `clone()` method acts kinda weird TBH.
@@ -325,77 +761,10 @@ class FunkinSprite extends FlxSprite
return _rect;
}
/**
* Returns the screen position of this object.
*
* @param result Optional arg for the returning point
* @param camera The desired "screen" coordinate space. If `null`, `FlxG.camera` is used.
* @return The screen position of this object.
*/
public override function getScreenPosition(?result:FlxPoint, ?camera:FlxCamera):FlxPoint
override function preparePixelPerfectMatrix(matrix:FlxMatrix)
{
if (result == null) result = FlxPoint.get();
if (camera == null) camera = FlxG.camera;
result.set(x, y);
if (pixelPerfectPosition)
{
_rect.width = _rect.width / this.scale.x;
_rect.height = _rect.height / this.scale.y;
_rect.x = _rect.x / this.scale.x;
_rect.y = _rect.y / this.scale.y;
_rect.round();
_rect.x = _rect.x * this.scale.x;
_rect.y = _rect.y * this.scale.y;
_rect.width = _rect.width * this.scale.x;
_rect.height = _rect.height * this.scale.y;
}
return result.subtract(camera.scroll.x * scrollFactor.x, camera.scroll.y * scrollFactor.y);
}
override function drawSimple(camera:FlxCamera):Void
{
getScreenPosition(_point, camera).subtractPoint(offset);
if (isPixelPerfectRender(camera))
{
_point.x = _point.x / this.scale.x;
_point.y = _point.y / this.scale.y;
_point.round();
_point.x = _point.x * this.scale.x;
_point.y = _point.y * this.scale.y;
}
_point.copyToFlash(_flashPoint);
camera.copyPixels(_frame, framePixels, _flashRect, _flashPoint, colorTransform, blend, antialiasing);
}
override function drawComplex(camera:FlxCamera):Void
{
_frame.prepareMatrix(_matrix, FlxFrameAngle.ANGLE_0, checkFlipX(), checkFlipY());
_matrix.translate(-origin.x, -origin.y);
_matrix.scale(scale.x, scale.y);
if (bakedRotationAngle <= 0)
{
updateTrig();
if (angle != 0) _matrix.rotateWithTrig(_cosAngle, _sinAngle);
}
getScreenPosition(_point, camera).subtractPoint(offset);
_point.add(origin.x, origin.y);
_matrix.translate(_point.x, _point.y);
if (isPixelPerfectRender(camera))
{
_matrix.tx = Math.round(_matrix.tx / this.scale.x) * this.scale.x;
_matrix.ty = Math.round(_matrix.ty / this.scale.y) * this.scale.y;
}
camera.drawPixels(_frame, framePixels, _matrix, colorTransform, blend, antialiasing, shader);
matrix.tx = Math.round(matrix.tx / this.scale.x) * this.scale.x;
matrix.ty = Math.round(matrix.ty / this.scale.y) * this.scale.y;
}
public override function destroy():Void
@@ -1,419 +0,0 @@
package funkin.graphics.adobeanimate;
import flixel.util.FlxSignal.FlxTypedSignal;
import flxanimate.FlxAnimate;
import flxanimate.FlxAnimate.Settings;
import flixel.graphics.frames.FlxFrame;
import flixel.system.FlxAssets.FlxGraphicAsset;
import flixel.math.FlxPoint;
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 =
{
// ?ButtonSettings:Map<String, flxanimate.animate.FlxAnim.ButtonSettings>,
FrameRate: 24.0,
Reversed: false,
// ?OnComplete:Void -> Void,
ShowPivot: false,
Antialiasing: true,
ScrollFactor: null,
// Offset: new FlxPoint(0, 0), // This is just FlxSprite.offset
};
/**
* Signal dispatched when an animation advances to the next frame.
*/
public var onAnimationFrame:FlxTypedSignal<String->Int->Void> = new FlxTypedSignal();
/**
* Signal dispatched when a non-looping animation finishes playing.
*/
public var onAnimationComplete:FlxTypedSignal<String->Void> = new FlxTypedSignal();
/**
* Signal dispatched when a looping animation finishes playing.
*/
public var onAnimationLoop:FlxTypedSignal<String->Void> = new FlxTypedSignal();
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;
if (path == null)
{
throw 'Null path specified for FlxAtlasSprite!';
}
// Validate asset path.
if (!Assets.exists('${path}/Animation.json'))
{
throw 'FlxAtlasSprite does not have an Animation.json file at the specified path (${path})';
}
super(x, y, path, settings);
if (this.anim.stageInstance == null)
{
throw 'FlxAtlasSprite not initialized properly. Are you sure the path (${path}) exists?';
}
onAnimationComplete.add(cleanupAnimation);
// This defaults the sprite to play the first animation in the atlas,
// then pauses it. This ensures symbols are initialized properly.
this.anim.play('');
this.anim.pause();
this.anim.onComplete.add(_onAnimationComplete);
this.anim.onFrame.add(_onAnimationFrame);
}
/**
* @return A list of all the animations this sprite has available.
*/
public function listAnimations():Array<String>
{
var mainSymbol = this.anim.symbolDictionary[this.anim.stageInstance.symbol.name];
if (mainSymbol == null)
{
FlxG.log.error('FlxAtlasSprite does not have its main symbol!');
return [];
}
return mainSymbol.getFrameLabels().map(keyFrame -> keyFrame.name).filterNull();
}
/**
* @param id A string ID of the animation.
* @return Whether the animation was found on this sprite.
*/
public function hasAnimation(id:String):Bool
{
return getLabelIndex(id) != -1 || anim.symbolDictionary.exists(id);
}
/**
* @return The current animation being played.
*/
public function getCurrentAnimation():String
{
return this.currentAnimation;
}
var _completeAnim:Bool = false;
var fr:Null<FlxKeyFrame> = null;
var looping:Bool = false;
public var ignoreExclusionPref:Array<String> = [];
/**
* Plays an animation.
* @param id A string ID of the animation to play.
* @param restart Whether to restart the animation if it is already playing.
* @param ignoreOther Whether to ignore all other animation inputs, until this one is done playing
* @param loop Whether to loop the animation
* @param startFrame The frame to start the animation on
* NOTE: `loop` and `ignoreOther` are not compatible with each other!
*/
public function playAnimation(id:String, restart:Bool = false, ignoreOther:Bool = false, loop:Bool = false, startFrame:Int = 0):Void
{
// Skip if not allowed to play animations.
if ((!canPlayOtherAnims))
{
if (this.currentAnimation == id && restart) {}
else if (ignoreExclusionPref != null && ignoreExclusionPref.length > 0)
{
var detected:Bool = false;
for (entry in ignoreExclusionPref)
{
if (StringTools.startsWith(id, entry))
{
detected = true;
break;
}
}
if (!detected) return;
}
else
return;
}
if (anim == null) return;
if (id == null || id == '') id = this.currentAnimation;
if (this.currentAnimation == id && !restart)
{
if (!anim.isPlaying)
{
if (fr != null) anim.curFrame = fr.index + startFrame;
else
anim.curFrame = startFrame;
// Resume animation if it's paused.
anim.resume();
}
return;
}
else if (!hasAnimation(id))
{
// Skip if the animation doesn't exist
trace('Animation ' + id + ' not found');
return;
}
this.currentAnimation = id;
anim.onComplete.removeAll();
anim.onComplete.add(function() {
_onAnimationComplete();
});
looping = loop;
// Prevent other animations from playing if `ignoreOther` is true.
if (ignoreOther) canPlayOtherAnims = false;
// Move to the first frame of the animation.
// goToFrameLabel(id);
// trace('Playing animation $id');
if ((id == null || id == "") || this.anim.symbolDictionary.exists(id) || (this.anim.getByName(id) != null))
{
this.anim.play(id, restart, false, startFrame);
this.currentAnimation = anim.curSymbol.name;
fr = null;
}
var frameLabelNames = getFrameLabelNames();
// Only call goToFrameLabel if there is a frame label with that name. This prevents annoying warnings!
if (frameLabelNames != null && frameLabelNames.indexOf(id) != -1)
{
goToFrameLabel(id);
fr = anim.getFrameLabel(id);
anim.curFrame += startFrame;
// Resume animation if it's paused.
anim.resume();
}
}
override public function update(elapsed:Float):Void
{
super.update(elapsed);
}
/**
* Returns true if the animation has finished playing.
* @return Whether the animation has finished playing.
*/
public function isAnimationFinished():Bool
{
return isLoopComplete();
}
/**
* Returns true if the animation has reached the last frame.
* Can be true even if animation is configured to loop.
* @return Whether the animation has reached the last frame.
*/
public function isLoopComplete():Bool
{
if (this.anim == null) return false;
if (!this.anim.isPlaying) return false;
if (fr != null)
{
var curFrame = anim.curFrame;
var startFrame = fr.index;
var endFrame = (fr.index + fr.duration);
return (anim.reversed) ? (curFrame < startFrame) : (curFrame >= endFrame);
}
return (anim.reversed && anim.curFrame == 0 || !(anim.reversed) && (anim.curFrame) >= (anim.length - 1));
}
/**
* Stops the current animation.
*/
public function stopAnimation():Void
{
if (this.currentAnimation == null) return;
this.anim.removeAllCallbacksFrom(getNextFrameLabel(this.currentAnimation));
goToFrameIndex(0);
}
function addFrameCallback(label:String, callback:Void->Void):Void
{
var frameLabel = this.anim.getFrameLabel(label);
frameLabel.add(callback);
}
function goToFrameLabel(label:String):Void
{
this.anim.goToFrameLabel(label);
}
function getFrameLabelNames(?layer:haxe.extern.EitherType<Int, String>):Null<Array<String>>
{
var labels = this.anim.getFrameLabels(layer);
var array = [];
for (label in labels)
{
array.push(label.name);
}
return array;
}
function getNextFrameLabel(label:String):String
{
return listAnimations()[(getLabelIndex(label) + 1) % listAnimations().length];
}
function getLabelIndex(label:String):Int
{
return listAnimations().indexOf(label);
}
function goToFrameIndex(index:Int):Void
{
this.anim.curFrame = index;
}
public function cleanupAnimation(_:String):Void
{
canPlayOtherAnims = true;
// this.currentAnimation = null;
this.anim.pause();
}
function _onAnimationFrame(frame:Int):Void
{
if (currentAnimation != null)
{
onAnimationFrame.dispatch(currentAnimation, frame);
if (isLoopComplete())
{
anim.pause();
if (looping)
{
anim.curFrame = (fr != null) ? fr.index : 0;
anim.resume();
_onAnimationLoop();
}
else if (fr != null && anim.curFrame != anim.length - 1)
{
anim.curFrame--;
_onAnimationComplete();
}
}
}
}
function _onAnimationComplete():Void
{
if (currentAnimation != null)
{
onAnimationComplete.dispatch(currentAnimation);
}
else
{
onAnimationComplete.dispatch('');
}
}
function _onAnimationLoop():Void
{
if (currentAnimation != null)
{
onAnimationLoop.dispatch(currentAnimation);
}
else
{
onAnimationLoop.dispatch('');
}
}
var prevFrames:Map<Int, FlxFrame> = [];
public function replaceFrameGraphic(index:Int, ?graphic:FlxGraphicAsset):Void
{
var cond = false;
if (graphic == null) cond = true;
else
{
if ((graphic is String)) cond = !Assets.exists(graphic)
else
cond = false;
}
if (cond)
{
var prevFrame:Null<FlxFrame> = prevFrames.get(index);
if (prevFrame == null) return;
prevFrame.copyTo(frames.getByIndex(index));
return;
}
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));
// Additional sizing fix.
@:privateAccess
if (true)
{
var frame = frames.getByIndex(index);
frame.tileMatrix[0] = prevFrame.frame.width / frame.frame.width;
frame.tileMatrix[3] = prevFrame.frame.height / frame.frame.height;
}
}
public function getBasePosition():Null<FlxPoint>
{
// var stagePos = new FlxPoint(anim.stageInstance.matrix.tx, anim.stageInstance.matrix.ty);
var instancePos = new FlxPoint(anim.curInstance.matrix.tx, anim.curInstance.matrix.ty);
var firstElement = anim.curSymbol.timeline?.get(0)?.get(0)?.get(0);
if (firstElement == null) return instancePos;
var firstElementPos = new FlxPoint(firstElement.matrix.tx, firstElement.matrix.ty);
return instancePos + firstElementPos;
}
public function getPivotPosition():Null<FlxPoint>
{
return anim.curInstance.symbol.transformationPoint;
}
public override function destroy():Void
{
for (prevFrameId in prevFrames.keys())
{
replaceFrameGraphic(prevFrameId, null);
}
super.destroy();
}
}
@@ -68,7 +68,7 @@ class FrameBuffer
{
camera.clearDrawStack();
camera.canvas.graphics.clear();
camera.fill(camera.bgColor.to24Bit(), camera.useBgAlphaBlending, camera.bgColor.alphaFloat);
camera.fill(camera.bgColor.rgb, camera.useBgAlphaBlending, camera.bgColor.alphaFloat);
#if FLX_DEBUG
camera.debugLayer.graphics.clear();
#end
@@ -70,6 +70,11 @@ class MeshRender extends FlxStrip
add_tri(a, c, d);
}
public override function draw():Void
{
super.draw();
}
/**
* Build a quad from four points.
*
@@ -1,230 +0,0 @@
package funkin.graphics.shaders;
import flixel.system.FlxAssets.FlxShader;
import flixel.util.FlxColor;
import flixel.FlxSprite;
import flixel.math.FlxAngle;
import flixel.graphics.frames.FlxFrame;
import openfl.display.BitmapData;
/*
A screenspace version of the DropShadowShader.. currently the only way to use this effect with
FlxAnimate :(
*/
class DropShadowScreenspace extends DropShadowShader
{
/*
The current zoom of the camera. Needed to figure out how much to multiply the drop shadow size.
*/
public var curZoom(default, set):Float;
function set_curZoom(val:Float):Float
{
curZoom = val;
zoom.value = [val];
return val;
}
@:glFragmentSource('
#pragma header
// This shader aims to mostly recreate how Adobe Animate/Flash handles drop shadows, but its main use here is for rim lighting.
// this shader also includes a recreation of the Animate/Flash "Adjust Color" filter,
// which was kindly provided and written by Rozebud https://github.com/ThatRozebudDude ( thank u rozebud :) )
// Adapted from Andrey-Postelzhuks shader found here: https://forum.unity.com/threads/hue-saturation-brightness-contrast-shader.260649/
// Hue rotation stuff is from here: https://www.w3.org/TR/filter-effects/#feColorMatrixElement
// equals (frame.left, frame.top, frame.right, frame.bottom)
uniform vec4 uFrameBounds;
uniform float ang;
uniform float dist;
uniform float str;
uniform float thr;
// need to account for rotated frames... oops
uniform float angOffset;
uniform sampler2D altMask;
uniform bool useMask;
uniform float thr2;
uniform vec3 dropColor;
uniform float hue;
uniform float saturation;
uniform float brightness;
uniform float contrast;
uniform float zoom;
uniform float AA_STAGES;
const vec3 grayscaleValues = vec3(0.3098039215686275, 0.607843137254902, 0.0823529411764706);
const float e = 2.718281828459045;
vec3 applyHueRotate(vec3 aColor, float aHue){
float angle = radians(aHue);
mat3 m1 = mat3(0.213, 0.213, 0.213, 0.715, 0.715, 0.715, 0.072, 0.072, 0.072);
mat3 m2 = mat3(0.787, -0.213, -0.213, -0.715, 0.285, -0.715, -0.072, -0.072, 0.928);
mat3 m3 = mat3(-0.213, 0.143, -0.787, -0.715, 0.140, 0.715, 0.928, -0.283, 0.072);
mat3 m = m1 + cos(angle) * m2 + sin(angle) * m3;
return m * aColor;
}
vec3 applySaturation(vec3 aColor, float value){
if(value > 0.0){ value = value * 3.0; }
value = (1.0 + (value / 100.0));
vec3 grayscale = vec3(dot(aColor, grayscaleValues));
return clamp(mix(grayscale, aColor, value), 0.0, 1.0);
}
vec3 applyContrast(vec3 aColor, float value){
value = (1.0 + (value / 100.0));
if(value > 1.0){
value = (((0.00852259 * pow(e, 4.76454 * (value - 1.0))) * 1.01) - 0.0086078159) * 10.0; //Just roll with it...
value += 1.0;
}
return clamp((aColor - 0.25) * value + 0.25, 0.0, 1.0);
}
vec3 applyHSBCEffect(vec3 color){
//Brightness
color = color + ((brightness) / 255.0);
//Hue
color = applyHueRotate(color, hue);
//Contrast
color = applyContrast(color, contrast);
//Saturation
color = applySaturation(color, saturation);
return color;
}
vec2 hash22(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * vec3(.1031, .1030, .0973));
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.xx + p3.yz) * p3.zy);
}
float intensityPass(vec2 fragCoord, float curThreshold, bool useMask) {
vec4 col = texture2D(bitmap, fragCoord);
float maskIntensity = 0.0;
if(useMask == true){
maskIntensity = mix(0.0, 1.0, texture2D(altMask, fragCoord).b);
}
if(col.a == 0.0){
return 0.0;
}
float intensity = dot(col.rgb, vec3(0.3098, 0.6078, 0.0823));
intensity = maskIntensity > 0.0 ? float(intensity > thr2) : float(intensity > thr);
return intensity;
}
// essentially just stole this from the AngleMask shader but repurposed it to smooth
// the threshold because without any sort of smoothing it produces horrible edges
float antialias(vec2 fragCoord, float curThreshold, bool useMask) {
if (AA_STAGES == 0.0) {
return intensityPass(fragCoord, curThreshold, useMask);
}
// In GLSL 100, we need to use constant loop bounds
// Well assume a reasonable maximum for AA_STAGES and use a fixed loop
// The actual number of iterations will be controlled by a condition inside
const int MAX_AA = 8; // This should be large enough for most uses
float AA_TOTAL_PASSES = AA_STAGES * AA_STAGES + 1.0;
const float AA_JITTER = 0.5;
// Run the shader multiple times with a random subpixel offset each time and average the results
float color = intensityPass(fragCoord, curThreshold, useMask);
for (int i = 0; i < MAX_AA * MAX_AA; i++) {
// Calculate x and y from i
int x = i / MAX_AA;
int y = i - (MAX_AA * int(i/MAX_AA)); // poor mans modulus
// Skip iterations beyond our desired AA_STAGES
if (float(x) >= AA_STAGES || float(y) >= AA_STAGES) {
continue;
}
vec2 offset = AA_JITTER * (2.0 * hash22(vec2(float(x), float(y))) - 1.0) / openfl_TextureSize.xy;
color += intensityPass(fragCoord + offset, curThreshold, useMask);
}
return color / AA_TOTAL_PASSES;
}
vec3 createDropShadow(vec3 col, float curThreshold, bool useMask) {
// essentially a mask so that areas under the threshold dont show the rimlight (mainly the outlines)
float intensity = antialias(openfl_TextureCoordv, curThreshold, useMask);
// the distance the dropshadow moves needs to be correctly scaled based on the texture size
vec2 imageRatio = vec2(1.0/openfl_TextureSize.x, 1.0/openfl_TextureSize.y);
// check the pixel in the direction and distance specified
vec2 checkedPixel = vec2(openfl_TextureCoordv.x + ((dist*zoom) * cos(ang + angOffset) * imageRatio.x), openfl_TextureCoordv.y - ((dist*zoom) * sin(ang + angOffset) * imageRatio.y));
// multiplier for the intensity of the drop shadow
float dropShadowAmount = 0.0;
dropShadowAmount = texture2D(bitmap, checkedPixel).a;
// add the dropshadow color based on the amount, strength, and intensity
col.rgb += dropColor.rgb * ((1.0 - (dropShadowAmount * str))*intensity);
return col;
}
void main()
{
vec4 col = texture2D(bitmap, openfl_TextureCoordv);
vec3 unpremultipliedColor = col.a > 0.0 ? col.rgb / col.a : col.rgb;
vec3 outColor = applyHSBCEffect(unpremultipliedColor);
outColor = createDropShadow(outColor, thr, useMask);
gl_FragColor = vec4(outColor.rgb * col.a, col.a);
}
')
override public function new()
{
super();
angle = 90;
strength = 1;
distance = 15;
threshold = 0.1;
baseHue = 0;
baseSaturation = 0;
baseBrightness = 0;
baseContrast = 0;
useAltMask = false;
color = 0xFFDFEF3C;
antialiasAmt = 2;
curZoom = 1;
angOffset.value = [0];
}
}

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