diff --git a/.vscode/settings.json b/.vscode/settings.json index 0685989de52..b0e6606dffa 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -150,6 +150,16 @@ "target": "windows", "args": ["-debug", "-DRESULTS"] }, + { + "label": "Windows / Debug (Straight to Stage Editor)", + "target": "windows", + "args": ["-debug", "-DSTAGING", "-DFEATURE_DEBUG_FUNCTIONS"] + }, + { + "label": "Windows / Debug (Straight to Stage Builder)", + "target": "windows", + "args": ["-debug", "-DSTAGEBUILD", "-DFEATURE_DEBUG_FUNCTIONS"] + }, { "label": "Windows / Debug (Straight to Animation Editor)", "target": "windows", diff --git a/assets b/assets index c108a7ff0d1..a3e5c317935 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit c108a7ff0d11bf328e7b232160b8f68c71e21bca +Subproject commit a3e5c31793505e7e04f1bbd042b9cda96a18f760 diff --git a/project.hxp b/project.hxp index e0ba25c5d2e..3691ac283e2 100644 --- a/project.hxp +++ b/project.hxp @@ -15,56 +15,57 @@ using StringTools; * and improves management of feature flag logic. */ @:nullSafety -class Project extends HXProject { - // - // METADATA - // - - /** - * The game's version number, as a Semantic Versioning string with no prefix. - * REMEMBER TO CHANGE THIS WHEN THE GAME UPDATES! - * You only have to change it here, the rest of the game will query this value. - */ - static final VERSION:String = "0.6.4"; - - /** - * The game's name. Used as the default window title. - */ - static final TITLE:String = "Friday Night Funkin'"; - - /** - * The name of the generated executable file. - * For example, `"Funkin"` will create a file called `Funkin.exe`. - */ - static final EXECUTABLE_NAME:String = "Funkin"; - - /** - * The relative location of the source code. - */ - static final SOURCE_DIR:String = "source"; - - /** - * The fully qualified class path for the game's preloader. - * Particularly important on HTML5 but we use it on all platforms. - */ - static final PRELOADER:String = "funkin.ui.transition.preload.FunkinPreloader"; - - /** - * A package name used for identifying the app on various app stores. - */ - static final PACKAGE_NAME:String = "me.funkin.fnf"; - - /** - * The fully qualified class path for the entry point class to execute when launching the game. - * It's where `public static function main():Void` goes. - */ - static final MAIN_CLASS:String = "Main"; - - /** - * The company name for the game. - * This appears in metadata in places I think. - */ - static final COMPANY:String = "The Funkin' Crew"; +class Project extends HXProject +{ + // + // METADATA + // + + /** + * The game's version number, as a Semantic Versioning string with no prefix. + * REMEMBER TO CHANGE THIS WHEN THE GAME UPDATES! + * You only have to change it here, the rest of the game will query this value. + */ + static final VERSION:String = "0.6.4"; + + /** + * The game's name. Used as the default window title. + */ + static final TITLE:String = "Friday Night Funkin'"; + + /** + * The name of the generated executable file. + * For example, `"Funkin"` will create a file called `Funkin.exe`. + */ + static final EXECUTABLE_NAME:String = "Funkin"; + + /** + * The relative location of the source code. + */ + static final SOURCE_DIR:String = "source"; + + /** + * The fully qualified class path for the game's preloader. + * Particularly important on HTML5 but we use it on all platforms. + */ + static final PRELOADER:String = "funkin.ui.transition.preload.FunkinPreloader"; + + /** + * A package name used for identifying the app on various app stores. + */ + static final PACKAGE_NAME:String = "me.funkin.fnf"; + + /** + * The fully qualified class path for the entry point class to execute when launching the game. + * It's where `public static function main():Void` goes. + */ + static final MAIN_CLASS:String = "Main"; + + /** + * The company name for the game. + * This appears in metadata in places I think. + */ + static final COMPANY:String = "The Funkin' Crew"; /** * Path to the Haxe script run before building the game. @@ -76,63 +77,64 @@ class Project extends HXProject { */ static final POSTBUILD_HX:String = "source/Postbuild.hx"; - /** - * Asset path globs to always exclude from asset libraries. - */ - static final EXCLUDE_ASSETS:Array = [".*", "cvs", "thumbs.db", "desktop.ini", "*.hash", "*.md"]; - - /** - * Asset path globs to exclude on web platforms. - */ - static final EXCLUDE_ASSETS_WEB:Array = ["*.ogg"]; - /** - * Asset path globs to exclude on native platforms. - */ - static final EXCLUDE_ASSETS_NATIVE:Array = ["*.mp3"]; - - // - // FEATURE FLAGS - // Inverse feature flags are automatically populated. - // - - /** - * `-DGITHUB_BUILD` - * If this flag is enabled, the game will use the configuration used by GitHub Actions - * to generate playtest builds to be pushed to the launcher. - * - * This is generally used to forcibly enable debugging features, - * even when the game is built in release mode for performance reasons. - */ - static final GITHUB_BUILD:FeatureFlag = "GITHUB_BUILD"; - - /** - * `-DREDIRECT_ASSETS_FOLDER` - * If this flag is enabled, the game will redirect the `assets` folder from the `export` folder - * to the `assets` folder at the root of the workspace. - * This is useful for ensuring hot reloaded changes don't get lost when rebuilding the game. - */ - static final REDIRECT_ASSETS_FOLDER:FeatureFlag = "REDIRECT_ASSETS_FOLDER"; - - /** - * `-DTOUCH_HERE_TO_PLAY` - * If this flag is enabled, the game will display a prompt to the user after the preloader completes, - * requiring them to click anywhere on the screen to start the game. - * This is done to ensure that the audio context can initialize properly on HTML5. Not necessary on desktop. - */ - static final TOUCH_HERE_TO_PLAY:FeatureFlag = "TOUCH_HERE_TO_PLAY"; - - /** - * `-DPRELOAD_ALL` - * Whether to preload all asset libraries. - * Disabled on web, enabled on desktop. - */ - static final PRELOAD_ALL:FeatureFlag = "PRELOAD_ALL"; - - /** - * `-DEMBED_ASSETS` - * Whether to embed all asset libraries into the executable. - */ - static final EMBED_ASSETS:FeatureFlag = "EMBED_ASSETS"; + /** + * Asset path globs to always exclude from asset libraries. + */ + static final EXCLUDE_ASSETS:Array = [".*", "cvs", "thumbs.db", "desktop.ini", "*.hash", "*.md"]; + + /** + * Asset path globs to exclude on web platforms. + */ + static final EXCLUDE_ASSETS_WEB:Array = ["*.ogg"]; + + /** + * Asset path globs to exclude on native platforms. + */ + static final EXCLUDE_ASSETS_NATIVE:Array = ["*.mp3"]; + + // + // FEATURE FLAGS + // Inverse feature flags are automatically populated. + // + + /** + * `-DGITHUB_BUILD` + * If this flag is enabled, the game will use the configuration used by GitHub Actions + * to generate playtest builds to be pushed to the launcher. + * + * This is generally used to forcibly enable debugging features, + * even when the game is built in release mode for performance reasons. + */ + static final GITHUB_BUILD:FeatureFlag = "GITHUB_BUILD"; + + /** + * `-DREDIRECT_ASSETS_FOLDER` + * If this flag is enabled, the game will redirect the `assets` folder from the `export` folder + * to the `assets` folder at the root of the workspace. + * This is useful for ensuring hot reloaded changes don't get lost when rebuilding the game. + */ + static final REDIRECT_ASSETS_FOLDER:FeatureFlag = "REDIRECT_ASSETS_FOLDER"; + + /** + * `-DTOUCH_HERE_TO_PLAY` + * If this flag is enabled, the game will display a prompt to the user after the preloader completes, + * requiring them to click anywhere on the screen to start the game. + * This is done to ensure that the audio context can initialize properly on HTML5. Not necessary on desktop. + */ + static final TOUCH_HERE_TO_PLAY:FeatureFlag = "TOUCH_HERE_TO_PLAY"; + + /** + * `-DPRELOAD_ALL` + * Whether to preload all asset libraries. + * Disabled on web, enabled on desktop. + */ + static final PRELOAD_ALL:FeatureFlag = "PRELOAD_ALL"; + + /** + * `-DEMBED_ASSETS` + * Whether to embed all asset libraries into the executable. + */ + static final EMBED_ASSETS:FeatureFlag = "EMBED_ASSETS"; /** * `-DHARDCODED_CREDITS` @@ -141,538 +143,574 @@ class Project extends HXProject { */ static final HARDCODED_CREDITS:FeatureFlag = "HARDCODED_CREDITS"; - /** - * `-DFEATURE_DEBUG_FUNCTIONS` - * If this flag is enabled, the game will have all playtester-only debugging functionality enabled. - * This includes debug hotkeys like time travel in the Play State. - * By default, enabled on debug builds or playtester builds and disabled on release builds. - */ - static final FEATURE_DEBUG_FUNCTIONS:FeatureFlag = "FEATURE_DEBUG_FUNCTIONS"; - - /** - * `-DFEATURE_DEBUG_TRACY` - * If this flag is enabled, the game will have the necessary hooks for the Tracy profiler. - * Only enable this if you're using the correct fork of Haxe to support this. - * @see https://github.com/HaxeFoundation/hxcpp/pull/1153 - */ - static final FEATURE_DEBUG_TRACY:FeatureFlag = "FEATURE_DEBUG_TRACY"; - - /** - * `-DFEATURE_LOG_TRACE` - * If this flag is enabled, the game will print debug traces to the console. - * Disable to improve performance a bunch. - */ - static final FEATURE_LOG_TRACE:FeatureFlag = "FEATURE_LOG_TRACE"; - - /** - * `-DFEATURE_DISCORD_RPC` - * If this flag is enabled, the game will enable the Discord Remote Procedure Call library. - * This is used to provide Discord Rich Presence support. - */ - static final FEATURE_DISCORD_RPC:FeatureFlag = "FEATURE_DISCORD_RPC"; - - /** - * `-DFEATURE_NEWGROUNDS` - * If this flag is enabled, the game will enable the Newgrounds library. - * This is used to provide Medal and Leaderboard support. - */ - static final FEATURE_NEWGROUNDS:FeatureFlag = "FEATURE_NEWGROUNDS"; - - /** - * `-DFEATURE_NEWGROUNDS_DEBUG` - * If this flag is enabled, the game will enable Newgrounds.io's debug functions. - * This provides additional information in requests, as well as "faking" medal and leaderboard submissions. - */ - static final FEATURE_NEWGROUNDS_DEBUG:FeatureFlag = "FEATURE_NEWGROUNDS_DEBUG"; - - /** - * `-DFEATURE_NEWGROUNDS_AUTOLOGIN` - * If this flag is enabled, the game will attempt to automatically login to Newgrounds on startup. - */ - static final FEATURE_NEWGROUNDS_AUTOLOGIN:FeatureFlag = "FEATURE_NEWGROUNDS_AUTOLOGIN"; - - /** - * `-DFEATURE_NEWGROUNDS_TESTING_MEDALS` - * If this flag is enabled, use the medal IDs from the debug test bench. - * If disabled, use the actual medal IDs from the release project on Newgrounds. - */ - static final FEATURE_NEWGROUNDS_TESTING_MEDALS:FeatureFlag = "FEATURE_NEWGROUNDS_TESTING_MEDALS"; - - /** - * `-DFEATURE_NEWGROUNDS_EVENTS` - * If this flag is enabled, the game will attempt to send events to Newgrounds when the user does stuff. - * This lets us see cool anonymized stats! It only works if the user is logged in. - */ - static final FEATURE_NEWGROUNDS_EVENTS:FeatureFlag = "FEATURE_NEWGROUNDS_EVENTS"; - - /** - * `-DFEATURE_FUNKVIS` - * If this flag is enabled, the game will enable the Funkin Visualizer library. - * This is used to provide audio visualization like Nene's speaker. - * Disabling this will make some waveforms inactive. - */ - static final FEATURE_FUNKVIS:FeatureFlag = "FEATURE_FUNKVIS"; - - /** - * `-DFEATURE_PARTIAL_SOUNDS` - * If this flag is enabled, the game will enable the FlxPartialSound library. - * This is used to provide audio previews in Freeplay. - * Disabling this will make those previews not play. - */ - static final FEATURE_PARTIAL_SOUNDS:FeatureFlag = "FEATURE_PARTIAL_SOUNDS"; - - /** - * `-DFEATURE_VIDEO_PLAYBACK` - * If this flag is enabled, the game will enable support for video playback. - * This requires the hxvlc library on desktop platforms. - */ - static final FEATURE_VIDEO_PLAYBACK:FeatureFlag = "FEATURE_VIDEO_PLAYBACK"; - - /** - * `-DFEATURE_FILE_DROP` + /** + * `-DFEATURE_DEBUG_FUNCTIONS` + * If this flag is enabled, the game will have all playtester-only debugging functionality enabled. + * This includes debug hotkeys like time travel in the Play State. + * By default, enabled on debug builds or playtester builds and disabled on release builds. + */ + static final FEATURE_DEBUG_FUNCTIONS:FeatureFlag = "FEATURE_DEBUG_FUNCTIONS"; + + /** + * `-DFEATURE_DEBUG_TRACY` + * If this flag is enabled, the game will have the necessary hooks for the Tracy profiler. + * Only enable this if you're using the correct fork of Haxe to support this. + * @see https://github.com/HaxeFoundation/hxcpp/pull/1153 + */ + static final FEATURE_DEBUG_TRACY:FeatureFlag = "FEATURE_DEBUG_TRACY"; + + /** + * `-DFEATURE_LOG_TRACE` + * If this flag is enabled, the game will print debug traces to the console. + * Disable to improve performance a bunch. + */ + static final FEATURE_LOG_TRACE:FeatureFlag = "FEATURE_LOG_TRACE"; + + /** + * `-DFEATURE_DISCORD_RPC` + * If this flag is enabled, the game will enable the Discord Remote Procedure Call library. + * This is used to provide Discord Rich Presence support. + */ + static final FEATURE_DISCORD_RPC:FeatureFlag = "FEATURE_DISCORD_RPC"; + + /** + * `-DFEATURE_NEWGROUNDS` + * If this flag is enabled, the game will enable the Newgrounds library. + * This is used to provide Medal and Leaderboard support. + */ + static final FEATURE_NEWGROUNDS:FeatureFlag = "FEATURE_NEWGROUNDS"; + + /** + * `-DFEATURE_NEWGROUNDS_DEBUG` + * If this flag is enabled, the game will enable Newgrounds.io's debug functions. + * This provides additional information in requests, as well as "faking" medal and leaderboard submissions. + */ + static final FEATURE_NEWGROUNDS_DEBUG:FeatureFlag = "FEATURE_NEWGROUNDS_DEBUG"; + + /** + * `-DFEATURE_NEWGROUNDS_AUTOLOGIN` + * If this flag is enabled, the game will attempt to automatically login to Newgrounds on startup. + */ + static final FEATURE_NEWGROUNDS_AUTOLOGIN:FeatureFlag = "FEATURE_NEWGROUNDS_AUTOLOGIN"; + + /** + * `-DFEATURE_NEWGROUNDS_TESTING_MEDALS` + * If this flag is enabled, use the medal IDs from the debug test bench. + * If disabled, use the actual medal IDs from the release project on Newgrounds. + */ + static final FEATURE_NEWGROUNDS_TESTING_MEDALS:FeatureFlag = "FEATURE_NEWGROUNDS_TESTING_MEDALS"; + + /** + * `-DFEATURE_NEWGROUNDS_EVENTS` + * If this flag is enabled, the game will attempt to send events to Newgrounds when the user does stuff. + * This lets us see cool anonymized stats! It only works if the user is logged in. + */ + static final FEATURE_NEWGROUNDS_EVENTS:FeatureFlag = "FEATURE_NEWGROUNDS_EVENTS"; + + /** + * `-DFEATURE_FUNKVIS` + * If this flag is enabled, the game will enable the Funkin Visualizer library. + * This is used to provide audio visualization like Nene's speaker. + * Disabling this will make some waveforms inactive. + */ + static final FEATURE_FUNKVIS:FeatureFlag = "FEATURE_FUNKVIS"; + + /** + * `-DFEATURE_PARTIAL_SOUNDS` + * If this flag is enabled, the game will enable the FlxPartialSound library. + * This is used to provide audio previews in Freeplay. + * Disabling this will make those previews not play. + */ + static final FEATURE_PARTIAL_SOUNDS:FeatureFlag = "FEATURE_PARTIAL_SOUNDS"; + + /** + * `-DFEATURE_VIDEO_PLAYBACK` + * If this flag is enabled, the game will enable support for video playback. + * This requires the hxvlc library on desktop platforms. + */ + static final FEATURE_VIDEO_PLAYBACK:FeatureFlag = "FEATURE_VIDEO_PLAYBACK"; + + /** + * `-DFEATURE_FILE_DROP` * If this flag is enabled, the game will support dragging and dropping files onto it for various features. * Disabled on MacOS. */ static final FEATURE_FILE_DROP:FeatureFlag = "FEATURE_FILE_DROP"; /** - * `-DFEATURE_OPEN_URL` + * `-DFEATURE_OPEN_URL` * If this flag is enabled, the game will support opening URLs (such as the merch page). */ static final FEATURE_OPEN_URL:FeatureFlag = "FEATURE_OPEN_URL"; /** - * `-DFEATURE_SCREENSHOTS` + * `-DFEATURE_SCREENSHOTS` * If this flag is enabled, the game will support the screenshots feature. */ static final FEATURE_SCREENSHOTS:FeatureFlag = "FEATURE_SCREENSHOTS"; - /** + /** * `-DFEATURE_CHART_EDITOR` - * If this flag is enabled, the Chart Editor will be accessible from the debug menu. - */ - static final FEATURE_CHART_EDITOR:FeatureFlag = "FEATURE_CHART_EDITOR"; + * If this flag is enabled, the Chart Editor will be accessible from the debug menu. + */ + static final FEATURE_CHART_EDITOR:FeatureFlag = "FEATURE_CHART_EDITOR"; - /** - * `-DFEATURE_STAGE_EDITOR` + /** + * `-DFEATURE_STAGE_EDITOR` * If this flag is enabled, the Stage Editor will be accessible from the debug menu. - */ + */ static final FEATURE_STAGE_EDITOR:FeatureFlag = "FEATURE_STAGE_EDITOR"; - /** - * `-DFEATURE_RESULTS_DEBUG - * If this flag is enabled, a debug menu for Results screen will be accessible from the debug menu. - */ - static final FEATURE_RESULTS_DEBUG:FeatureFlag = "FEATURE_RESULTS_DEBUG"; - - /** - * `-DFEATURE_POLYMOD_MODS` - * If this flag is enabled, the game will enable the Polymod library's support for atomic mod loading from the `./mods` folder. - * If this flag is disabled, no mods will be loaded. - */ - static final FEATURE_POLYMOD_MODS:FeatureFlag = "FEATURE_POLYMOD_MODS"; - - /** - * `-DFEATURE_GHOST_TAPPING` - * If this flag is enabled, misses will not be counted when it is not the player's turn. - * Misses are still counted when the player has notes to hit. - */ - static final FEATURE_GHOST_TAPPING:FeatureFlag = "FEATURE_GHOST_TAPPING"; - - // - // CONFIGURATION FUNCTIONS - // - - public function new() { - super(); - - flair(); - configureApp(); - - displayTarget(); - configureFeatureFlags(); + /** + * `-DFEATURE_RESULTS_DEBUG + * If this flag is enabled, a debug menu for Results screen will be accessible from the debug menu. + */ + static final FEATURE_RESULTS_DEBUG:FeatureFlag = "FEATURE_RESULTS_DEBUG"; + + /** + * `-DFEATURE_POLYMOD_MODS` + * If this flag is enabled, the game will enable the Polymod library's support for atomic mod loading from the `./mods` folder. + * If this flag is disabled, no mods will be loaded. + */ + static final FEATURE_POLYMOD_MODS:FeatureFlag = "FEATURE_POLYMOD_MODS"; + + /** + * `-DFEATURE_GHOST_TAPPING` + * If this flag is enabled, misses will not be counted when it is not the player's turn. + * Misses are still counted when the player has notes to hit. + */ + static final FEATURE_GHOST_TAPPING:FeatureFlag = "FEATURE_GHOST_TAPPING"; + + // + // CONFIGURATION FUNCTIONS + // + + public function new() + { + super(); + + flair(); + configureApp(); + + displayTarget(); + configureFeatureFlags(); configureCompileDefines(); - configureIncludeMacros(); - configureCustomMacros(); + configureIncludeMacros(); + configureCustomMacros(); configureOutputDir(); configurePolymod(); configureHaxelibs(); configureAssets(); configureIcons(); - } - - /** - * Do something before building, display some ASCII or something IDK - */ - function flair() { - // TODO: Implement this. - info("Friday Night Funkin'"); - info("Initializing build..."); - - info("Target Version: " + VERSION); - info("Git Branch: " + getGitBranch()); - info("Git Commit: " + getGitCommit()); - info("Git Modified? " + getGitModified()); + } + + /** + * Do something before building, display some ASCII or something IDK + */ + function flair() + { + // TODO: Implement this. + info("Friday Night Funkin'"); + info("Initializing build..."); + + info("Target Version: " + VERSION); + info("Git Branch: " + getGitBranch()); + info("Git Commit: " + getGitCommit()); + info("Git Modified? " + getGitModified()); info("Display? " + isDisplay()); - } - - /** - * Apply basic project metadata, such as the game title and version number, - * as well as info like the package name and company (used by various app stores). - */ - function configureApp() { - this.meta.title = TITLE; - this.meta.version = VERSION; - this.meta.packageName = PACKAGE_NAME; - this.meta.company = COMPANY; - - this.app.main = MAIN_CLASS; - this.app.file = EXECUTABLE_NAME; - this.app.preloader = PRELOADER; - - // Tell Lime where to look for the game's source code. - // If for some reason we have multiple source directories, we can add more entries here. - this.sources.push(SOURCE_DIR); + } + + /** + * Apply basic project metadata, such as the game title and version number, + * as well as info like the package name and company (used by various app stores). + */ + function configureApp() + { + this.meta.title = TITLE; + this.meta.version = VERSION; + this.meta.packageName = PACKAGE_NAME; + this.meta.company = COMPANY; + + this.app.main = MAIN_CLASS; + this.app.file = EXECUTABLE_NAME; + this.app.preloader = PRELOADER; + + // Tell Lime where to look for the game's source code. + // If for some reason we have multiple source directories, we can add more entries here. + this.sources.push(SOURCE_DIR); // Tell Lime to run some prebuild and postbuild scripts. this.preBuildCallbacks.push(buildHaxeCLICommand(PREBUILD_HX)); this.postBuildCallbacks.push(buildHaxeCLICommand(POSTBUILD_HX)); - // TODO: Should we provide this? - // this.meta.buildNumber = 0; - - // These values are only used by the SWF target I think. - // this.app.path - // this.app.init - // this.app.swfVersion - // this.app.url - - // These values are only used by... FIREFOX MARKETPLACE WHAT? - // this.meta.description = ""; - // this.meta.companyId = COMPANY; - // this.meta.companyUrl = COMPANY; - - // Configure the window. - // Automatically configure FPS. - this.window.fps = 60; - // Set the window size. - this.window.width = 1280; - this.window.height = 720; - // Black background on release builds, magenta on debug builds. - this.window.background = FEATURE_DEBUG_FUNCTIONS.isEnabled(this) ? 0xFFFF00FF : 0xFF000000; - - this.window.hardware = true; - this.window.vsync = false; - // force / allow high DPI - this.window.allowHighDPI = true; - - if (isWeb()) { - this.window.resizable = true; - } - - if (isDesktop()) { - this.window.orientation = Orientation.LANDSCAPE; - this.window.fullscreen = false; - this.window.resizable = true; - this.window.vsync = false; - } - - if (isMobile()) { - this.window.orientation = Orientation.LANDSCAPE; - this.window.fullscreen = false; - this.window.resizable = false; - this.window.width = 0; - this.window.height = 0; - } - } - - /** - * Log information about the configured target platform. - */ - function displayTarget() { - // Display the target operating system. - switch (this.target) { - case Platform.WINDOWS: - info('Target Platform: Windows'); - case Platform.MAC: - info('Target Platform: MacOS'); - case Platform.LINUX: - info('Target Platform: Linux'); - case Platform.ANDROID: - info('Target Platform: Android'); - case Platform.IOS: - info('Target Platform: IOS'); - case Platform.HTML5: - info('Target Platform: HTML5'); - // See lime.tools.Platform for a full list. - // case Platform.EMSCRITEN: // A WebAssembly build might be interesting... - // case Platform.AIR: - // case Platform.BLACKBERRY: - // case Platform.CONSOLE_PC: - // case Platform.FIREFOX: - // case Platform.FLASH: - // case Platform.PS3: - // case Platform.PS4: - // case Platform.TIZEN: - // case Platform.TVOS: - // case Platform.VITA: - // case Platform.WEBOS: - // case Platform.WIIU: - // case Platform.XBOX1: - default: - error('Unsupported platform (got ${target})'); - } - - switch (this.platformType) { - case PlatformType.DESKTOP: - info('Platform Type: Desktop'); - case PlatformType.MOBILE: - info('Platform Type: Mobile'); - case PlatformType.WEB: - info('Platform Type: Web'); - case PlatformType.CONSOLE: - info('Platform Type: Console'); - default: - error('Unknown platform type (got ${platformType})'); - } + // TODO: Should we provide this? + // this.meta.buildNumber = 0; + + // These values are only used by the SWF target I think. + // this.app.path + // this.app.init + // this.app.swfVersion + // this.app.url + + // These values are only used by... FIREFOX MARKETPLACE WHAT? + // this.meta.description = ""; + // this.meta.companyId = COMPANY; + // this.meta.companyUrl = COMPANY; + + // Configure the window. + // Automatically configure FPS. + this.window.fps = 60; + // Set the window size. + this.window.width = 1280; + this.window.height = 720; + // Black background on release builds, magenta on debug builds. + this.window.background = FEATURE_DEBUG_FUNCTIONS.isEnabled(this) ? 0xFFFF00FF : 0xFF000000; + + this.window.hardware = true; + this.window.vsync = false; + // force / allow high DPI + this.window.allowHighDPI = true; + + if (isWeb()) + { + this.window.resizable = true; + } + + if (isDesktop()) + { + this.window.orientation = Orientation.LANDSCAPE; + this.window.fullscreen = false; + this.window.resizable = true; + this.window.vsync = false; + } + + if (isMobile()) + { + this.window.orientation = Orientation.LANDSCAPE; + this.window.fullscreen = false; + this.window.resizable = false; + this.window.width = 0; + this.window.height = 0; + } + } + + /** + * Log information about the configured target platform. + */ + function displayTarget() + { + // Display the target operating system. + switch (this.target) + { + case Platform.WINDOWS: + info('Target Platform: Windows'); + case Platform.MAC: + info('Target Platform: MacOS'); + case Platform.LINUX: + info('Target Platform: Linux'); + case Platform.ANDROID: + info('Target Platform: Android'); + case Platform.IOS: + info('Target Platform: IOS'); + case Platform.HTML5: + info('Target Platform: HTML5'); + // See lime.tools.Platform for a full list. + // case Platform.EMSCRITEN: // A WebAssembly build might be interesting... + // case Platform.AIR: + // case Platform.BLACKBERRY: + // case Platform.CONSOLE_PC: + // case Platform.FIREFOX: + // case Platform.FLASH: + // case Platform.PS3: + // case Platform.PS4: + // case Platform.TIZEN: + // case Platform.TVOS: + // case Platform.VITA: + // case Platform.WEBOS: + // case Platform.WIIU: + // case Platform.XBOX1: + default: + error('Unsupported platform (got ${target})'); + } + + switch (this.platformType) + { + case PlatformType.DESKTOP: + info('Platform Type: Desktop'); + case PlatformType.MOBILE: + info('Platform Type: Mobile'); + case PlatformType.WEB: + info('Platform Type: Web'); + case PlatformType.CONSOLE: + info('Platform Type: Console'); + default: + error('Unknown platform type (got ${platformType})'); + } // Print whether we are using HXCPP, HashLink, or something else. - if (isWeb()) { + if (isWeb()) + { info('Target Language: JavaScript (HTML5)'); - } else if (isHashLink()) { + } + else if (isHashLink()) + { info('Target Language: HashLink'); - } else if (isNeko()) { + } + else if (isNeko()) + { info('Target Language: Neko'); - } else if (isJava()) { + } + else if (isJava()) + { info('Target Language: Java'); - } else if (isNodeJS()) { + } + else if (isNodeJS()) + { info('Target Language: JavaScript (NodeJS)'); - } else if (isCSharp()) { + } + else if (isCSharp()) + { info('Target Language: C#'); - } else { + } + else + { info('Target Language: C++'); } - for (arch in this.architectures) { - // Display the list of target architectures. - switch (arch) { - case Architecture.X86: - info('Architecture: x86'); - case Architecture.X64: - info('Architecture: x64'); - case Architecture.ARMV5: - info('Architecture: ARMv5'); - case Architecture.ARMV6: - info('Architecture: ARMv6'); - case Architecture.ARMV7: - info('Architecture: ARMv7'); - case Architecture.ARMV7S: - info('Architecture: ARMv7S'); - case Architecture.ARM64: - info('Architecture: ARMx64'); - case Architecture.MIPS: - info('Architecture: MIPS'); - case Architecture.MIPSEL: - info('Architecture: MIPSEL'); - case null: - if (!isWeb()) { - error('Unsupported architecture (got null on non-web platform)'); - } else { - info('Architecture: Web'); - } - default: - error('Unsupported architecture (got ${arch})'); - } - } - } - - /** - * Apply various feature flags based on the target platform and the user-provided build flags. - */ - function configureFeatureFlags() { - // You can explicitly override any of these. - // For example, `-DGITHUB_BUILD` or `-DNO_HARDCODED_CREDITS` - - // Should be false unless explicitly requested. - GITHUB_BUILD.apply(this, false); - FEATURE_GHOST_TAPPING.apply(this, false); - - // Should be true unless explicitly requested. - HARDCODED_CREDITS.apply(this, true); - FEATURE_OPEN_URL.apply(this, true); - FEATURE_POLYMOD_MODS.apply(this, true); - FEATURE_FUNKVIS.apply(this, true); - FEATURE_PARTIAL_SOUNDS.apply(this, true); - FEATURE_VIDEO_PLAYBACK.apply(this, true); - FEATURE_STAGE_EDITOR.apply(this, true); - - // Should be true on debug builds or if GITHUB_BUILD is enabled. - FEATURE_DEBUG_FUNCTIONS.apply(this, isDebug() || GITHUB_BUILD.isEnabled(this)); - FEATURE_RESULTS_DEBUG.apply(this, isDebug() || GITHUB_BUILD.isEnabled(this)); - - // Got a lot of complains about this being turned off by default on some builds. - // TODO: Look into ways to optimize logging (maybe by using a thread pool?) - FEATURE_LOG_TRACE.apply(this, true); - - FEATURE_NEWGROUNDS.apply(this, true); - FEATURE_NEWGROUNDS_DEBUG.apply(this, false); - FEATURE_NEWGROUNDS_TESTING_MEDALS.apply(this, FEATURE_NEWGROUNDS.isEnabled(this) && FEATURE_DEBUG_FUNCTIONS.isEnabled(this)); - FEATURE_NEWGROUNDS_AUTOLOGIN.apply(this, FEATURE_NEWGROUNDS.isEnabled(this) && isWeb()); - FEATURE_NEWGROUNDS_EVENTS.apply(this, FEATURE_NEWGROUNDS.isEnabled(this)); - - // Should default to true on workspace builds and false on release builds. - REDIRECT_ASSETS_FOLDER.apply(this, isDebug() && isDesktop()); - - // Should be true on desktop, release, non-tester builds. - // We don't want testers to accidentally leak songs to their Discord friends! - FEATURE_DISCORD_RPC.apply(this, isDesktop() && !FEATURE_DEBUG_FUNCTIONS.isEnabled(this)); - - // Should be true only on web builds. - // Audio context issues only exist there. - TOUCH_HERE_TO_PLAY.apply(this, isWeb()); - - // Should be true only on web builds. - // Enabling embedding and preloading is required to preload assets properly. - EMBED_ASSETS.apply(this, isWeb()); - PRELOAD_ALL.apply(this, !isWeb()); - - // Should be true except on MacOS. - // File drop doesn't work there. - FEATURE_FILE_DROP.apply(this, !isMac()); - - // Should be true except on web builds. - // Chart editor doesn't work there. - FEATURE_CHART_EDITOR.apply(this, !isWeb()); - - // Should be true except on web builds. - // Screenshots doesn't work there. - FEATURE_SCREENSHOTS.apply(this, !isWeb()); - } + for (arch in this.architectures) + { + // Display the list of target architectures. + switch (arch) + { + case Architecture.X86: + info('Architecture: x86'); + case Architecture.X64: + info('Architecture: x64'); + case Architecture.ARMV5: + info('Architecture: ARMv5'); + case Architecture.ARMV6: + info('Architecture: ARMv6'); + case Architecture.ARMV7: + info('Architecture: ARMv7'); + case Architecture.ARMV7S: + info('Architecture: ARMv7S'); + case Architecture.ARM64: + info('Architecture: ARMx64'); + case Architecture.MIPS: + info('Architecture: MIPS'); + case Architecture.MIPSEL: + info('Architecture: MIPSEL'); + case null: + if (!isWeb()) + { + error('Unsupported architecture (got null on non-web platform)'); + } + else + { + info('Architecture: Web'); + } + default: + error('Unsupported architecture (got ${arch})'); + } + } + } + + /** + * Apply various feature flags based on the target platform and the user-provided build flags. + */ + function configureFeatureFlags() + { + // You can explicitly override any of these. + // For example, `-DGITHUB_BUILD` or `-DNO_HARDCODED_CREDITS` + + // Should be false unless explicitly requested. + GITHUB_BUILD.apply(this, false); + FEATURE_GHOST_TAPPING.apply(this, false); + + // Should be true unless explicitly requested. + HARDCODED_CREDITS.apply(this, true); + FEATURE_OPEN_URL.apply(this, true); + FEATURE_POLYMOD_MODS.apply(this, true); + FEATURE_FUNKVIS.apply(this, true); + FEATURE_PARTIAL_SOUNDS.apply(this, true); + FEATURE_VIDEO_PLAYBACK.apply(this, true); + FEATURE_STAGE_EDITOR.apply(this, true); + + // Should be true on debug builds or if GITHUB_BUILD is enabled. + FEATURE_DEBUG_FUNCTIONS.apply(this, isDebug() || GITHUB_BUILD.isEnabled(this)); + FEATURE_RESULTS_DEBUG.apply(this, isDebug() || GITHUB_BUILD.isEnabled(this)); + + // Got a lot of complains about this being turned off by default on some builds. + // TODO: Look into ways to optimize logging (maybe by using a thread pool?) + FEATURE_LOG_TRACE.apply(this, true); + + FEATURE_NEWGROUNDS.apply(this, true); + FEATURE_NEWGROUNDS_DEBUG.apply(this, false); + FEATURE_NEWGROUNDS_TESTING_MEDALS.apply(this, FEATURE_NEWGROUNDS.isEnabled(this) && FEATURE_DEBUG_FUNCTIONS.isEnabled(this)); + FEATURE_NEWGROUNDS_AUTOLOGIN.apply(this, FEATURE_NEWGROUNDS.isEnabled(this) && isWeb()); + FEATURE_NEWGROUNDS_EVENTS.apply(this, FEATURE_NEWGROUNDS.isEnabled(this)); + + // Should default to true on workspace builds and false on release builds. + REDIRECT_ASSETS_FOLDER.apply(this, isDebug() && isDesktop()); + + // Should be true on desktop, release, non-tester builds. + // We don't want testers to accidentally leak songs to their Discord friends! + FEATURE_DISCORD_RPC.apply(this, isDesktop() && !FEATURE_DEBUG_FUNCTIONS.isEnabled(this)); + + // Should be true only on web builds. + // Audio context issues only exist there. + TOUCH_HERE_TO_PLAY.apply(this, isWeb()); + + // Should be true only on web builds. + // Enabling embedding and preloading is required to preload assets properly. + EMBED_ASSETS.apply(this, isWeb()); + PRELOAD_ALL.apply(this, !isWeb()); + + // Should be true except on MacOS. + // File drop doesn't work there. + FEATURE_FILE_DROP.apply(this, !isMac()); + + // Should be true except on web builds. + // Chart editor doesn't work there. + FEATURE_CHART_EDITOR.apply(this, !isWeb()); + + // Should be true except on web builds. + // Screenshots doesn't work there. + FEATURE_SCREENSHOTS.apply(this, !isWeb()); + } /** * Set compilation flags which are not feature flags. */ - function configureCompileDefines() { - // Enable OpenFL's error handler. Required for the crash logger. + function configureCompileDefines() + { + // Enable OpenFL's error handler. Required for the crash logger. setHaxedef("openfl-enable-handle-error"); - // Enable stack trace tracking. Good for debugging but has a (minor) performance impact. - setHaxedef("HXCPP_CHECK_POINTER"); - setHaxedef("HXCPP_STACK_LINE"); - setHaxedef("HXCPP_STACK_TRACE"); - setHaxedef("hscriptPos"); - - setHaxedef("safeMode"); - - // If we aren't using the Flixel debugger, strip it out. - if (FEATURE_DEBUG_FUNCTIONS.isDisabled(this)) { - setHaxedef("FLX_NO_DEBUG"); - } - - // Disable the built in pause screen when unfocusing the game. - setHaxedef("FLX_NO_FOCUS_LOST_SCREEN"); - - // HaxeUI configuration. - setHaxedef("haxeui_no_mouse_reset"); - setHaxedef("haxeui_focus_out_on_click"); // Unfocus a dialog when clicking out of it - setHaxedef("haxeui_dont_impose_base_class"); // Suppress a macro error - - if (isRelease()) { - // Improve performance on Nape - // TODO: Do we even use Nape? - setHaxedef("NAPE_RELEASE_BUILD"); - } - - // Cleaner looking compiler errors. - setHaxedef("message.reporting", "pretty"); - - if (FEATURE_DEBUG_TRACY.isEnabled(this)) { - setHaxedef("HXCPP_TELEMETRY"); // Enable telemetry - setHaxedef("HXCPP_TRACY"); // Enable Tracy telemetry - setHaxedef("HXCPP_TRACY_MEMORY"); // Track memory allocations - setHaxedef("HXCPP_TRACY_ON_DEMAND"); // Only collect telemetry when Tracy is open and reachable - // setHaxedef("HXCPP_TRACY_INCLUDE_CALLSTACKS"); // Inspect callstacks per zone, inflating telemetry data - - setHaxedef("absolute-paths"); // Fix source locations so Tracy can see them - } + // Enable stack trace tracking. Good for debugging but has a (minor) performance impact. + setHaxedef("HXCPP_CHECK_POINTER"); + setHaxedef("HXCPP_STACK_LINE"); + setHaxedef("HXCPP_STACK_TRACE"); + setHaxedef("hscriptPos"); + + setHaxedef("safeMode"); + + // If we aren't using the Flixel debugger, strip it out. + if (FEATURE_DEBUG_FUNCTIONS.isDisabled(this)) + { + setHaxedef("FLX_NO_DEBUG"); + } + + // Disable the built in pause screen when unfocusing the game. + setHaxedef("FLX_NO_FOCUS_LOST_SCREEN"); + + // HaxeUI configuration. + setHaxedef("haxeui_no_mouse_reset"); + setHaxedef("haxeui_focus_out_on_click"); // Unfocus a dialog when clicking out of it + setHaxedef("haxeui_dont_impose_base_class"); // Suppress a macro error + + if (isRelease()) + { + // Improve performance on Nape + // TODO: Do we even use Nape? + setHaxedef("NAPE_RELEASE_BUILD"); + } + + // Cleaner looking compiler errors. + setHaxedef("message.reporting", "pretty"); + + if (FEATURE_DEBUG_TRACY.isEnabled(this)) + { + setHaxedef("HXCPP_TELEMETRY"); // Enable telemetry + setHaxedef("HXCPP_TRACY"); // Enable Tracy telemetry + setHaxedef("HXCPP_TRACY_MEMORY"); // Track memory allocations + setHaxedef("HXCPP_TRACY_ON_DEMAND"); // Only collect telemetry when Tracy is open and reachable + // setHaxedef("HXCPP_TRACY_INCLUDE_CALLSTACKS"); // Inspect callstacks per zone, inflating telemetry data + + setHaxedef("absolute-paths"); // Fix source locations so Tracy can see them + } + } + + /** + * Set compilation flags which manage dead code elimination. + */ + function configureIncludeMacros() + { + // Disable dead code elimination. + // This prevents functions that are unused by the base game from being unavailable to HScript. + addHaxeFlag("-dce no"); + + // Forcibly include all Funkin' classes in builds. + // This prevents classes that are unused by the base game from being unavailable to HScript. + addHaxeMacro("include('funkin')"); + + // Ensure all HaxeUI components are available at runtime. + addHaxeMacro("include('haxe.ui.backend.flixel.components')"); + addHaxeMacro("include('haxe.ui.core')"); + addHaxeMacro("include('haxe.ui.components')"); + addHaxeMacro("include('haxe.ui.containers')"); + addHaxeMacro("include('haxe.ui.containers.dialogs')"); + addHaxeMacro("include('haxe.ui.containers.menus')"); + addHaxeMacro("include('haxe.ui.containers.properties')"); + + // Ensure all Flixel classes are available at runtime. + // Explicitly ignore packages which require additional dependencies. + addHaxeMacro("include('flixel', true, [ 'flixel.addons.editors.spine.*', 'flixel.addons.nape.*', 'flixel.system.macros.*', 'flixel.addons.tile.FlxRayCastTilemap' ])"); + } + + /** + * Set compilation flags which manage bespoke build-time macros. + */ + function configureCustomMacros() + { + // This macro allows addition of new functionality to existing Flixel. --> + addHaxeMacro("addMetadata('@:build(funkin.util.macro.FlxMacro.buildFlxBasic())', 'flixel.FlxBasic')"); } - /** - * Set compilation flags which manage dead code elimination. - */ - function configureIncludeMacros() { - // Disable dead code elimination. - // This prevents functions that are unused by the base game from being unavailable to HScript. - addHaxeFlag("-dce no"); - - // Forcibly include all Funkin' classes in builds. - // This prevents classes that are unused by the base game from being unavailable to HScript. - addHaxeMacro("include('funkin')"); - - // Ensure all HaxeUI components are available at runtime. - addHaxeMacro("include('haxe.ui.backend.flixel.components')"); - addHaxeMacro("include('haxe.ui.core')"); - addHaxeMacro("include('haxe.ui.components')"); - addHaxeMacro("include('haxe.ui.containers')"); - addHaxeMacro("include('haxe.ui.containers.dialogs')"); - addHaxeMacro("include('haxe.ui.containers.menus')"); - addHaxeMacro("include('haxe.ui.containers.properties')"); - - // Ensure all Flixel classes are available at runtime. - // Explicitly ignore packages which require additional dependencies. - addHaxeMacro("include('flixel', true, [ 'flixel.addons.editors.spine.*', 'flixel.addons.nape.*', 'flixel.system.macros.*', 'flixel.addons.tile.FlxRayCastTilemap' ])"); - } - - /** - * Set compilation flags which manage bespoke build-time macros. - */ - function configureCustomMacros() { - // This macro allows addition of new functionality to existing Flixel. --> - addHaxeMacro("addMetadata('@:build(funkin.util.macro.FlxMacro.buildFlxBasic())', 'flixel.FlxBasic')"); - } - - function configureOutputDir() { - // Set the output directory. Depends on the target platform and build type. - - var buildDir = 'export/${isDebug() ? 'debug' : 'release'}'; - - // we use a dedicated 'tracy' folder, since it generally needs a recompile when in use - if (FEATURE_DEBUG_TRACY.isEnabled(this)) - buildDir += "-tracy"; - - // trailing slash might not be needed, works fine on macOS without it, but I haven't tested on Windows! - buildDir += "/"; - - - info('Output directory: $buildDir'); - // setenv('BUILD_DIR', buildDir); - app.path = buildDir; + function configureOutputDir() + { + // Set the output directory. Depends on the target platform and build type. + + var buildDir = 'export/${isDebug() ? 'debug' : 'release'}'; + + // we use a dedicated 'tracy' folder, since it generally needs a recompile when in use + if (FEATURE_DEBUG_TRACY.isEnabled(this)) buildDir += "-tracy"; + + // trailing slash might not be needed, works fine on macOS without it, but I haven't tested on Windows! + buildDir += "/"; + + info('Output directory: $buildDir'); + // setenv('BUILD_DIR', buildDir); + app.path = buildDir; } - function configurePolymod() { - // The file extension to use for script files. - setHaxedef("POLYMOD_SCRIPT_EXT", ".hscript"); - // Which asset library to use for scripts. - setHaxedef("POLYMOD_SCRIPT_LIBRARY", "scripts"); - // The base path from which scripts should be accessed. - setHaxedef("POLYMOD_ROOT_PATH", "scripts/"); - // Determines the subdirectory of the mod folder used for file appending. - setHaxedef("POLYMOD_APPEND_FOLDER", "_append"); - // Determines the subdirectory of the mod folder used for file merges. - setHaxedef("POLYMOD_MERGE_FOLDER", "_merge"); - // Determines the file in the mod folder used for metadata. - setHaxedef("POLYMOD_MOD_METADATA_FILE", "_polymod_meta.json"); - // Determines the file in the mod folder used for the icon. - setHaxedef("POLYMOD_MOD_ICON_FILE", "_polymod_icon.png"); - - if (isDebug()) { - // Turns on additional debug logging. + function configurePolymod() + { + // The file extension to use for script files. + setHaxedef("POLYMOD_SCRIPT_EXT", ".hscript"); + // Which asset library to use for scripts. + setHaxedef("POLYMOD_SCRIPT_LIBRARY", "scripts"); + // The base path from which scripts should be accessed. + setHaxedef("POLYMOD_ROOT_PATH", "scripts/"); + // Determines the subdirectory of the mod folder used for file appending. + setHaxedef("POLYMOD_APPEND_FOLDER", "_append"); + // Determines the subdirectory of the mod folder used for file merges. + setHaxedef("POLYMOD_MERGE_FOLDER", "_merge"); + // Determines the file in the mod folder used for metadata. + setHaxedef("POLYMOD_MOD_METADATA_FILE", "_polymod_meta.json"); + // Determines the file in the mod folder used for the icon. + setHaxedef("POLYMOD_MOD_ICON_FILE", "_polymod_icon.png"); + + if (isDebug()) + { + // Turns on additional debug logging. setHaxedef("POLYMOD_DEBUG"); } } - function configureHaxelibs() { - // Don't enforce + function configureHaxelibs() + { + // Don't enforce addHaxelib('lime'); // Game engine backend addHaxelib('openfl'); // Game engine backend @@ -688,506 +726,599 @@ class Project extends HXProject { addHaxelib('polymod'); // Modding framework addHaxelib('flxanimate'); // Texture atlas rendering - addHaxelib('json2object'); // JSON parsing + addHaxelib('json2object'); // JSON parsing addHaxelib('jsonpath'); // JSON parsing addHaxelib('jsonpatch'); // JSON parsing - addHaxelib('thx.core'); // General utility library, "the lodash of Haxe" - addHaxelib('thx.semver'); // Version string handling + addHaxelib('thx.core'); // General utility library, "the lodash of Haxe" + addHaxelib('thx.semver'); // Version string handling - if (isDebug()) { - addHaxelib('hxcpp-debug-server'); // VSCode debug support - } + if (isDebug()) + { + addHaxelib('hxcpp-debug-server'); // VSCode debug support + } - if (isDesktop() && !isHashLink() && FEATURE_VIDEO_PLAYBACK.isEnabled(this)) { + if (isDesktop() && !isHashLink() && FEATURE_VIDEO_PLAYBACK.isEnabled(this)) + { // hxvlc doesn't function on HashLink or non-desktop platforms - // It's also unnecessary if video playback is disabled + // It's also unnecessary if video playback is disabled addHaxelib('hxvlc'); // Video playback } - if (FEATURE_DISCORD_RPC.isEnabled(this)) { - addHaxelib('hxdiscord_rpc'); // Discord API - } + if (FEATURE_DISCORD_RPC.isEnabled(this)) + { + addHaxelib('hxdiscord_rpc'); // Discord API + } - if (FEATURE_NEWGROUNDS.isEnabled(this)) { - addHaxelib('newgrounds'); // Newgrounds API - } + if (FEATURE_NEWGROUNDS.isEnabled(this)) + { + addHaxelib('newgrounds'); // Newgrounds API + } - if (FEATURE_FUNKVIS.isEnabled(this)) { - addHaxelib('funkin.vis'); // Audio visualization + if (FEATURE_FUNKVIS.isEnabled(this)) + { + addHaxelib('funkin.vis'); // Audio visualization addHaxelib('grig.audio'); // Audio data utilities - } + } - if (FEATURE_PARTIAL_SOUNDS.isEnabled(this)) { - addHaxelib('FlxPartialSound'); // Partial sound - } + if (FEATURE_PARTIAL_SOUNDS.isEnabled(this)) + { + addHaxelib('FlxPartialSound'); // Partial sound + } } - function configureAssets() { - var exclude = EXCLUDE_ASSETS.concat(isWeb() ? EXCLUDE_ASSETS_WEB : EXCLUDE_ASSETS_NATIVE); - var shouldPreload = PRELOAD_ALL.isEnabled(this); - var shouldEmbed = EMBED_ASSETS.isEnabled(this); - - if (shouldEmbed) { - info('Embedding assets into executable...'); - } else { - info('Including assets alongside executable...'); - } - - // Default asset library - var shouldPreloadDefault = true; - addAssetLibrary("default", shouldEmbed, shouldPreloadDefault); - addAssetPath("assets/preload", "assets", "default", ["*"], exclude, shouldEmbed); - - // Font assets - var shouldEmbedFonts = true; - addAssetPath("assets/fonts", null, "default", ["*"], exclude, shouldEmbedFonts); - - // Shared asset libraries - addAssetLibrary("songs", shouldEmbed, shouldPreload); - addAssetPath("assets/songs", "assets/songs", "songs", ["*"], exclude, shouldEmbed); - addAssetLibrary("shared", shouldEmbed, shouldPreload); - addAssetPath("assets/shared", "assets/shared", "shared", ["*"], exclude, shouldEmbed); - if (FEATURE_VIDEO_PLAYBACK.isEnabled(this)) { - var shouldEmbedVideos = false; - addAssetLibrary("videos", shouldEmbedVideos, shouldPreload); - addAssetPath("assets/videos", "assets/videos", "videos", ["*"], exclude, shouldEmbedVideos); - } - - // Level asset libraries - addAssetLibrary("tutorial", shouldEmbed, shouldPreload); - addAssetPath("assets/tutorial", "assets/tutorial", "tutorial", ["*"], exclude, shouldEmbed); - addAssetLibrary("week1", shouldEmbed, shouldPreload); - addAssetPath("assets/week1", "assets/week1", "week1", ["*"], exclude, shouldEmbed); - addAssetLibrary("week2", shouldEmbed, shouldPreload); - addAssetPath("assets/week2", "assets/week2", "week2", ["*"], exclude, shouldEmbed); - addAssetLibrary("week3", shouldEmbed, shouldPreload); - addAssetPath("assets/week3", "assets/week3", "week3", ["*"], exclude, shouldEmbed); - addAssetLibrary("week4", shouldEmbed, shouldPreload); - addAssetPath("assets/week4", "assets/week4", "week4", ["*"], exclude, shouldEmbed); - addAssetLibrary("week5", shouldEmbed, shouldPreload); - addAssetPath("assets/week5", "assets/week5", "week5", ["*"], exclude, shouldEmbed); - addAssetLibrary("week6", shouldEmbed, shouldPreload); - addAssetPath("assets/week6", "assets/week6", "week6", ["*"], exclude, shouldEmbed); - addAssetLibrary("week7", shouldEmbed, shouldPreload); - addAssetPath("assets/week7", "assets/week7", "week7", ["*"], exclude, shouldEmbed); - addAssetLibrary("weekend1", shouldEmbed, shouldPreload); - addAssetPath("assets/weekend1", "assets/weekend1", "weekend1", ["*"], exclude, shouldEmbed); - - // Art asset library (where README pulls from) - var shouldEmbedArt = false; - var shouldPreloadArt = false; - addAssetLibrary("art", shouldEmbedArt, shouldPreloadArt); - addAsset("art/readme.txt", "do NOT readme.txt", "art", shouldEmbedArt); - addAsset("LICENSE.md", "LICENSE.md", "art", shouldEmbedArt); - } + function configureAssets() + { + var exclude = EXCLUDE_ASSETS.concat(isWeb() ? EXCLUDE_ASSETS_WEB : EXCLUDE_ASSETS_NATIVE); + var shouldPreload = PRELOAD_ALL.isEnabled(this); + var shouldEmbed = EMBED_ASSETS.isEnabled(this); + + if (shouldEmbed) + { + info('Embedding assets into executable...'); + } + else + { + info('Including assets alongside executable...'); + } + + // Default asset library + var shouldPreloadDefault = true; + addAssetLibrary("default", shouldEmbed, shouldPreloadDefault); + addAssetPath("assets/preload", "assets", "default", ["*"], exclude, shouldEmbed); + + // Font assets + var shouldEmbedFonts = true; + addAssetPath("assets/fonts", null, "default", ["*"], exclude, shouldEmbedFonts); + + // Shared asset libraries + addAssetLibrary("songs", shouldEmbed, shouldPreload); + addAssetPath("assets/songs", "assets/songs", "songs", ["*"], exclude, shouldEmbed); + addAssetLibrary("shared", shouldEmbed, shouldPreload); + addAssetPath("assets/shared", "assets/shared", "shared", ["*"], exclude, shouldEmbed); + if (FEATURE_VIDEO_PLAYBACK.isEnabled(this)) + { + var shouldEmbedVideos = false; + addAssetLibrary("videos", shouldEmbedVideos, shouldPreload); + addAssetPath("assets/videos", "assets/videos", "videos", ["*"], exclude, shouldEmbedVideos); + } + + // Level asset libraries + addAssetLibrary("tutorial", shouldEmbed, shouldPreload); + addAssetPath("assets/tutorial", "assets/tutorial", "tutorial", ["*"], exclude, shouldEmbed); + addAssetLibrary("week1", shouldEmbed, shouldPreload); + addAssetPath("assets/week1", "assets/week1", "week1", ["*"], exclude, shouldEmbed); + addAssetLibrary("week2", shouldEmbed, shouldPreload); + addAssetPath("assets/week2", "assets/week2", "week2", ["*"], exclude, shouldEmbed); + addAssetLibrary("week3", shouldEmbed, shouldPreload); + addAssetPath("assets/week3", "assets/week3", "week3", ["*"], exclude, shouldEmbed); + addAssetLibrary("week4", shouldEmbed, shouldPreload); + addAssetPath("assets/week4", "assets/week4", "week4", ["*"], exclude, shouldEmbed); + addAssetLibrary("week5", shouldEmbed, shouldPreload); + addAssetPath("assets/week5", "assets/week5", "week5", ["*"], exclude, shouldEmbed); + addAssetLibrary("week6", shouldEmbed, shouldPreload); + addAssetPath("assets/week6", "assets/week6", "week6", ["*"], exclude, shouldEmbed); + addAssetLibrary("week7", shouldEmbed, shouldPreload); + addAssetPath("assets/week7", "assets/week7", "week7", ["*"], exclude, shouldEmbed); + addAssetLibrary("weekend1", shouldEmbed, shouldPreload); + addAssetPath("assets/weekend1", "assets/weekend1", "weekend1", ["*"], exclude, shouldEmbed); + + // Art asset library (where README pulls from) + var shouldEmbedArt = false; + var shouldPreloadArt = false; + addAssetLibrary("art", shouldEmbedArt, shouldPreloadArt); + addAsset("art/readme.txt", "do NOT readme.txt", "art", shouldEmbedArt); + addAsset("LICENSE.md", "LICENSE.md", "art", shouldEmbedArt); + } /** * Configure the application's favicon and executable icon. */ - function configureIcons() { - addIcon("art/icon16.png", 16); - addIcon("art/icon32.png", 32); - addIcon("art/icon64.png", 64); - addIcon("art/iconOG.png"); + function configureIcons() + { + addIcon("art/icon16.png", 16); + addIcon("art/icon32.png", 32); + addIcon("art/icon64.png", 64); + addIcon("art/iconOG.png"); } - // - // HELPER FUNCTIONS - // Easy functions to make the code more readable. - // + // + // HELPER FUNCTIONS + // Easy functions to make the code more readable. + // - public function isWeb():Bool { - return this.platformType == PlatformType.WEB; - } + public function isWeb():Bool + { + return this.platformType == PlatformType.WEB; + } - public function isMobile():Bool { - return this.platformType == PlatformType.MOBILE; - } + public function isMobile():Bool + { + return this.platformType == PlatformType.MOBILE; + } - public function isDesktop():Bool { - return this.platformType == PlatformType.DESKTOP; - } + public function isDesktop():Bool + { + return this.platformType == PlatformType.DESKTOP; + } - public function isConsole():Bool { - return this.platformType == PlatformType.CONSOLE; - } + public function isConsole():Bool + { + return this.platformType == PlatformType.CONSOLE; + } - public function is32Bit():Bool { - return this.architectures.contains(Architecture.X86); - } + public function is32Bit():Bool + { + return this.architectures.contains(Architecture.X86); + } - public function is64Bit():Bool { - return this.architectures.contains(Architecture.X64); - } + public function is64Bit():Bool + { + return this.architectures.contains(Architecture.X64); + } - public function isWindows():Bool { - return this.target == Platform.WINDOWS; - } + public function isWindows():Bool + { + return this.target == Platform.WINDOWS; + } - public function isMac():Bool { - return this.target == Platform.MAC; - } + public function isMac():Bool + { + return this.target == Platform.MAC; + } - public function isLinux():Bool { - return this.target == Platform.LINUX; - } + public function isLinux():Bool + { + return this.target == Platform.LINUX; + } - public function isAndroid():Bool { - return this.target == Platform.ANDROID; - } + public function isAndroid():Bool + { + return this.target == Platform.ANDROID; + } - public function isIOS():Bool { - return this.target == Platform.IOS; - } + public function isIOS():Bool + { + return this.target == Platform.IOS; + } - public function isHashLink():Bool { + public function isHashLink():Bool + { return this.targetFlags.exists("hl"); } - public function isNeko():Bool { + public function isNeko():Bool + { return this.targetFlags.exists("neko"); } - public function isJava():Bool { + public function isJava():Bool + { return this.targetFlags.exists("java"); } - public function isNodeJS():Bool { + public function isNodeJS():Bool + { return this.targetFlags.exists("nodejs"); } - public function isCSharp():Bool { + public function isCSharp():Bool + { return this.targetFlags.exists("cs"); } - public function isDisplay():Bool { + public function isDisplay():Bool + { return this.command == "display"; } - public function isDebug():Bool { - return this.debug; - } + public function isDebug():Bool + { + return this.debug; + } - public function isRelease():Bool { - return !isDebug(); - } + public function isRelease():Bool + { + return !isDebug(); + } - public function getHaxedef(name:String):Null { + public function getHaxedef(name:String):Null + { return this.haxedefs.get(name); } - public function setHaxedef(name:String, ?value:String):Void { + public function setHaxedef(name:String, ?value:String):Void + { if (value == null) value = ""; this.haxedefs.set(name, value); } - public function unsetHaxedef(name:String):Void { + public function unsetHaxedef(name:String):Void + { this.haxedefs.remove(name); } - public function getDefine(name:String):Null { - return this.defines.get(name); - } - - public function hasDefine(name:String):Bool { - return this.defines.exists(name); - } - - /** - * Add a library to the list of dependencies for the project. - * @param name The name of the library to add. - * @param version The version of the library to add. Optional. - */ - public function addHaxelib(name:String, version:String = ""):Void { - this.haxelibs.push(new Haxelib(name, version)); - } - - /** - * Add a `haxeflag` to the project. - */ - public function addHaxeFlag(value:String):Void { - this.haxeflags.push(value); - } - - /** - * Call a Haxe build macro. - */ - public function addHaxeMacro(value:String):Void { - addHaxeFlag('--macro ${value}'); - } - - /** - * Add an icon to the project. - * @param icon The path to the icon. - * @param size The size of the icon. Optional. - */ - public function addIcon(icon:String, ?size:Int):Void { - this.icons.push(new Icon(icon, size)); - } - - /** - * Add an asset to the game build. - * @param path The path the asset is located at. - * @param rename The path the asset should be placed. - * @param library The asset library to add the asset to. `null` = "default" - * @param embed Whether to embed the asset in the executable. - */ - public function addAsset(path:String, ?rename:String, ?library:String, embed:Bool = false):Void { - // path, rename, type, embed, setDefaults - var asset = new Asset(path, rename, null, embed, true); - @:nullSafety(Off) - { - asset.library = library ?? "default"; - } - this.assets.push(asset); - } - - /** - * Add an entire path of assets to the game build. - * @param path The path the assets are located at. - * @param rename The path the assets should be placed. - * @param library The asset library to add the assets to. `null` = "default" - * @param include An optional array to include specific asset names. - * @param exclude An optional array to exclude specific asset names. - * @param embed Whether to embed the assets in the executable. - */ - public function addAssetPath(path:String, ?rename:String, library:String, ?include:Array, ?exclude:Array, embed:Bool = false):Void { - // Argument parsing. - if (path == "") return; - - if (include == null) include = []; - - if (exclude == null) exclude = []; - - var targetPath = rename ?? path; - if (targetPath != "") targetPath += "/"; - - // Validate path. - if (!sys.FileSystem.exists(path)) { - error('Could not find asset path "${path}".'); - } else if (!sys.FileSystem.isDirectory(path)) { - error('Could not parse asset path "${path}", expected a directory.'); - } else { - // info(' Found asset path "${path}".'); - } - - for (file in sys.FileSystem.readDirectory(path)) { - if (sys.FileSystem.isDirectory('${path}/${file}')) { - // Attempt to recursively add all assets in the directory. - if (this.filter(file, ["*"], exclude)) { - addAssetPath('${path}/${file}', '${targetPath}${file}', library, include, exclude, embed); - } - } else { - if (this.filter(file, include, exclude)) { - addAsset('${path}/${file}', '${targetPath}${file}', library, embed); - } - } - } - } - - /** - * Add an asset library to the game build. - * @param name The name of the library. - * @param embed - * @param preload - */ - public function addAssetLibrary(name:String, embed:Bool = false, preload:Bool = false):Void { - // sourcePath, name, type, embed, preload, generate, prefix + public function getDefine(name:String):Null + { + return this.defines.get(name); + } + + public function hasDefine(name:String):Bool + { + return this.defines.exists(name); + } + + /** + * Add a library to the list of dependencies for the project. + * @param name The name of the library to add. + * @param version The version of the library to add. Optional. + */ + public function addHaxelib(name:String, version:String = ""):Void + { + this.haxelibs.push(new Haxelib(name, version)); + } + + /** + * Add a `haxeflag` to the project. + */ + public function addHaxeFlag(value:String):Void + { + this.haxeflags.push(value); + } + + /** + * Call a Haxe build macro. + */ + public function addHaxeMacro(value:String):Void + { + addHaxeFlag('--macro ${value}'); + } + + /** + * Add an icon to the project. + * @param icon The path to the icon. + * @param size The size of the icon. Optional. + */ + public function addIcon(icon:String, ?size:Int):Void + { + this.icons.push(new Icon(icon, size)); + } + + /** + * Add an asset to the game build. + * @param path The path the asset is located at. + * @param rename The path the asset should be placed. + * @param library The asset library to add the asset to. `null` = "default" + * @param embed Whether to embed the asset in the executable. + */ + public function addAsset(path:String, ?rename:String, ?library:String, embed:Bool = false):Void + { + // path, rename, type, embed, setDefaults + var asset = new Asset(path, rename, null, embed, true); + @:nullSafety(Off) + { + asset.library = library ?? "default"; + } + this.assets.push(asset); + } + + /** + * Add an entire path of assets to the game build. + * @param path The path the assets are located at. + * @param rename The path the assets should be placed. + * @param library The asset library to add the assets to. `null` = "default" + * @param include An optional array to include specific asset names. + * @param exclude An optional array to exclude specific asset names. + * @param embed Whether to embed the assets in the executable. + */ + public function addAssetPath(path:String, ?rename:String, library:String, ?include:Array, ?exclude:Array, embed:Bool = false):Void + { + // Argument parsing. + if (path == "") return; + + if (include == null) include = []; + + if (exclude == null) exclude = []; + + var targetPath = rename ?? path; + if (targetPath != "") targetPath += "/"; + + // Validate path. + if (!sys.FileSystem.exists(path)) + { + error('Could not find asset path "${path}".'); + } + else if (!sys.FileSystem.isDirectory(path)) + { + error('Could not parse asset path "${path}", expected a directory.'); + } + else + { + // info(' Found asset path "${path}".'); + } + + for (file in sys.FileSystem.readDirectory(path)) + { + if (sys.FileSystem.isDirectory('${path}/${file}')) + { + // Attempt to recursively add all assets in the directory. + if (this.filter(file, ["*"], exclude)) + { + addAssetPath('${path}/${file}', '${targetPath}${file}', library, include, exclude, embed); + } + } + else + { + if (this.filter(file, include, exclude)) + { + addAsset('${path}/${file}', '${targetPath}${file}', library, embed); + } + } + } + } + + /** + * Add an asset library to the game build. + * @param name The name of the library. + * @param embed + * @param preload + */ + public function addAssetLibrary(name:String, embed:Bool = false, preload:Bool = false):Void + { + // sourcePath, name, type, embed, preload, generate, prefix var sourcePath = ''; - this.libraries.push(new Library(sourcePath, name, null, embed, preload, false, "")); - } + this.libraries.push(new Library(sourcePath, name, null, embed, preload, false, "")); + } - // - // PROCESS FUNCTIONS - // + // + // PROCESS FUNCTIONS + // /** * A CLI command to run a command in the shell. */ - public function buildCLICommand(cmd:String):CLICommand { + public function buildCLICommand(cmd:String):CLICommand + { return CommandHelper.fromSingleString(cmd); } /** * A CLI command to run a Haxe script via `--interp`. */ - public function buildHaxeCLICommand(path:String):CLICommand { + public function buildHaxeCLICommand(path:String):CLICommand + { return CommandHelper.interpretHaxe(path); } - public function getGitCommit():String { - // Cannibalized from GitCommit.hx - var process = new sys.io.Process('git', ['rev-parse', 'HEAD']); - if (process.exitCode() != 0) { - var message = process.stderr.readAll().toString(); - error('[ERROR] Could not determine current git commit; is this a proper Git repository?'); - } - - var commitHash:String = process.stdout.readLine(); - var commitHashSplice:String = commitHash.substr(0, 7); - - process.close(); - - return commitHashSplice; - } - - public function getGitBranch():String { - // Cannibalized from GitCommit.hx - var branchProcess = new sys.io.Process('git', ['rev-parse', '--abbrev-ref', 'HEAD']); - - if (branchProcess.exitCode() != 0) { - var message = branchProcess.stderr.readAll().toString(); - error('Could not determine current git branch; is this a proper Git repository?'); - } - - var branchName:String = branchProcess.stdout.readLine(); - - branchProcess.close(); - - return branchName; - } - - public function getGitModified():Bool { - var branchProcess = new sys.io.Process('git', ['status', '--porcelain']); - - if (branchProcess.exitCode() != 0) { - var message = branchProcess.stderr.readAll().toString(); - error('Could not determine current git status; is this a proper Git repository?'); - } - - var output:String = ''; - try { - output = branchProcess.stdout.readLine(); - } catch (e) { - if (e.message == 'Eof') { - // Do nothing. - // Eof = No output. - } else { - // Rethrow other exceptions. - throw e; - } - } - - branchProcess.close(); - - return output.length > 0; - } - - // - // LOGGING FUNCTIONS - // - - /** - * Display an error message. This should stop the build process. - */ - public function error(message:String):Void { - Log.error('${message}'); - } - - /** - * Display an info message. This should not interfere with the build process. - */ - public function info(message:String):Void { - if(command != "display") { Log.info('[INFO] ${message}'); } - } + public function getGitCommit():String + { + // Cannibalized from GitCommit.hx + var process = new sys.io.Process('git', ['rev-parse', 'HEAD']); + if (process.exitCode() != 0) + { + var message = process.stderr.readAll().toString(); + error('[ERROR] Could not determine current git commit; is this a proper Git repository?'); + } + + var commitHash:String = process.stdout.readLine(); + var commitHashSplice:String = commitHash.substr(0, 7); + + process.close(); + + return commitHashSplice; + } + + public function getGitBranch():String + { + // Cannibalized from GitCommit.hx + var branchProcess = new sys.io.Process('git', ['rev-parse', '--abbrev-ref', 'HEAD']); + + if (branchProcess.exitCode() != 0) + { + var message = branchProcess.stderr.readAll().toString(); + error('Could not determine current git branch; is this a proper Git repository?'); + } + + var branchName:String = branchProcess.stdout.readLine(); + + branchProcess.close(); + + return branchName; + } + + public function getGitModified():Bool + { + var branchProcess = new sys.io.Process('git', ['status', '--porcelain']); + + if (branchProcess.exitCode() != 0) + { + var message = branchProcess.stderr.readAll().toString(); + error('Could not determine current git status; is this a proper Git repository?'); + } + + var output:String = ''; + try + { + output = branchProcess.stdout.readLine(); + } + catch (e) + { + if (e.message == 'Eof') + { + // Do nothing. + // Eof = No output. + } + else + { + // Rethrow other exceptions. + throw e; + } + } + + branchProcess.close(); + + return output.length > 0; + } + + // + // LOGGING FUNCTIONS + // + + /** + * Display an error message. This should stop the build process. + */ + public function error(message:String):Void + { + Log.error('${message}'); + } + + /** + * Display an info message. This should not interfere with the build process. + */ + public function info(message:String):Void + { + if (command != "display") + { + Log.info('[INFO] ${message}'); + } + } } /** * An object representing a feature flag, which can be enabled or disabled. * Includes features such as automatic generation of compile defines and inversion. */ -abstract FeatureFlag(String) { - static final INVERSE_PREFIX:String = "NO_"; - - public function new(input:String) { - this = input; - } - - @:from - public static function fromString(input:String):FeatureFlag { - return new FeatureFlag(input); - } - - /** - * Enable/disable a feature flag if it is unset, and handle the inverse flag. - * Doesn't override a feature flag that was set explicitly. - * @param enableByDefault Whether to enable this feature flag if it is unset. - */ - public function apply(project:Project, enableByDefault:Bool = false):Void { - // TODO: Name this function better? - - if (isEnabled(project)) { - // If this flag was already enabled, disable the inverse. - project.info('Enabling feature flag ${this}'); - getInverse().disable(project, false); - } else if (getInverse().isEnabled(project)) { - // If the inverse flag was already enabled, disable this flag. - project.info('Disabling feature flag ${this}'); - disable(project, false); - } else { - if (enableByDefault) { - // Enable this flag if it was unset, and disable the inverse. - project.info('Enabling feature flag ${this}'); - enable(project, true); - } else { - // Disable this flag if it was unset, and enable the inverse. - project.info('Disabling feature flag ${this}'); - disable(project, true); - } - } - } - - /** - * Enable this feature flag by setting the appropriate compile define. - * - * @param project The project to modify. - * @param andInverse Also disable the feature flag's inverse. - */ - public function enable(project:Project, andInverse:Bool = true) { - project.setHaxedef(this, ""); - if (andInverse) { - getInverse().disable(project, false); - } - } - - /** - * Disable this feature flag by removing the appropriate compile define. - * - * @param project The project to modify. - * @param andInverse Also enable the feature flag's inverse. - */ - public function disable(project:Project, andInverse:Bool = true) { - project.unsetHaxedef(this); - if (andInverse) { - getInverse().enable(project, false); - } - } - - /** - * Query if this feature flag is enabled. - * @param project The project to query. - */ - public function isEnabled(project:Project):Bool { - // Check both Haxedefs and Defines for this flag. - return project.haxedefs.exists(this) || project.defines.exists(this); - } - - /** - * Query if this feature flag's inverse is enabled. - */ - public function isDisabled(project:Project):Bool { - return getInverse().isEnabled(project); - } - - /** - * Return the inverse of this feature flag. - * @return A new feature flag that is the inverse of this one. - */ - public function getInverse():FeatureFlag { - if (this.startsWith(INVERSE_PREFIX)) { - return this.substring(INVERSE_PREFIX.length); - } - return INVERSE_PREFIX + this; - } +abstract FeatureFlag(String) +{ + static final INVERSE_PREFIX:String = "NO_"; + + public function new(input:String) + { + this = input; + } + + @:from + public static function fromString(input:String):FeatureFlag + { + return new FeatureFlag(input); + } + + /** + * Enable/disable a feature flag if it is unset, and handle the inverse flag. + * Doesn't override a feature flag that was set explicitly. + * @param enableByDefault Whether to enable this feature flag if it is unset. + */ + public function apply(project:Project, enableByDefault:Bool = false):Void + { + // TODO: Name this function better? + + if (isEnabled(project)) + { + // If this flag was already enabled, disable the inverse. + project.info('Enabling feature flag ${this}'); + getInverse().disable(project, false); + } + else if (getInverse().isEnabled(project)) + { + // If the inverse flag was already enabled, disable this flag. + project.info('Disabling feature flag ${this}'); + disable(project, false); + } + else + { + if (enableByDefault) + { + // Enable this flag if it was unset, and disable the inverse. + project.info('Enabling feature flag ${this}'); + enable(project, true); + } + else + { + // Disable this flag if it was unset, and enable the inverse. + project.info('Disabling feature flag ${this}'); + disable(project, true); + } + } + } + + /** + * Enable this feature flag by setting the appropriate compile define. + * + * @param project The project to modify. + * @param andInverse Also disable the feature flag's inverse. + */ + public function enable(project:Project, andInverse:Bool = true) + { + project.setHaxedef(this, ""); + if (andInverse) + { + getInverse().disable(project, false); + } + } + + /** + * Disable this feature flag by removing the appropriate compile define. + * + * @param project The project to modify. + * @param andInverse Also enable the feature flag's inverse. + */ + public function disable(project:Project, andInverse:Bool = true) + { + project.unsetHaxedef(this); + if (andInverse) + { + getInverse().enable(project, false); + } + } + + /** + * Query if this feature flag is enabled. + * @param project The project to query. + */ + public function isEnabled(project:Project):Bool + { + // Check both Haxedefs and Defines for this flag. + return project.haxedefs.exists(this) || project.defines.exists(this); + } + + /** + * Query if this feature flag's inverse is enabled. + */ + public function isDisabled(project:Project):Bool + { + return getInverse().isEnabled(project); + } + + /** + * Return the inverse of this feature flag. + * @return A new feature flag that is the inverse of this one. + */ + public function getInverse():FeatureFlag + { + if (this.startsWith(INVERSE_PREFIX)) + { + return this.substring(INVERSE_PREFIX.length); + } + return INVERSE_PREFIX + this; + } } diff --git a/source/funkin/Conductor.hx b/source/funkin/Conductor.hx index 803b9e1b362..f47135e9e66 100644 --- a/source/funkin/Conductor.hx +++ b/source/funkin/Conductor.hx @@ -92,6 +92,12 @@ class Conductor */ public var songPosition(default, null):Float = 0; + /** + * The offset between frame time and music time. + * Used in `getTimeWithDelta()` to get a more accurate music time when on higher framerates. + */ + var songPositionDelta(default, null):Float = 0; + var prevTimestamp:Float = 0; var prevTime:Float = 0; @@ -422,7 +428,8 @@ class Conductor // If the song is playing, limit the song position to the length of the song or beginning of the song. if (FlxG.sound.music != null && FlxG.sound.music.playing) { - this.songPosition = Math.min(currentLength, Math.max(0, songPos)); + this.songPosition = FlxMath.bound(Math.min(this.combinedOffset, 0), songPos, currentLength); + this.songPositionDelta += FlxG.elapsed * 1000 * FlxG.sound.music.pitch; } else { @@ -488,12 +495,23 @@ class Conductor // which it doesn't do every frame! if (prevTime != this.songPosition) { + this.songPositionDelta = 0; + // Update the timestamp for use in-between frames prevTime = this.songPosition; prevTimestamp = Std.int(Timer.stamp() * 1000); } } + /** + * Returns a more accurate music time for higher framerates. + * @return Float + */ + public function getTimeWithDelta():Float + { + return this.songPosition + this.songPositionDelta; + } + /** * Can be called in-between frames, usually for input related things * that can potentially get processed on exact milliseconds/timestmaps. diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index d374e4c9ca3..9a581630c42 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -27,6 +27,7 @@ import funkin.play.character.CharacterData.CharacterDataParser; import funkin.play.notes.notekind.NoteKindManager; import funkin.play.PlayStatePlaylist; import funkin.ui.debug.charting.ChartEditorState; +import funkin.ui.debug.stageeditor.StageEditorState; import funkin.ui.title.TitleState; import funkin.ui.transition.LoadingState; import funkin.util.CLIUtil; @@ -241,6 +242,9 @@ class InitState extends FlxState #elseif CHARTING // -DCHARTING FlxG.switchState(() -> new funkin.ui.debug.charting.ChartEditorState()); + #elseif STAGING + // -DSTAGING + FlxG.switchState(() -> new funkin.ui.debug.stageeditor.StageEditorState()); #elseif STAGEBUILD // -DSTAGEBUILD FlxG.switchState(() -> new funkin.ui.debug.stage.StageBuilderState()); @@ -303,6 +307,13 @@ class InitState extends FlxState fnfcTargetPath: params.chart.chartPath, })); } + else if (params.stage.shouldLoadStage) + { + FlxG.switchState(() -> new StageEditorState( + { + fnfsTargetPath: params.stage.stagePath, + })); + } else { FlxG.sound.cache(Paths.music('freakyMenu/freakyMenu')); diff --git a/source/funkin/Preferences.hx b/source/funkin/Preferences.hx index ce5aba6d2a4..3ac18194167 100644 --- a/source/funkin/Preferences.hx +++ b/source/funkin/Preferences.hx @@ -343,44 +343,6 @@ class Preferences return value; } - /** - * The game will save any screenshots taken to this format. - * @default `PNG` - */ - public static var saveFormat(get, set):Any; - - static function get_saveFormat():Any - { - return Save?.instance?.options?.screenshot?.saveFormat ?? 'PNG'; - } - - static function set_saveFormat(value):Any - { - var save:Save = Save.instance; - save.options.screenshot.saveFormat = value; - save.flush(); - return value; - } - - /** - * The game will save JPEG screenshots with this quality percentage. - * @default `80` - */ - public static var jpegQuality(get, set):Int; - - static function get_jpegQuality():Int - { - return Save?.instance?.options?.screenshot?.jpegQuality ?? 80; - } - - static function set_jpegQuality(value:Int):Int - { - var save:Save = Save.instance; - save.options.screenshot.jpegQuality = value; - save.flush(); - return value; - } - /** * Loads the user's preferences from the save data and apply them. */ diff --git a/source/funkin/api/discord/DiscordClient.hx b/source/funkin/api/discord/DiscordClient.hx index b0cb3180a13..1d8dd68e6c2 100644 --- a/source/funkin/api/discord/DiscordClient.hx +++ b/source/funkin/api/discord/DiscordClient.hx @@ -56,8 +56,6 @@ class DiscordClient { while (true) { - trace('[DISCORD] Performing client update...'); - #if DISCORD_DISABLE_IO_THREAD Discord.updateConnection(); #end @@ -76,8 +74,6 @@ class DiscordClient public function setPresence(params:DiscordClientPresenceParams):Void { - trace('[DISCORD] Updating presence... (${params})'); - Discord.updatePresence(buildPresence(params)); } @@ -101,8 +97,6 @@ class DiscordClient // IMPORTANT NOTE: This can be an asset key uploaded to Discord's developer panel OR any URL you like. presence.largeImageKey = cast(params.largeImageKey, Null) ?? "album-volume1"; - trace('[DISCORD] largeImageKey: ${presence.largeImageKey}'); - // TODO: Make this use the song's album art. // presence.largeImageKey = "icon"; // presence.largeImageKey = "https://f4.bcbits.com/img/a0746694746_16.jpg"; @@ -204,4 +198,17 @@ typedef DiscordClientPresenceParams = */ var ?smallImageKey:String; } + +class DiscordClientSandboxed +{ + public static function setPresence(params:DiscordClientPresenceParams) + { + return DiscordClient.instance.setPresence(params); + } + + public static function shutdown() + { + DiscordClient.instance.shutdown(); + } +} #end diff --git a/source/funkin/api/newgrounds/Leaderboards.hx b/source/funkin/api/newgrounds/Leaderboards.hx index 5c7e5e73e0f..75cbcc8da78 100644 --- a/source/funkin/api/newgrounds/Leaderboards.hx +++ b/source/funkin/api/newgrounds/Leaderboards.hx @@ -2,7 +2,10 @@ package funkin.api.newgrounds; #if FEATURE_NEWGROUNDS import io.newgrounds.Call.CallError; +import io.newgrounds.components.ScoreBoardComponent; +import io.newgrounds.objects.Score; import io.newgrounds.objects.ScoreBoard as LeaderboardData; +import io.newgrounds.objects.User; import io.newgrounds.objects.events.Outcome; import io.newgrounds.utils.ScoreBoardList; @@ -66,6 +69,41 @@ class Leaderboards } } + /** + * Request to receive scores from Newgrounds. + * @param leaderboard The leaderboard to fetch scores from. + * @param params Additional parameters for fetching the score. + */ + public static function requestScores(leaderboard:Leaderboard, params:RequestScoresParams) + { + // Silently reject retrieving scores from unknown leaderboards. + if (leaderboard == Leaderboard.Unknown) return; + + var leaderboardList = NewgroundsClient.instance.leaderboards; + if (leaderboardList == null) return; + + var leaderboardData:Null = leaderboardList.get(leaderboard.getId()); + if (leaderboardData == null) return; + + var user:Null = null; + if ((params?.useCurrentUser ?? false) && NewgroundsClient.instance.isLoggedIn()) user = NewgroundsClient.instance.user; + + leaderboardData.requestScores(params?.limit ?? 10, params?.skip ?? 0, params?.period ?? ALL, params?.social ?? false, params?.tag, user, + function(outcome:Outcome):Void { + switch (outcome) + { + case SUCCESS: + trace('[NEWGROUNDS] Fetched scores!'); + if (params?.onComplete != null) params.onComplete(leaderboardData.scores); + + case FAIL(error): + trace('[NEWGROUNDS] Failed to fetch scores!'); + trace(error); + if (params?.onFail != null) params.onFail(); + } + }); + } + /** * Submit a score for a Story Level to Newgrounds. */ @@ -84,6 +122,74 @@ class Leaderboards Leaderboards.submitScore(Leaderboard.getLeaderboardBySong(songId, difficultyId), score, tag); } } + +/** + * Wrapper for `Leaderboards` that prevents submitting scores. + */ +@:nullSafety +class LeaderboardsSandboxed +{ + public static function getLeaderboardBySong(songId:String, difficultyId:String) + { + return Leaderboard.getLeaderboardBySong(songId, difficultyId); + } + + public static function getLeaderboardByLevel(levelId:String) + { + return Leaderboard.getLeaderboardByLevel(levelId); + } + + public function requestScores(leaderboard:Leaderboard, params:RequestScoresParams) + { + Leaderboards.requestScores(leaderboard, params); + } +} + +/** + * Additional parameters for `Leaderboards.requestScores()` + */ +typedef RequestScoresParams = +{ + /** + * How many scores to include in a list. + * @default `10` + */ + var ?limit:Int; + + /** + * How many scores to skip before starting the list. + * @default `0` + */ + var ?skip:Int; + + /** + * The time-frame to pull the scores from. + * @default `Period.ALL` + */ + var ?period:Period; + + /** + * If true, only scores by the user and their friends will be loaded. Ignored if no user is set. + * @default `false` + */ + var ?social:Bool; + + /** + * An optional tag to filter the results by. + * @default `null` + */ + var ?tag:String; + + /** + * If true, only the scores from the currently logged in user will be loaded. + * Additionally, if `social` is set to true, the scores of the user's friend will be loaded. + * @default `false` + */ + var ?useCurrentUser:Bool; + + var ?onComplete:Array->Void; + var ?onFail:Void->Void; +} #end enum abstract Leaderboard(Int) @@ -285,7 +391,7 @@ enum abstract Leaderboard(Int) { case "darnell": return DarnellBFMix; - case "litup": + case "lit-up": return LitUpBFMix; default: return Unknown; @@ -379,7 +485,7 @@ enum abstract Leaderboard(Int) return Stress; case "darnell": return Darnell; - case "litup": + case "lit-up": return LitUp; case "2hot": return TwoHot; diff --git a/source/funkin/api/newgrounds/Medals.hx b/source/funkin/api/newgrounds/Medals.hx index 0559a004fc9..e64274793e4 100644 --- a/source/funkin/api/newgrounds/Medals.hx +++ b/source/funkin/api/newgrounds/Medals.hx @@ -131,33 +131,79 @@ class Medals } } + public static function fetchMedalData(medal:Medal):Null + { + var medalList = NewgroundsClient.instance.medals; + @:privateAccess + if (medalList == null || medalList._map == null) return null; + + var medalData:Null = medalList.get(medal.getId()); + @:privateAccess + if (medalData == null || medalData._data == null) + { + trace('[NEWGROUNDS] Could not retrieve data for medal: ${medal}'); + return null; + } + + return { + id: medalData.id, + name: medalData.name, + description: medalData.description, + icon: medalData.icon, + value: medalData.value, + difficulty: medalData.difficulty, + secret: medalData.secret, + unlocked: medalData.unlocked + } + } + public static function awardStoryLevel(id:String):Void { - switch (id) + var medal:Medal = Medal.getMedalByStoryLevel(id); + if (medal == Medal.Unknown) { - case 'tutorial': - Medals.award(Medal.StoryTutorial); - case 'week1': - Medals.award(Medal.StoryWeek1); - case 'week2': - Medals.award(Medal.StoryWeek2); - case 'week3': - Medals.award(Medal.StoryWeek3); - case 'week4': - Medals.award(Medal.StoryWeek4); - case 'week5': - Medals.award(Medal.StoryWeek5); - case 'week6': - Medals.award(Medal.StoryWeek6); - case 'week7': - Medals.award(Medal.StoryWeek7); - case 'weekend1': - Medals.award(Medal.StoryWeekend1); - default: - trace('[NEWGROUNDS] Story level does not have a medal! (${id}).'); + trace('[NEWGROUNDS] Story level does not have a medal! (${id}).'); + return; } + Medals.award(medal); + } +} + +/** + * Wrapper for `Medals` that prevents awarding medals. + */ +class MedalsSandboxed +{ + public static function fetchMedalData(medal:Medal):Null + { + return Medals.fetchMedalData(medal); + } + + public static function getMedalByStoryLevel(id:String):Medal + { + return Medal.getMedalByStoryLevel(id); + } + + public static function getAllMedals():Array + { + return Medal.getAllMedals(); } } + +/** + * Contains data for a Medal, but excludes functions like `sendUnlock()`. + */ +typedef FetchedMedalData = +{ + var id:Int; + var name:String; + var description:String; + var icon:String; + var value:Int; + var difficulty:Int; + var secret:Bool; + var unlocked:Bool; +} #end /** @@ -324,6 +370,8 @@ enum abstract Medal(Int) from Int to Int { switch (levelId) { + case "tutorial": + return StoryTutorial; case "week1": return StoryWeek1; case "week2": @@ -344,4 +392,33 @@ enum abstract Medal(Int) from Int to Int return Unknown; } } + + /** + * Lists all medals aside from the `Unknown` one. + */ + public static function getAllMedals() + { + return [ + StartGame, + StoryTutorial, + StoryWeek1, + StoryWeek2, + StoryWeek3, + StoryWeek4, + StoryWeek5, + StoryWeek6, + StoryWeek7, + StoryWeekend1, + CharSelect, + FreeplayPicoMix, + FreeplayStressPico, + LossRating, + PerfectRatingHard, + GoldPerfectRatingHard, + ErectDifficulty, + GoldPerfectRatingNightmare, + FridayNight, + Nice + ]; + } } diff --git a/source/funkin/api/newgrounds/NewgroundsClient.hx b/source/funkin/api/newgrounds/NewgroundsClient.hx index 36ab563abd8..8bd993ff64d 100644 --- a/source/funkin/api/newgrounds/NewgroundsClient.hx +++ b/source/funkin/api/newgrounds/NewgroundsClient.hx @@ -331,4 +331,22 @@ class NewgroundsClient return Save.instance.ngSessionId; } } + +/** + * Wrapper for `NewgroundsClient` that prevents submitting cheated data. + */ +class NewgroundsClientSandboxed +{ + public static var user(get, never):Null; + + static function get_user() + { + return NewgroundsClient.instance.user; + } + + public static function isLoggedIn() + { + return NewgroundsClient.instance.isLoggedIn(); + } +} #end diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx index fb1f8aa1171..9644a6d20f2 100644 --- a/source/funkin/audio/FunkinSound.hx +++ b/source/funkin/audio/FunkinSound.hx @@ -551,6 +551,7 @@ class FunkinSound extends FlxSound implements ICloneable } FlxTween.cancelTweensOf(this); this._label = 'unknown'; + this._waveformData = null; } @:access(openfl.media.Sound) diff --git a/source/funkin/data/song/SongDataUtils.hx b/source/funkin/data/song/SongDataUtils.hx index d0e554e0151..1445864a7a5 100644 --- a/source/funkin/data/song/SongDataUtils.hx +++ b/source/funkin/data/song/SongDataUtils.hx @@ -5,7 +5,6 @@ import funkin.data.song.SongData.SongEventData; import funkin.data.song.SongData.SongNoteData; import funkin.data.song.SongData.SongTimeChange; import funkin.util.ClipboardUtil; -import funkin.util.SerializerUtil; using Lambda; diff --git a/source/funkin/data/song/SongNoteDataUtils.hx b/source/funkin/data/song/SongNoteDataUtils.hx new file mode 100644 index 00000000000..18d5beb1c9d --- /dev/null +++ b/source/funkin/data/song/SongNoteDataUtils.hx @@ -0,0 +1,138 @@ +package funkin.data.song; + +using SongData.SongNoteData; + +/** + * Utility class for extra handling of song notes + */ +@:nullSafety +class SongNoteDataUtils +{ + static final CHUNK_INTERVAL_MS:Float = 2500; + + /** + * Retrieves all stacked notes. It does this by cycling through "chunks" of notes within a certain interval. + * + * @param notes Sorted notes by time. + * @param threshold The note stack threshold. Refer to `doNotesStack` for more details. + * @param includeOverlapped (Optional) If overlapped notes should be included. + * @param overlapped (Optional) An array that gets populated with overlapped notes. + * Note that it's only guaranteed to work properly if the provided notes are sorted. + * @return Stacked notes. + */ + public static function listStackedNotes(notes:Array, threshold:Float, includeOverlapped:Bool = true, + ?overlapped:Array):Array + { + var stackedNotes:Array = []; + + var chunkTime:Float = 0; + var chunks:Array> = [[]]; + + for (note in notes) + { + if (note == null) + { + continue; + } + + while (note.time >= chunkTime + CHUNK_INTERVAL_MS) + { + chunkTime += CHUNK_INTERVAL_MS; + chunks.push([]); + } + + chunks[chunks.length - 1].push(note); + } + + for (chunk in chunks) + { + for (i in 0...(chunk.length - 1)) + { + for (j in (i + 1)...chunk.length) + { + var noteI:SongNoteData = chunk[i]; + var noteJ:SongNoteData = chunk[j]; + + if (doNotesStack(noteI, noteJ, threshold)) + { + if (!stackedNotes.fastContains(noteI)) + { + if (includeOverlapped) stackedNotes.push(noteI); + + if (overlapped != null && !overlapped.contains(noteI)) overlapped.push(noteI); + } + + if (!stackedNotes.fastContains(noteJ)) + { + stackedNotes.push(noteJ); + } + } + } + } + } + + return stackedNotes; + } + + /** + * Concatenates two arrays of notes but overwrites notes in `lhs` that are overlapped by notes in `rhs`. + * Hold notes are only overwritten by longer hold notes. + * This operation only modifies the second array and `overwrittenNotes`. + * + * @param lhs An array of notes + * @param rhs An array of notes to concatenate into `lhs` + * @param overwrittenNotes An optional array that is modified in-place with the notes in `lhs` that were overwritten. + * @param threshold The note stack threshold. Refer to `doNotesStack` for more details. + * @return The unsorted resulting array. + */ + public static function concatOverwrite(lhs:Array, rhs:Array, ?overwrittenNotes:Array, + threshold:Float = 0):Array + { + if (lhs == null || rhs == null || rhs.length == 0) return lhs; + if (lhs.length == 0) return rhs; + + var result = lhs.copy(); + for (i in 0...rhs.length) + { + var noteB:SongNoteData = rhs[i]; + var hasOverlap:Bool = false; + + for (j in 0...lhs.length) + { + var noteA:SongNoteData = lhs[j]; + if (doNotesStack(noteA, noteB, threshold)) + { + // Long hold notes should have priority over shorter hold notes + if (noteA.length <= noteB.length) + { + overwrittenNotes?.push(result[j].clone()); + result[j] = noteB; + } + hasOverlap = true; + break; + } + } + + if (!hasOverlap) result.push(noteB); + } + + return result; + } + + /** + * @param noteA First note. + * @param noteB Second note. + * @param threshold The note stack threshold, in steps. + * @return Returns `true` if both notes are on the same strumline, have the same direction + * and their time difference in steps is less than the step-based threshold. + * A threshold of 0 will return `true` if notes are nearly perfectly aligned. + */ + public static function doNotesStack(noteA:SongNoteData, noteB:SongNoteData, threshold:Float = 0):Bool + { + if (noteA.data != noteB.data) return false; + else if (threshold == 0) return Math.ffloor(Math.abs(noteA.time - noteB.time)) < 1; + + final stepDiff:Float = Math.abs(noteA.getStepTime() - noteB.getStepTime()); + return stepDiff <= threshold + 0.001; + } +} diff --git a/source/funkin/data/song/SongRegistry.hx b/source/funkin/data/song/SongRegistry.hx index fcdabc91fa4..bc688f94c5c 100644 --- a/source/funkin/data/song/SongRegistry.hx +++ b/source/funkin/data/song/SongRegistry.hx @@ -324,7 +324,7 @@ class SongRegistry extends BaseRegistry implements ISingleto } else { - throw '[${registryId}] Chart entry ${id}:${variation} does not support migration to version ${SONG_CHART_DATA_VERSION_RULE}.'; + throw '[${registryId}] Chart entry ${id}:${variation} does not support migration to version ${SONG_MUSIC_DATA_VERSION_RULE}.'; } } @@ -337,7 +337,7 @@ class SongRegistry extends BaseRegistry implements ISingleto } else { - throw '[${registryId}] Chart entry "$fileName" does not support migration to version ${SONG_CHART_DATA_VERSION_RULE}.'; + throw '[${registryId}] Chart entry "$fileName" does not support migration to version ${SONG_MUSIC_DATA_VERSION_RULE}.'; } } diff --git a/source/funkin/data/stage/StageData.hx b/source/funkin/data/stage/StageData.hx index 88cffc7e7e7..1b4eb5b74a0 100644 --- a/source/funkin/data/stage/StageData.hx +++ b/source/funkin/data/stage/StageData.hx @@ -205,7 +205,7 @@ typedef StageDataProp = /** * The angle of the prop, as a float. - * @default 1.0 + * @default 0.0 */ @:optional @:default(0.0) @@ -284,7 +284,7 @@ typedef StageDataCharacter = /** * The angle of the character, as a float. - * @default 1.0 + * @default 0.0 */ @:optional @:default(0.0) diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx index 4ae1d46bc5a..89fccb167e0 100644 --- a/source/funkin/modding/PolymodHandler.hx +++ b/source/funkin/modding/PolymodHandler.hx @@ -258,6 +258,21 @@ class PolymodHandler // `funkin.util.FileUtil` has unrestricted access to the file system. Polymod.addImportAlias('funkin.util.FileUtil', funkin.util.FileUtilSandboxed); + #if FEATURE_NEWGROUNDS + // `funkin.api.newgrounds.Leaderboards` allows for submitting cheated scores. + Polymod.addImportAlias('funkin.api.newgrounds.Leaderboards', funkin.api.newgrounds.Leaderboards.LeaderboardsSandboxed); + + // `funkin.api.newgrounds.Medals` allows for unfair granting of medals. + Polymod.addImportAlias('funkin.api.newgrounds.Medals', funkin.api.newgrounds.Medals.MedalsSandboxed); + + // `funkin.api.newgrounds.NewgroundsClientSandboxed` allows for submitting cheated data. + Polymod.addImportAlias('funkin.api.newgrounds.NewgroundsClient', funkin.api.newgrounds.NewgroundsClient.NewgroundsClientSandboxed); + #end + + #if FEATURE_DISCORD_RPC + Polymod.addImportAlias('funkin.api.discord.DiscordClient', funkin.api.discord.DiscordClient.DiscordClientSandboxed); + #end + // Add blacklisting for prohibited classes and packages. // `Sys` @@ -276,9 +291,9 @@ class PolymodHandler // Lib.load() can load malicious DLLs Polymod.blacklistImport('cpp.Lib'); - // `Unserializer` + // `haxe.Unserializer` // Unserializer.DEFAULT_RESOLVER.resolveClass() can access blacklisted packages - Polymod.blacklistImport('Unserializer'); + Polymod.blacklistImport('haxe.Unserializer'); // `lime.system.CFFI` // Can load and execute compiled binaries. @@ -310,6 +325,7 @@ class PolymodHandler { if (cls == null) continue; var className:String = Type.getClassName(cls); + if (polymod.hscript._internal.PolymodScriptClass.importOverrides.exists(className)) continue; Polymod.blacklistImport(className); } @@ -322,27 +338,28 @@ class PolymodHandler Polymod.blacklistImport(className); } - // `funkin.api.newgrounds.*` + // `io.newgrounds.*` // Contains functions which allow for cheating medals and leaderboards. - for (cls in ClassMacro.listClassesInPackage('funkin.api.newgrounds')) + for (cls in ClassMacro.listClassesInPackage('io.newgrounds')) { if (cls == null) continue; var className:String = Type.getClassName(cls); Polymod.blacklistImport(className); } - // `io.newgrounds.*` - // Contains functions which allow for cheating medals and leaderboards. - for (cls in ClassMacro.listClassesInPackage('io.newgrounds')) + // `sys.*` + // Access to system utilities such as the file system. + for (cls in ClassMacro.listClassesInPackage('sys')) { if (cls == null) continue; var className:String = Type.getClassName(cls); Polymod.blacklistImport(className); } - // `sys.*` - // Access to system utilities such as the file system. - for (cls in ClassMacro.listClassesInPackage('sys')) + // `funkin.util.macro.*` + // CompiledClassList's get function allows access to sys and Newgrounds classes + // None of the classes are suitable for mods anyway + for (cls in ClassMacro.listClassesInPackage('funkin.util.macro')) { if (cls == null) continue; var className:String = Type.getClassName(cls); diff --git a/source/funkin/modding/events/ScriptEvent.hx b/source/funkin/modding/events/ScriptEvent.hx index 6f4742a296f..6710e4138fb 100644 --- a/source/funkin/modding/events/ScriptEvent.hx +++ b/source/funkin/modding/events/ScriptEvent.hx @@ -255,7 +255,7 @@ class HoldNoteScriptEvent extends NoteScriptEvent */ public var doesNotesplash:Bool = false; - public function new(type:ScriptEventType, holdNote:SustainTrail, healthChange:Float, score:Int, isComboBreak:Bool, cancelable:Bool = false):Void + public function new(type:ScriptEventType, holdNote:SustainTrail, healthChange:Float, score:Int, isComboBreak:Bool, comboCount:Int = 0, cancelable:Bool = false):Void { super(type, null, healthChange, comboCount, true); this.holdNote = holdNote; diff --git a/source/funkin/modding/module/Module.hx b/source/funkin/modding/module/Module.hx index d53639f0901..2638b82e21e 100644 --- a/source/funkin/modding/module/Module.hx +++ b/source/funkin/modding/module/Module.hx @@ -176,13 +176,13 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte /** * Called when the game regains focus. - * This does not get called if "Auto Pause" is disabled. + * This does not get called if "Pause on Unfocus" is disabled. */ public function onFocusGained(event:FocusScriptEvent) {} /** * Called when the game loses focus. - * This does not get called if "Auto Pause" is disabled. + * This does not get called if "Pause on Unfocus" is disabled. */ public function onFocusLost(event:FocusScriptEvent) {} diff --git a/source/funkin/play/PauseSubState.hx b/source/funkin/play/PauseSubState.hx index 977a7808a47..1d3951ddc0c 100644 --- a/source/funkin/play/PauseSubState.hx +++ b/source/funkin/play/PauseSubState.hx @@ -766,11 +766,15 @@ class PauseSubState extends MusicBeatSubState * Quit the game and return to the chart editor. * @param state The current PauseSubState. */ + @:access(funkin.play.PlayState) static function quitToChartEditor(state:PauseSubState):Void { + // This should come first because the sounds list gets cleared! + PlayState.instance?.forEachPausedSound(s -> s.destroy()); state.close(); - if (FlxG.sound.music != null) FlxG.sound.music.pause(); // Don't reset song position! - PlayState.instance.close(); // This only works because PlayState is a substate! + FlxG.sound.music?.pause(); // Don't reset song position! + PlayState.instance?.vocals?.pause(); + PlayState.instance?.close(); // This only works because PlayState is a substate! } } diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 472a7f5e9c0..87aac3febfb 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -7,6 +7,7 @@ import flixel.FlxObject; import flixel.FlxSubState; import flixel.math.FlxMath; import flixel.math.FlxPoint; +import flixel.sound.FlxSound; import flixel.text.FlxText; import flixel.tweens.FlxTween; import flixel.ui.FlxBar; @@ -185,6 +186,11 @@ class PlayState extends MusicBeatSubState */ public var needsReset:Bool = false; + /** + * A timer that gets active once resetting happens. Used to vwoosh in notes. + */ + public var vwooshTimer:FlxTimer = new FlxTimer(); + /** * The current 'Blueball Counter' to display in the pause menu. * Resets when you beat a song or go back to the main menu. @@ -304,13 +310,13 @@ class PlayState extends MusicBeatSubState /** * Whether the game is currently in Practice Mode. - * If true, player will not lose gain or lose score from notes. + * If true, player will not gain or lose score from notes. */ public var isPracticeMode:Bool = false; /** * Whether the game is currently in Bot Play Mode. - * If true, player will not lose gain or lose score from notes. + * If true, player will not gain or lose score from notes. */ public var isBotPlayMode:Bool = false; @@ -432,6 +438,11 @@ class PlayState extends MusicBeatSubState */ var cameraTweensPausedBySubState:List = new List(); + /** + * Track any sounds we've paused for a Pause substate, so we can unpause them when we return. + */ + var soundsPausedBySubState:List = new List(); + /** * False until `create()` has completed. */ @@ -807,7 +818,6 @@ class PlayState extends MusicBeatSubState super.update(elapsed); - var list = FlxG.sound.list; updateHealthBar(); updateScoreText(); @@ -837,9 +847,9 @@ class PlayState extends MusicBeatSubState // Reset music properly. if (FlxG.sound.music != null) { - FlxG.sound.music.time = startTimestamp - Conductor.instance.combinedOffset; - FlxG.sound.music.pitch = playbackRate; FlxG.sound.music.pause(); + FlxG.sound.music.time = startTimestamp; + FlxG.sound.music.pitch = playbackRate; } if (!overrideMusic) @@ -854,7 +864,7 @@ class PlayState extends MusicBeatSubState } } vocals.pause(); - vocals.time = 0 - Conductor.instance.combinedOffset; + vocals.time = startTimestamp - Conductor.instance.instrumentalOffset; if (FlxG.sound.music != null) FlxG.sound.music.volume = 1; vocals.volume = 1; @@ -875,9 +885,6 @@ class PlayState extends MusicBeatSubState // Delete all notes and reset the arrays. regenNoteData(); - // so the song doesn't start too early :D - Conductor.instance.update(-5000, false); - // Reset camera zooming cameraBopIntensity = Constants.DEFAULT_BOP_INTENSITY; hudCameraZoomIntensity = (cameraBopIntensity - 1.0) * 2.0; @@ -886,10 +893,13 @@ class PlayState extends MusicBeatSubState health = Constants.HEALTH_STARTING; songScore = 0; Highscore.tallies.combo = 0; + + // so the song doesn't start too early :D + var vwooshDelay:Float = 0.5; + Conductor.instance.update(-vwooshDelay * 1000 + startTimestamp + Conductor.instance.beatLengthMs * -5); + // timer for vwoosh - var vwooshTimer = new FlxTimer(); - vwooshTimer.start(0.5, function(t:FlxTimer) { - Conductor.instance.update(startTimestamp - Conductor.instance.combinedOffset, false); + vwooshTimer.start(vwooshDelay, function(_) { if (playerStrumline.notes.length == 0) playerStrumline.updateNotes(); if (opponentStrumline.notes.length == 0) opponentStrumline.updateNotes(); playerStrumline.vwooshInNotes(); @@ -897,6 +907,9 @@ class PlayState extends MusicBeatSubState Countdown.performCountdown(); }); + // Stops any existing countdown. + Countdown.stopCountdown(); + // Reset the health icons. currentStage?.getBoyfriend()?.initHealthIcon(false); currentStage?.getDad()?.initHealthIcon(true); @@ -959,18 +972,16 @@ class PlayState extends MusicBeatSubState // Enable drawing while the substate is open, allowing the game state to be shown behind the pause menu. persistentDraw = true; - // There is a 1/1000 change to use a special pause menu. + // Prevent vwoosh timer from starting countdown in pause menu + vwooshTimer.active = false; + + // There is a 1/1000 chance to use a special pause menu. // This prevents the player from resuming, but that's the point. // It's a reference to Gitaroo Man, which doesn't let you pause the game. if (!isSubState && event.gitaroo) { this.remove(currentStage); - FlxG.switchState(() -> new GitarooPause( - { - targetSong: currentSong, - targetDifficulty: currentDifficulty, - targetVariation: currentVariation, - })); + FlxG.switchState(() -> new GitarooPause(lastParams)); } else { @@ -1127,6 +1138,8 @@ class PlayState extends MusicBeatSubState playerStrumline.clean(); opponentStrumline.clean(); + vwooshTimer.cancel(); + songScore = 0; updateScoreText(); @@ -1230,9 +1243,29 @@ class PlayState extends MusicBeatSubState musicPausedBySubState = true; } - // Pause vocals. - // Not tracking that we've done this via a bool because vocal re-syncing involves pausing the vocals anyway. - if (vocals != null) vocals.pause(); + // Pause any sounds that are playing and keep track of them. + // Vocals are also paused here but are not included as they are handled separately. + if (Std.isOfType(subState, PauseSubState)) + { + FlxG.sound.list.forEachAlive(function(sound:FlxSound) { + if (!sound.active || sound == FlxG.sound.music) return; + // In case it's a scheduled sound + var funkinSound:FunkinSound = cast sound; + if (funkinSound != null && !funkinSound.isPlaying) return; + if (!sound.playing && sound.time >= 0) return; + + sound.pause(); + soundsPausedBySubState.add(sound); + }); + + vocals?.forEach(function(voice:FunkinSound) { + soundsPausedBySubState.remove(voice); + }); + } + else + { + vocals?.pause(); + } } // Pause camera tweening, and keep track of which tweens we pause. @@ -1281,6 +1314,9 @@ class PlayState extends MusicBeatSubState if (event.eventCanceled) return; + // Resume vwooshTimer + if (!vwooshTimer.finished) vwooshTimer.active = true; + // Resume music if we paused it. if (musicPausedBySubState) { @@ -1288,6 +1324,8 @@ class PlayState extends MusicBeatSubState musicPausedBySubState = false; } + forEachPausedSound((s) -> needsReset ? s.destroy() : s.resume()); + // Resume camera tweens if we paused any. for (camTween in cameraTweensPausedBySubState) { @@ -1423,6 +1461,10 @@ class PlayState extends MusicBeatSubState { performCleanup(); + // `performCleanup()` clears the static reference to this state + // scripts might still need it, so we set it back to `this` + instance = this; + funkin.modding.PolymodHandler.forceReloadAssets(); lastParams.targetSong = SongRegistry.instance.fetchEntry(currentSong.id); LoadingState.loadPlayState(lastParams); @@ -2056,9 +2098,9 @@ class PlayState extends MusicBeatSubState FlxG.sound.music.onComplete = function() { if (mayPauseGame) endSong(skipEndingTransition); }; - // A negative instrumental offset means the song skips the first few milliseconds of the track. - // This just gets added into the startTimestamp behavior so we don't need to do anything extra. - FlxG.sound.music.play(true, Math.max(0, startTimestamp - Conductor.instance.combinedOffset)); + + FlxG.sound.music.pause(); + FlxG.sound.music.time = startTimestamp; FlxG.sound.music.pitch = playbackRate; // Prevent the volume from being wrong. @@ -2067,13 +2109,17 @@ class PlayState extends MusicBeatSubState trace('Playing vocals...'); add(vocals); - vocals.play(); - vocals.volume = 1.0; + + vocals.time = startTimestamp - Conductor.instance.instrumentalOffset; vocals.pitch = playbackRate; - vocals.time = FlxG.sound.music.time; + vocals.volume = 1.0; + + // trace('STARTING SONG AT:'); // trace('${FlxG.sound.music.time}'); // trace('${vocals.time}'); - resyncVocals(); + + FlxG.sound.music.play(); + vocals.play(); #if FEATURE_DISCORD_RPC // Updating Discord Rich Presence (with Time Left) @@ -2102,7 +2148,7 @@ class PlayState extends MusicBeatSubState } /** - * Resyncronize the vocal tracks if they have become offset from the instrumental. + * Resynchronize the vocal tracks if they have become offset from the instrumental. */ function resyncVocals():Void { @@ -2111,8 +2157,10 @@ class PlayState extends MusicBeatSubState // Skip this if the music is paused (GameOver, Pause menu, start-of-song offset, etc.) if (!(FlxG.sound.music?.playing ?? false)) return; - var timeToPlayAt:Float = Math.min(FlxG.sound.music.length, Math.max(0, Conductor.instance.songPosition - Conductor.instance.combinedOffset)); + var timeToPlayAt:Float = Math.min(FlxG.sound.music.length, + Math.max(Math.min(Conductor.instance.combinedOffset, 0), Conductor.instance.songPosition) - Conductor.instance.combinedOffset); trace('Resyncing vocals to ${timeToPlayAt}'); + FlxG.sound.music.pause(); vocals.pause(); @@ -2347,7 +2395,7 @@ class PlayState extends MusicBeatSubState { // Call an event to allow canceling the note miss. // NOTE: This is what handles the character animations! - var event:NoteScriptEvent = new NoteScriptEvent(NOTE_MISS, note, Constants.HEALTH_MISS_PENALTY, 0, true); + var event:NoteScriptEvent = new NoteScriptEvent(NOTE_MISS, note, Constants.HEALTH_MISS_PENALTY, Highscore.tallies.combo, true); dispatchEvent(event); // Calling event.cancelEvent() skips all the other logic! Neat! @@ -2414,7 +2462,7 @@ class PlayState extends MusicBeatSubState var healthChange = healthChangeUncapped.clamp(healthChangeMax, 0); var scoreChange = Std.int(Constants.SCORE_HOLD_DROP_PENALTY_PER_SECOND * remainingLengthSec); - var event:HoldNoteScriptEvent = new HoldNoteScriptEvent(NOTE_HOLD_DROP, holdNote, healthChange, scoreChange, true); + var event:HoldNoteScriptEvent = new HoldNoteScriptEvent(NOTE_HOLD_DROP, holdNote, healthChange, scoreChange, true, Highscore.tallies.combo); dispatchEvent(event); trace('Penalizing score by ${event.score} and health by ${event.healthChange} for dropping hold note (is combo break: ${event.isComboBreak})!'); @@ -2448,7 +2496,7 @@ class PlayState extends MusicBeatSubState } } - // Respawns notes that were b + // Respawns notes that were between the previous time and the current time when skipping backward, or destroy notes between the previous time and the current time when skipping forward. playerStrumline.handleSkippedNotes(); opponentStrumline.handleSkippedNotes(); } @@ -2709,6 +2757,8 @@ class PlayState extends MusicBeatSubState FlxG.switchState(() -> new ChartEditorState( { targetSongId: currentSong.id, + targetSongDifficulty: currentDifficulty, + targetSongVariation: currentVariation, })); } } @@ -3154,6 +3204,9 @@ class PlayState extends MusicBeatSubState // TODO: Uncache the song. } + // Prevent vwoosh timer from running outside PlayState (e.g Chart Editor) + vwooshTimer.cancel(); + if (overrideMusic) { // Stop the music. Do NOT destroy it, something still references it! @@ -3175,6 +3228,8 @@ class PlayState extends MusicBeatSubState } } + forEachPausedSound((s) -> s.destroy()); + // Remove reference to stage and remove sprites from it to save memory. if (currentStage != null) { @@ -3429,7 +3484,7 @@ class PlayState extends MusicBeatSubState cancelCameraZoomTween(); } - var prevScrollTargets:Array = []; // used to snap scroll speed when things go unruely + var prevScrollTargets:Array = []; // used to snap scroll speed when things go unruly /** * The magical function that shall tween the scroll speed. @@ -3483,6 +3538,15 @@ class PlayState extends MusicBeatSubState scrollSpeedTweens = []; } + function forEachPausedSound(f:FlxSound->Void):Void + { + for (sound in soundsPausedBySubState) + { + f(sound); + } + soundsPausedBySubState.clear(); + } + #if FEATURE_DEBUG_FUNCTIONS /** * Jumps forward or backward a number of sections in the song. diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx index 7e56553009e..1603f9e1853 100644 --- a/source/funkin/play/ResultState.hx +++ b/source/funkin/play/ResultState.hx @@ -563,16 +563,6 @@ class ResultState extends MusicBeatSubState // scorePopin.animation.play("score"); // scorePopin.visible = true; - - if (params.isNewHighscore ?? false) - { - highscoreNew.visible = true; - highscoreNew.animation.play("new"); - } - else - { - highscoreNew.visible = false; - } }; } diff --git a/source/funkin/play/cutscene/dialogue/DialogueBox.hx b/source/funkin/play/cutscene/dialogue/DialogueBox.hx index f4372d71cab..f546a605da5 100644 --- a/source/funkin/play/cutscene/dialogue/DialogueBox.hx +++ b/source/funkin/play/cutscene/dialogue/DialogueBox.hx @@ -5,7 +5,6 @@ import funkin.data.IRegistryEntry; import flixel.group.FlxSpriteGroup; import flixel.graphics.frames.FlxFramesCollection; import funkin.graphics.FunkinSprite; -import flixel.addons.text.FlxTypeText; import funkin.util.assets.FlxAnimationUtil; import funkin.modding.events.ScriptEvent; import funkin.audio.FunkinSound; @@ -66,7 +65,7 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass imple } var boxSprite:FlxSprite; - var textDisplay:FlxTypeText; + var textDisplay:FunkinTypeText; var text(default, set):String; @@ -273,7 +272,7 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass imple function loadText():Void { - textDisplay = new FlxTypeText(0, 0, 300, '', 32); + textDisplay = new FunkinTypeText(0, 0, 300, '', 32); textDisplay.fieldWidth = _data.text.width; textDisplay.setFormat(_data.text.fontFamily, _data.text.size, FlxColor.fromString(_data.text.color), LEFT, SHADOW, FlxColor.fromString(_data.text.shadowColor ?? '#00000000'), false); diff --git a/source/funkin/play/cutscene/dialogue/FunkinTypeText.hx b/source/funkin/play/cutscene/dialogue/FunkinTypeText.hx new file mode 100644 index 00000000000..8f16282f8cb --- /dev/null +++ b/source/funkin/play/cutscene/dialogue/FunkinTypeText.hx @@ -0,0 +1,104 @@ +package funkin.play.cutscene.dialogue; + +import flixel.addons.text.FlxTypeText; +import flixel.input.keyboard.FlxKey; + +/** + * An FlxTypeText that better accounts for text-wrapping, + * by overriding the functions of insertBreakLines() to check the finished state. + * Also fixes a bug where empty strings would make the typing never 'finish'. + */ +class FunkinTypeText extends FlxTypeText +{ + public var preWrapping:Bool = true; + + public function new(X:Float, Y:Float, Width:Int, Text:String, Size:Int = 8, EmbeddedFont:Bool = true, CheckWrapping:Bool = true) + { + super(X, Y, Width, "", Size, EmbeddedFont); + _finalText = Text; + preWrapping = CheckWrapping; + } + + override public function start(?Delay:Float, ForceRestart:Bool = false, AutoErase:Bool = false, ?SkipKeys:Array, ?Callback:Void->Void):Void + { + if (Delay != null) + { + delay = Delay; + } + + _typing = true; + _erasing = false; + paused = false; + _waiting = false; + + if (ForceRestart) + { + text = ""; + _length = 0; + } + + autoErase = AutoErase; + + if (SkipKeys != null) + { + skipKeys = SkipKeys; + } + + if (Callback != null) + { + completeCallback = Callback; + } + + if (useDefaultSound) + { + loadDefaultSound(); + } + + // Autocomplete if the text is empty anyway. Why bother? + if (_finalText.length == 0) + { + onComplete(); + return; + } + + if (preWrapping) + { + insertBreakLines(); + } + } + + override function insertBreakLines() + { + var saveText = text; + + // See what it looks like when it's finished typing. + text = prefix + _finalText; + var prefixLength:Null = prefix.length; + var split:String = ''; + + // trace('Breaking apart text lines...'); + + for (i in 0...textField.numLines) + { + var curLine = textField.getLineText(i); + // trace('now at line $i, curLine: $curLine'); + if (prefixLength >= curLine.length) + { + prefixLength -= curLine.length; + } + else if (prefixLength != null) + { + split += curLine.substr(prefixLength); + prefixLength = null; + } + else + { + split += '\n' + curLine; + } + // trace('now at line $i, split: $split'); + } + + _finalText = split; + text = saveText; + } +} diff --git a/source/funkin/play/event/FocusCameraSongEvent.hx b/source/funkin/play/event/FocusCameraSongEvent.hx index 3198e0792ce..137a9fbc541 100644 --- a/source/funkin/play/event/FocusCameraSongEvent.hx +++ b/source/funkin/play/event/FocusCameraSongEvent.hx @@ -218,9 +218,21 @@ class FocusCameraSongEvent extends SongEvent 'Smooth Step In' => 'smoothStepIn', 'Smooth Step Out' => 'smoothStepOut', 'Smooth Step In/Out' => 'smoothStepInOut', + 'Smoother Step In' => 'smootherStepIn', + 'Smoother Step Out' => 'smootherStepOut', + 'Smoother Step In/Out' => 'smootherStepInOut', 'Elastic In' => 'elasticIn', 'Elastic Out' => 'elasticOut', 'Elastic In/Out' => 'elasticInOut', + 'Back In' => 'backIn', + 'Back Out' => 'backOut', + 'Back In/Out' => 'backInOut', + 'Bounce In' => 'bounceIn', + 'Bounce Out' => 'bounceOut', + 'Bounce In/Out' => 'bounceInOut', + 'Circ In' => 'circIn', + 'Circ Out' => 'circOut', + 'Circ In/Out' => 'circInOut', 'Instant (Ignores duration)' => 'INSTANT', 'Classic (Ignores duration)' => 'CLASSIC' ] diff --git a/source/funkin/play/event/ScrollSpeedEvent.hx b/source/funkin/play/event/ScrollSpeedEvent.hx index bc0e99ba524..227fa0be24e 100644 --- a/source/funkin/play/event/ScrollSpeedEvent.hx +++ b/source/funkin/play/event/ScrollSpeedEvent.hx @@ -149,9 +149,21 @@ class ScrollSpeedEvent extends SongEvent 'Smooth Step In' => 'smoothStepIn', 'Smooth Step Out' => 'smoothStepOut', 'Smooth Step In/Out' => 'smoothStepInOut', + 'Smoother Step In' => 'smootherStepIn', + 'Smoother Step Out' => 'smootherStepOut', + 'Smoother Step In/Out' => 'smootherStepInOut', 'Elastic In' => 'elasticIn', 'Elastic Out' => 'elasticOut', - 'Elastic In/Out' => 'elasticInOut' + 'Elastic In/Out' => 'elasticInOut', + 'Back In' => 'backIn', + 'Back Out' => 'backOut', + 'Back In/Out' => 'backInOut', + 'Bounce In' => 'bounceIn', + 'Bounce Out' => 'bounceOut', + 'Bounce In/Out' => 'bounceInOut', + 'Circ In' => 'circIn', + 'Circ Out' => 'circOut', + 'Circ In/Out' => 'circInOut' ] }, { diff --git a/source/funkin/play/event/ZoomCameraSongEvent.hx b/source/funkin/play/event/ZoomCameraSongEvent.hx index 0c34028c8bf..d3635f8934d 100644 --- a/source/funkin/play/event/ZoomCameraSongEvent.hx +++ b/source/funkin/play/event/ZoomCameraSongEvent.hx @@ -158,9 +158,21 @@ class ZoomCameraSongEvent extends SongEvent 'Smooth Step In' => 'smoothStepIn', 'Smooth Step Out' => 'smoothStepOut', 'Smooth Step In/Out' => 'smoothStepInOut', + 'Smoother Step In' => 'smootherStepIn', + 'Smoother Step Out' => 'smootherStepOut', + 'Smoother Step In/Out' => 'smootherStepInOut', 'Elastic In' => 'elasticIn', 'Elastic Out' => 'elasticOut', - 'Elastic In/Out' => 'elasticInOut' + 'Elastic In/Out' => 'elasticInOut', + 'Back In' => 'backIn', + 'Back Out' => 'backOut', + 'Back In/Out' => 'backInOut', + 'Bounce In' => 'bounceIn', + 'Bounce Out' => 'bounceOut', + 'Bounce In/Out' => 'bounceInOut', + 'Circ In' => 'circIn', + 'Circ Out' => 'circOut', + 'Circ In/Out' => 'circInOut' ] } ]); diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx index 50c85b73377..fe4f4a4d3a9 100644 --- a/source/funkin/play/notes/Strumline.hx +++ b/source/funkin/play/notes/Strumline.hx @@ -448,7 +448,6 @@ class Strumline extends FlxSpriteGroup } } - /** * For a note's strumTime, calculate its Y position relative to the strumline. * NOTE: Assumes Conductor and PlayState are both initialized. @@ -458,7 +457,7 @@ class Strumline extends FlxSpriteGroup public function calculateNoteYPos(strumTime:Float):Float { return - Constants.PIXELS_PER_MS * (conductorInUse.songPosition - strumTime - Conductor.instance.inputOffset) * scrollSpeed * (Preferences.downscroll ? 1 : -1); + Constants.PIXELS_PER_MS * (conductorInUse.getTimeWithDelta() - strumTime - Conductor.instance.inputOffset) * scrollSpeed * (Preferences.downscroll ? 1 : -1); } public function updateNotes():Void diff --git a/source/funkin/play/song/SongSerializer.hx b/source/funkin/play/song/SongSerializer.hx deleted file mode 100644 index 3c6c18ab24b..00000000000 --- a/source/funkin/play/song/SongSerializer.hx +++ /dev/null @@ -1,76 +0,0 @@ -package funkin.play.song; - -import funkin.data.song.SongData.SongChartData; -import funkin.data.song.SongData.SongMetadata; -import funkin.util.FileUtil; -import openfl.net.FileReference; - -/** - * TODO: Refactor and remove this. - */ -class SongSerializer -{ - /** - * Access a SongChartData JSON file from a specific path, then load it. - * @param path The file path to read from. - */ - public static function importSongChartDataSync(path:String):SongChartData - { - var fileData = FileUtil.readStringFromPath(path); - - if (fileData == null) return null; - - var songChartData:SongChartData = fileData.parseJSON(); - - return songChartData; - } - - /** - * Access a SongMetadata JSON file from a specific path, then load it. - * @param path The file path to read from. - */ - public static function importSongMetadataSync(path:String):SongMetadata - { - var fileData = FileUtil.readStringFromPath(path); - - if (fileData == null) return null; - - var songMetadata:SongMetadata = fileData.parseJSON(); - - return songMetadata; - } - - /** - * Prompt the user to browse for a SongChartData JSON file path, then load it. - * @param callback The function to call when the file is loaded. - */ - public static function importSongChartDataAsync(callback:SongChartData->Void):Void - { - FileUtil.browseFileReference(function(fileReference:FileReference) { - var data = fileReference.data.toString(); - - if (data == null) return; - - var songChartData:SongChartData = data.parseJSON(); - - if (songChartData != null) callback(songChartData); - }); - } - - /** - * Prompt the user to browse for a SongMetadata JSON file path, then load it. - * @param callback The function to call when the file is loaded. - */ - public static function importSongMetadataAsync(callback:SongMetadata->Void):Void - { - FileUtil.browseFileReference(function(fileReference:FileReference) { - var data = fileReference.data.toString(); - - if (data == null) return; - - var songMetadata:SongMetadata = data.parseJSON(); - - if (songMetadata != null) callback(songMetadata); - }); - } -} diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx index cf432a52abe..22fa4580dd3 100644 --- a/source/funkin/play/stage/Stage.hx +++ b/source/funkin/play/stage/Stage.hx @@ -248,8 +248,8 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements propSprite.scrollFactor.y = dataProp.scroll[1]; propSprite.angle = dataProp.angle; - propSprite.color = FlxColor.fromString(dataProp.color); - @:privateAccess if (!isSolidColor) propSprite.blend = BlendMode.fromString(dataProp.blend); + if (!isSolidColor) propSprite.color = FlxColor.fromString(dataProp.color); + @:privateAccess propSprite.blend = BlendMode.fromString(dataProp.blend); propSprite.zIndex = dataProp.zIndex; diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx index 1ef2810df53..ceef12e204c 100644 --- a/source/funkin/save/Save.hx +++ b/source/funkin/save/Save.hx @@ -2,6 +2,7 @@ package funkin.save; import flixel.util.FlxSave; import funkin.input.Controls.Device; +import funkin.play.character.CharacterData.CharacterDataParser; import funkin.play.scoring.Scoring; import funkin.play.scoring.Scoring.ScoringRank; import funkin.save.migrator.RawSaveData_v1_0_0; @@ -127,8 +128,6 @@ class Save shouldHideMouse: true, fancyPreview: true, previewOnSave: true, - saveFormat: 'PNG', - jpegQuality: 80, }, controls: @@ -173,6 +172,10 @@ class Save metronomeVolume: 1.0, hitsoundVolumePlayer: 1.0, hitsoundVolumeOpponent: 1.0, + instVolume: 1.0, + playerVoiceVolume: 1.0, + opponentVoiceVolume: 1.0, + playbackSpeed: 0.5, themeMusic: true }, @@ -181,7 +184,10 @@ class Save previousFiles: [], moveStep: "1px", angleStep: 5, - theme: StageEditorTheme.Light + theme: StageEditorTheme.Light, + bfChar: "bf", + gfChar: "gf", + dadChar: "dad" } }; } @@ -407,6 +413,57 @@ class Save return data.optionsChartEditor.hitsoundVolumeOpponent; } + public var chartEditorInstVolume(get, set):Float; + + function get_chartEditorInstVolume():Float + { + if (data.optionsChartEditor.instVolume == null) data.optionsChartEditor.instVolume = 1.0; + + return data.optionsChartEditor.instVolume; + } + + function set_chartEditorInstVolume(value:Float):Float + { + // Set and apply. + data.optionsChartEditor.instVolume = value; + flush(); + return data.optionsChartEditor.instVolume; + } + + public var chartEditorPlayerVoiceVolume(get, set):Float; + + function get_chartEditorPlayerVoiceVolume():Float + { + if (data.optionsChartEditor.playerVoiceVolume == null) data.optionsChartEditor.playerVoiceVolume = 1.0; + + return data.optionsChartEditor.playerVoiceVolume; + } + + function set_chartEditorPlayerVoiceVolume(value:Float):Float + { + // Set and apply. + data.optionsChartEditor.playerVoiceVolume = value; + flush(); + return data.optionsChartEditor.playerVoiceVolume; + } + + public var chartEditorOpponentVoiceVolume(get, set):Float; + + function get_chartEditorOpponentVoiceVolume():Float + { + if (data.optionsChartEditor.opponentVoiceVolume == null) data.optionsChartEditor.opponentVoiceVolume = 1.0; + + return data.optionsChartEditor.opponentVoiceVolume; + } + + function set_chartEditorOpponentVoiceVolume(value:Float):Float + { + // Set and apply. + data.optionsChartEditor.opponentVoiceVolume = value; + flush(); + return data.optionsChartEditor.opponentVoiceVolume; + } + public var chartEditorThemeMusic(get, set):Bool; function get_chartEditorThemeMusic():Bool @@ -428,7 +485,7 @@ class Save function get_chartEditorPlaybackSpeed():Float { - if (data.optionsChartEditor.playbackSpeed == null) data.optionsChartEditor.playbackSpeed = 1.0; + if (data.optionsChartEditor.playbackSpeed == null) data.optionsChartEditor.playbackSpeed = 0.5; return data.optionsChartEditor.playbackSpeed; } @@ -550,6 +607,60 @@ class Save return data.optionsStageEditor.theme; } + public var stageBoyfriendChar(get, set):String; + + function get_stageBoyfriendChar():String + { + if (data.optionsStageEditor.bfChar == null + || CharacterDataParser.fetchCharacterData(data.optionsStageEditor.bfChar) == null) data.optionsStageEditor.bfChar = "bf"; + + return data.optionsStageEditor.bfChar; + } + + function set_stageBoyfriendChar(value:String):String + { + // Set and apply. + data.optionsStageEditor.bfChar = value; + flush(); + return data.optionsStageEditor.bfChar; + } + + public var stageGirlfriendChar(get, set):String; + + function get_stageGirlfriendChar():String + { + if (data.optionsStageEditor.gfChar == null + || CharacterDataParser.fetchCharacterData(data.optionsStageEditor.gfChar ?? "") == null) data.optionsStageEditor.gfChar = "gf"; + + return data.optionsStageEditor.gfChar; + } + + function set_stageGirlfriendChar(value:String):String + { + // Set and apply. + data.optionsStageEditor.gfChar = value; + flush(); + return data.optionsStageEditor.gfChar; + } + + public var stageDadChar(get, set):String; + + function get_stageDadChar():String + { + if (data.optionsStageEditor.dadChar == null + || CharacterDataParser.fetchCharacterData(data.optionsStageEditor.dadChar ?? "") == null) data.optionsStageEditor.dadChar = "dad"; + + return data.optionsStageEditor.dadChar; + } + + function set_stageDadChar(value:String):String + { + // Set and apply. + data.optionsStageEditor.dadChar = value; + flush(); + return data.optionsStageEditor.dadChar; + } + /** * When we've seen a character unlock, add it to the list of characters seen. * @param character @@ -1441,16 +1552,12 @@ typedef SaveDataOptions = * @param shouldHideMouse Should the mouse be hidden when taking a screenshot? Default: `true` * @param fancyPreview Show a fancy preview? Default: `true` * @param previewOnSave Only show the fancy preview after a screenshot is saved? Default: `true` - * @param saveFormat The save format of the screenshot, PNG or JPEG. Default: `PNG` - * @param jpegQuality The JPEG Quality, if we're saving to the format. Default: `80` */ var screenshot: { var shouldHideMouse:Bool; var fancyPreview:Bool; var previewOnSave:Bool; - var saveFormat:String; - var jpegQuality:Int; }; var controls: @@ -1653,10 +1760,16 @@ typedef SaveDataChartEditorOptions = var ?instVolume:Float; /** - * Voices volume in the Chart Editor. + * Player voice volume in the Chart Editor. + * @default `1.0` + */ + var ?playerVoiceVolume:Float; + + /** + * Opponent voice volume in the Chart Editor. * @default `1.0` */ - var ?voicesVolume:Float; + var ?opponentVoiceVolume:Float; /** * Playback speed in the Chart Editor. @@ -1699,4 +1812,22 @@ typedef SaveDataStageEditorOptions = * @default `StageEditorTheme.Light` */ var ?theme:StageEditorTheme; + + /** + * The BF character ID used in testing stages. + * @default bf + */ + var ?bfChar:String; + + /** + * The GF character ID used in testing stages. + * @default gf + */ + var ?gfChar:String; + + /** + * The Dad character ID used in testing stages. + * @default dad + */ + var ?dadChar:String; }; diff --git a/source/funkin/ui/MenuList.hx b/source/funkin/ui/MenuList.hx index 9e04da8defe..f5bd7c352f9 100644 --- a/source/funkin/ui/MenuList.hx +++ b/source/funkin/ui/MenuList.hx @@ -173,6 +173,12 @@ class MenuTypedList extends FlxTypedGroup } } + public function cancelAccept() + { + FlxFlicker.stopFlickering(members[selectedIndex]); + busy = false; + } + public function selectItem(index:Int) { members[selectedIndex].idle(); diff --git a/source/funkin/ui/charSelect/CharSelectSubState.hx b/source/funkin/ui/charSelect/CharSelectSubState.hx index 87f08960c8e..c0892fb3d71 100644 --- a/source/funkin/ui/charSelect/CharSelectSubState.hx +++ b/source/funkin/ui/charSelect/CharSelectSubState.hx @@ -62,7 +62,8 @@ class CharSelectSubState extends MusicBeatSubState var chooseDipshit:FlxSprite; var dipshitBlur:FlxSprite; var transitionGradient:FlxSprite; - var curChar(default, set):String = "pico"; + var curChar(default, set):String = Constants.DEFAULT_CHARACTER; + var rememberedChar:String; var nametag:Nametag; var camFollow:FlxObject; var autoFollow:Bool = false; @@ -87,9 +88,10 @@ class CharSelectSubState extends MusicBeatSubState var bopInfo:FramesJSFLInfo; var blackScreen:FunkinSprite; - public function new() + public function new(?params:CharSelectSubStateParams) { super(); + rememberedChar = params?.character; loadAvailableCharacters(); } @@ -167,18 +169,39 @@ class CharSelectSubState extends MusicBeatSubState charLightGF.loadGraphic(Paths.image('charSelect/charLight')); add(charLightGF); - gfChill = new CharSelectGF(); - gfChill.switchGF("bf"); - add(gfChill); - - playerChillOut = new CharSelectPlayer(0, 0); - playerChillOut.switchChar("bf"); - playerChillOut.visible = false; - add(playerChillOut); + function setupPlayerChill(character:String) + { + gfChill = new CharSelectGF(); + gfChill.switchGF(character); + add(gfChill); + + playerChillOut = new CharSelectPlayer(0, 0); + playerChillOut.switchChar(character); + playerChillOut.visible = false; + add(playerChillOut); + + playerChill = new CharSelectPlayer(0, 0); + playerChill.switchChar(character); + add(playerChill); + } - playerChill = new CharSelectPlayer(0, 0); - playerChill.switchChar("bf"); - add(playerChill); + // I think I can do the character preselect thing here? This better work + // Edit: [UH-OH!] yes! It does! + if (rememberedChar != null && rememberedChar != Constants.DEFAULT_CHARACTER) + { + setupPlayerChill(rememberedChar); + for (pos => charId in availableChars) + { + if (charId == rememberedChar) + { + setCursorPosition(pos); + break; + } + } + @:bypassAccessor curChar = rememberedChar; + } + else + setupPlayerChill(Constants.DEFAULT_CHARACTER); var speakers:FlxAtlasSprite = new FlxAtlasSprite(0, 0, Paths.animateAtlas("charSelect/charSelectSpeakers")); speakers.anim.play(""); @@ -224,7 +247,7 @@ class CharSelectSubState extends MusicBeatSubState dipshitBacking.scrollFactor.set(); dipshitBlur.scrollFactor.set(); - nametag = new Nametag(); + nametag = new Nametag(curChar); add(nametag); nametag.scrollFactor.set(); @@ -1063,6 +1086,25 @@ class CharSelectSubState extends MusicBeatSubState return gridPosition; } + // Moved this code into a function because is now used twice + function setCursorPosition(index:Int) + { + var copy = 3; + var yThing = -1; + + while ((index + 1) > copy) + { + yThing++; + copy += 3; + } + + var xThing = (copy - index - 2) * -1; + + // Look, I'd write better code but I had better aneurysms, my bad - Cheems + cursorY = yThing; + cursorX = xThing; + } + function set_curChar(value:String):String { if (curChar == value) return value; @@ -1113,3 +1155,11 @@ class CharSelectSubState extends MusicBeatSubState return value; } } + +/** + * Parameters used to initialize the CharSelectSubState. + */ +typedef CharSelectSubStateParams = +{ + ?character:String, // ?fromFreeplaySelect:Bool, +}; diff --git a/source/funkin/ui/charSelect/Nametag.hx b/source/funkin/ui/charSelect/Nametag.hx index 5bdb0e7e837..3bc51b2944f 100644 --- a/source/funkin/ui/charSelect/Nametag.hx +++ b/source/funkin/ui/charSelect/Nametag.hx @@ -10,14 +10,17 @@ class Nametag extends FlxSprite var midpointY(default, set):Float = 100; var mosaicShader:MosaicEffect; - public function new(?x:Float = 0, ?y:Float = 0) + public function new(?x:Float = 0, ?y:Float = 0, character:String) { super(x, y); mosaicShader = new MosaicEffect(); shader = mosaicShader; - switchChar("bf"); + // So that's why there was that cursed sight (originally defaulted to bf) + if (character != null) switchChar(character); + else + switchChar(Constants.DEFAULT_CHARACTER); } public function updatePosition():Void diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index ea1d21c97b1..a048d66cb89 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -30,6 +30,7 @@ import funkin.data.song.SongData.SongNoteData; import funkin.data.song.SongData.SongOffsets; import funkin.data.song.SongData.NoteParamData; import funkin.data.song.SongDataUtils; +import funkin.data.song.SongNoteDataUtils; import funkin.graphics.FunkinCamera; import funkin.graphics.FunkinSprite; import funkin.input.Cursor; @@ -61,6 +62,7 @@ import funkin.ui.debug.charting.commands.PasteItemsCommand; import funkin.ui.debug.charting.commands.RemoveEventsCommand; import funkin.ui.debug.charting.commands.RemoveItemsCommand; import funkin.ui.debug.charting.commands.RemoveNotesCommand; +import funkin.ui.debug.charting.commands.RemoveStackedNotesCommand; import funkin.ui.debug.charting.commands.SelectAllItemsCommand; import funkin.ui.debug.charting.commands.SelectItemsCommand; import funkin.ui.debug.charting.commands.SetItemSelectionCommand; @@ -88,6 +90,7 @@ import haxe.io.Bytes; import haxe.io.Path; import haxe.ui.backend.flixel.UIState; import haxe.ui.components.Button; +import haxe.ui.components.DropDown; import haxe.ui.components.Label; import haxe.ui.components.Slider; import haxe.ui.containers.dialogs.CollapsibleDialog; @@ -355,6 +358,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState value = 0; } + // Make sure playhead doesn't scroll outside the song. + if (value + playheadPositionInPixels < 0) playheadPositionInPixels = -value; + if (value + playheadPositionInPixels > songLengthInPixels) playheadPositionInPixels = songLengthInPixels - value; + if (value > songLengthInPixels) value = songLengthInPixels; if (value == scrollPositionInPixels) return value; @@ -779,6 +786,15 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ var currentLiveInputPlaceNoteData:Array = []; + /** + * Defines how "close" two notes must be to be considered stacked, based on steps. + * For example, setting this to `0.5` (16/32) will highlight notes half a step apart. + * Setting it to `0` only highlights notes that are nearly perfectly aligned. + * In the dropdown menu, the threshold is based on note snaps instead. + * For example, `0.5` would be displayed as `1/32`, and `0` would show as `Exact`. + */ + public static var stackedNoteThreshold:Float = 0; + // Note Movement /** @@ -851,6 +867,32 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState return currentNoteSelection; } + var currentOverlappingNotes(default, set):Array = []; + + function set_currentOverlappingNotes(value:Array):Array + { + // This value is true if all elements of the current overlapping array are also in the new array. + var isSuperset:Bool = currentOverlappingNotes.isSubset(value); + var isEqual:Bool = currentOverlappingNotes.isEqualUnordered(value); + + currentOverlappingNotes = value; + + if (!isEqual) + { + if (currentOverlappingNotes.length > 0 && isSuperset) + { + notePreview.addOverlappingNotes(currentOverlappingNotes, Std.int(songLengthInMs)); + } + else + { + // The new array might add or remove elements from the old array, so we have to redraw the note preview. + notePreviewDirty = true; + } + } + + return currentOverlappingNotes; + } + /** * The events which are currently in the user's selection. */ @@ -1294,6 +1336,16 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState function set_currentSongChartData(value:SongChartData):SongChartData { songChartData.set(selectedVariation, value); + var variationMetadata:Null = songMetadata.get(selectedVariation); + if (variationMetadata != null) + { + // Add the chartdata difficulties to the metadata difficulties if they don't exist so that the editor properly loads them + var keys:Array = [for (x in songChartData.get(selectedVariation).notes.keys()) x]; + for (key in keys) + { + variationMetadata.playData.difficulties.pushUnique(key); + } + } return value; } @@ -1742,6 +1794,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ var menubarItemDelete:MenuItem; + /** + * The `Edit -> Delete Stacked Notes` menu item. + */ + var menubarItemDeleteStacked:MenuItem; + /** * The `Edit -> Flip Notes` menu item. */ @@ -1787,6 +1844,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ var menuBarItemNoteSnapIncrease:MenuItem; + /** + * The `Edit -> Stacked Note Threshold` menu dropdown + */ + var menuBarStackedNoteThreshold:DropDown; + /** * The `View -> Downscroll` menu item. */ @@ -1978,9 +2040,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState /** * The IMAGE used for the selection squares. Updated by ChartEditorThemeHandler. - * Used two ways: + * Used three ways: * 1. A sprite is given this bitmap and placed over selected notes. - * 2. The image is split and used for a 9-slice sprite for the selection box. + * 2. Same as above but for notes that are overlapped by another. + * 3. The image is split and used for a 9-slice sprite for the selection box. */ var selectionSquareBitmap:Null = null; @@ -2231,7 +2294,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState } else if (params != null && params.targetSongId != null) { - this.loadSongAsTemplate(params.targetSongId); + var targetSongDifficulty = params.targetSongDifficulty ?? null; + var targetSongVariation = params.targetSongVariation ?? null; + this.loadSongAsTemplate(params.targetSongId, targetSongDifficulty, targetSongVariation); } else { @@ -2286,13 +2351,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState currentTheme = save.chartEditorTheme; metronomeVolume = save.chartEditorMetronomeVolume; hitsoundVolumePlayer = save.chartEditorHitsoundVolumePlayer; - hitsoundVolumePlayer = save.chartEditorHitsoundVolumeOpponent; + hitsoundVolumeOpponent = save.chartEditorHitsoundVolumeOpponent; this.welcomeMusic.active = save.chartEditorThemeMusic; - // audioInstTrack.volume = save.chartEditorInstVolume; - // audioInstTrack.pitch = save.chartEditorPlaybackSpeed; - // audioVocalTrackGroup.volume = save.chartEditorVoicesVolume; - // audioVocalTrackGroup.pitch = save.chartEditorPlaybackSpeed; + menubarItemVolumeInstrumental.value = Std.int(save.chartEditorInstVolume * 100); + menubarItemVolumeVocalsPlayer.value = Std.int(save.chartEditorPlayerVoiceVolume * 100); + menubarItemVolumeVocalsOpponent.value = Std.int(save.chartEditorOpponentVoiceVolume * 100); + menubarItemPlaybackSpeed.value = Std.int(save.chartEditorPlaybackSpeed * 100); } public function writePreferences(hasBackup:Bool):Void @@ -2318,9 +2383,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState save.chartEditorHitsoundVolumeOpponent = hitsoundVolumeOpponent; save.chartEditorThemeMusic = this.welcomeMusic.active; - // save.chartEditorInstVolume = audioInstTrack.volume; - // save.chartEditorVoicesVolume = audioVocalTrackGroup.volume; - // save.chartEditorPlaybackSpeed = audioInstTrack.pitch; + save.chartEditorInstVolume = menubarItemVolumeInstrumental.value / 100.0; + save.chartEditorPlayerVoiceVolume = menubarItemVolumeVocalsPlayer.value / 100.0; + save.chartEditorOpponentVoiceVolume = menubarItemVolumeVocalsOpponent.value / 100.0; + save.chartEditorPlaybackSpeed = menubarItemPlaybackSpeed.value / 100.0; } public function populateOpenRecentMenu():Void @@ -2469,7 +2535,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState add(gridPlayhead); gridPlayhead.zIndex = 30; - var playheadWidth:Int = GRID_SIZE * (STRUMLINE_SIZE * 2 + 1) + (PLAYHEAD_SCROLL_AREA_WIDTH * 2); + var playheadWidth:Int = GRID_SIZE * (STRUMLINE_SIZE * 2 + 1) + PLAYHEAD_SCROLL_AREA_WIDTH; var playheadBaseYPos:Float = GRID_INITIAL_Y_POS; gridPlayhead.setPosition(GRID_X_POS, playheadBaseYPos); var playheadSprite:FunkinSprite = new FunkinSprite().makeSolidColor(playheadWidth, PLAYHEAD_HEIGHT, PLAYHEAD_COLOR); @@ -2936,6 +3002,17 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState } }; + menubarItemDeleteStacked.onClick = _ -> { + if (currentEventSelection.length > 0 && currentNoteSelection.length == 0) + { + performCommand(new RemoveEventsCommand(currentEventSelection)); + } + else + { + performCommand(new RemoveStackedNotesCommand(currentNoteSelection.length > 0 ? currentNoteSelection : null)); + } + }; + menubarItemFlipNotes.onClick = _ -> performCommand(new FlipNotesCommand(currentNoteSelection)); menubarItemSelectAllNotes.onClick = _ -> performCommand(new SelectAllItemsCommand(true, false)); @@ -2958,6 +3035,21 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState if (noteSnapQuantIndex >= SNAP_QUANTS.length) noteSnapQuantIndex = 0; }; + final REVERSE_SNAPS = SNAP_QUANTS.reversed(); + for (snap in REVERSE_SNAPS) + { + menuBarStackedNoteThreshold.dataSource.add({text: '1/$snap'}); + } + + menuBarStackedNoteThreshold.onChange = event -> { + // NOTE: It needs to be offset by 1 because of the 'Exact' option + // -1 value means that it is the one selected + var selectedIdx:Int = menuBarStackedNoteThreshold.selectedIndex - 1; + stackedNoteThreshold = selectedIdx == -1 ? 0 : BASE_QUANT / REVERSE_SNAPS[selectedIdx]; + noteDisplayDirty = true; + notePreviewDirty = true; + } + menuBarItemInputStyleNone.onClick = function(event:UIEvent) { currentLiveInputStyle = None; }; @@ -2977,7 +3069,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState #if sys menubarItemGoToBackupsFolder.onClick = _ -> this.openBackupsFolder(); #else - // Disable the menu item if we're not on a desktop platform. + // Disable the menu item if we're not on a native platform. menubarItemGoToBackupsFolder.disabled = true; #end @@ -3486,7 +3578,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { // This sprite is off-screen or was deleted. // Kill the note sprite and recycle it. - noteSprite.noteData = null; + noteSprite.kill(); } } // Sort the note data array, using an algorithm that is fast on nearly-sorted data. @@ -3504,7 +3596,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // It will be displayed by gridGhostHoldNoteSprite instead. holdNoteSprite.kill(); } - else if (!holdNoteSprite.isHoldNoteVisible(FlxG.height - MENU_BAR_HEIGHT, GRID_TOP_PAD)) + else if (!holdNoteSprite.isHoldNoteVisible(viewAreaBottomPixels, viewAreaTopPixels)) { // This hold note is off-screen. // Kill the hold note sprite and recycle it. @@ -3528,7 +3620,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Update the event sprite's height and position. // var holdNoteHeight = holdNoteSprite.noteData.getStepLength() * GRID_SIZE; // holdNoteSprite.setHeightDirectly(holdNoteHeight); - holdNoteSprite.updateHoldNotePosition(renderedNotes); + holdNoteSprite.updateHoldNotePosition(renderedHoldNotes); } } // Sort the note data array, using an algorithm that is fast on nearly-sorted data. @@ -3544,7 +3636,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Resolve an issue where dragging an event too far would cause it to be hidden. var isSelectedAndDragged = currentEventSelection.fastContains(eventSprite.eventData) && (dragTargetCurrentStep != 0); - if ((eventSprite.isEventVisible(FlxG.height - PLAYBAR_HEIGHT, MENU_BAR_HEIGHT) + if ((eventSprite.isEventVisible(viewAreaBottomPixels, viewAreaTopPixels) && currentSongChartEventData.fastContains(eventSprite.eventData)) || isSelectedAndDragged) { @@ -3560,7 +3652,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { // This event was deleted. // Kill the event sprite and recycle it. - eventSprite.eventData = null; + eventSprite.kill(); } } // Sort the note data array, using an algorithm that is fast on nearly-sorted data. @@ -3617,6 +3709,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState var noteLengthPixels:Float = noteSprite.noteData.getStepLength() * GRID_SIZE; holdNoteSprite.noteData = noteSprite.noteData; + holdNoteSprite.overrideStepTime = null; + holdNoteSprite.overrideData = null; holdNoteSprite.noteDirection = noteSprite.noteData.getDirection(); holdNoteSprite.setHeightDirectly(noteLengthPixels); @@ -3684,6 +3778,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState var noteLengthPixels:Float = noteData.getStepLength() * GRID_SIZE; holdNoteSprite.noteData = noteData; + holdNoteSprite.overrideStepTime = null; + holdNoteSprite.overrideData = null; holdNoteSprite.noteDirection = noteData.getDirection(); holdNoteSprite.setHeightDirectly(noteLengthPixels); @@ -3701,13 +3797,32 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState member.kill(); } + // Gather stacked notes to render later + // No need to update it every time we scroll + if (Math.abs(currentScrollEase - scrollPositionInPixels) < .0001) + { + currentOverlappingNotes = SongNoteDataUtils.listStackedNotes(currentSongChartNoteData, stackedNoteThreshold); + } + // Readd selection squares for selected notes. // Recycle selection squares if possible. for (noteSprite in renderedNotes.members) { + if (noteSprite == null || noteSprite.noteData == null || !noteSprite.exists || !noteSprite.visible) continue; + // TODO: Handle selection of hold notes. if (isNoteSelected(noteSprite.noteData)) { + var holdNoteSprite:ChartEditorHoldNoteSprite = null; + + if (noteSprite.noteData != null && noteSprite.noteData.length > 0) + { + for (holdNote in renderedHoldNotes.members) + { + if (holdNote.noteData == noteSprite.noteData && holdNoteSprite == null) holdNoteSprite = holdNote; + } + } + // Determine if the note is being dragged and offset the vertical position accordingly. if (dragTargetCurrentStep != 0.0) { @@ -3716,6 +3831,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState noteSprite.overrideStepTime = (stepTime + dragTargetCurrentStep).clamp(0, songLengthInSteps - (1 * noteSnapRatio)); // Then reapply the note sprite's position relative to the grid. noteSprite.updateNotePosition(renderedNotes); + + // We only need to update the position of the hold note tails as we drag the note. + if (holdNoteSprite != null) + { + holdNoteSprite.overrideStepTime = noteSprite.overrideStepTime; + holdNoteSprite.updateHoldNotePosition(renderedHoldNotes); + } } else { @@ -3725,6 +3847,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState noteSprite.overrideStepTime = null; // Then reapply the note sprite's position relative to the grid. noteSprite.updateNotePosition(renderedNotes); + + if (holdNoteSprite != null) + { + holdNoteSprite.overrideStepTime = null; + holdNoteSprite.updateHoldNotePosition(renderedHoldNotes); + } } } @@ -3737,6 +3865,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState ChartEditorState.STRUMLINE_SIZE * 2 - 1)); // Then reapply the note sprite's position relative to the grid. noteSprite.updateNotePosition(renderedNotes); + + // We only need to update the position of the hold note tails as we drag the note. + if (holdNoteSprite != null) + { + holdNoteSprite.overrideData = noteSprite.overrideData; + holdNoteSprite.updateHoldNotePosition(renderedHoldNotes); + } } else { @@ -3746,6 +3881,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState noteSprite.overrideData = null; // Then reapply the note sprite's position relative to the grid. noteSprite.updateNotePosition(renderedNotes); + + if (holdNoteSprite != null) + { + holdNoteSprite.overrideData = null; + holdNoteSprite.noteDirection = noteSprite.noteData.getDirection(); + holdNoteSprite.updateHoldNotePosition(renderedHoldNotes); + } } } @@ -3758,14 +3900,30 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState selectionSquare.x = noteSprite.x; selectionSquare.y = noteSprite.y; selectionSquare.width = GRID_SIZE; + selectionSquare.color = FlxColor.WHITE; var stepLength = noteSprite.noteData.getStepLength(); selectionSquare.height = (stepLength <= 0) ? GRID_SIZE : ((stepLength + 1) * GRID_SIZE); } + else if (doesNoteStack(noteSprite.noteData, currentOverlappingNotes)) + { + // TODO: Maybe use another way to display these notes + var selectionSquare:ChartEditorSelectionSquareSprite = renderedSelectionSquares.recycle(buildSelectionSquare); + + // Set the position and size (because we might be recycling one with bad values). + selectionSquare.noteData = noteSprite.noteData; + selectionSquare.eventData = null; + selectionSquare.x = noteSprite.x; + selectionSquare.y = noteSprite.y; + selectionSquare.width = selectionSquare.height = GRID_SIZE; + selectionSquare.color = FlxColor.RED; + } } for (eventSprite in renderedEvents.members) { + if (eventSprite == null || eventSprite.eventData == null || !eventSprite.exists || !eventSprite.visible) continue; + if (isEventSelected(eventSprite.eventData)) { // Determine if the note is being dragged and offset the position accordingly. @@ -3797,6 +3955,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState selectionSquare.y = eventSprite.y; selectionSquare.width = eventSprite.width; selectionSquare.height = eventSprite.height; + selectionSquare.color = FlxColor.WHITE; } // Additional cleanup on notes. @@ -4371,7 +4530,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState else if (highlightedHoldNote != null && highlightedHoldNote.noteData != null) { // Control click to select/deselect an individual note. - if (isNoteSelected(highlightedNote.noteData)) + if (isNoteSelected(highlightedHoldNote.noteData)) { performCommand(new DeselectItemsCommand([highlightedHoldNote.noteData], [])); } @@ -4663,7 +4822,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState } else if (highlightedHoldNote != null && highlightedHoldNote.noteData != null) { - if (isNoteSelected(highlightedNote.noteData)) + if (isNoteSelected(highlightedHoldNote.noteData)) { performCommand(new DeselectItemsCommand([highlightedHoldNote.noteData], [])); } @@ -5107,7 +5266,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState playbarHeadLayout.y = FlxG.height - 48 - 8; var songPos:Float = Conductor.instance.songPosition + Conductor.instance.instrumentalOffset; - var songPosMilliseconds:String = Std.string(Math.floor(Math.abs(songPos) % 1000)).lpad('0', 2).substr(0, 2); + var songPosMilliseconds:String = Std.string(Math.floor(Math.abs(songPos) % 1000)).lpad('0', 3).substr(0, 2); var songPosSeconds:String = Std.string(Math.floor((Math.abs(songPos) / 1000) % 60)).lpad('0', 2); var songPosMinutes:String = Std.string(Math.floor((Math.abs(songPos) / 1000) / 60)).lpad('0', 2); if (songPos < 0) songPosMinutes = '-' + songPosMinutes; @@ -5501,18 +5660,36 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState if (delete) { - // Delete selected items. - if (currentNoteSelection.length > 0 && currentEventSelection.length > 0) - { - performCommand(new RemoveItemsCommand(currentNoteSelection, currentEventSelection)); - } - else if (currentNoteSelection.length > 0) + var noteSelection = currentNoteSelection.length > 0; + var eventSelection = currentEventSelection.length > 0; + + // Shift to delete stacked notes + if (FlxG.keys.pressed.SHIFT) { - performCommand(new RemoveNotesCommand(currentNoteSelection)); + if (eventSelection && !noteSelection) + { + performCommand(new RemoveEventsCommand(currentEventSelection)); + } + else + { + performCommand(new RemoveStackedNotesCommand(noteSelection ? currentNoteSelection : null)); + } } - else if (currentEventSelection.length > 0) + else { - performCommand(new RemoveEventsCommand(currentEventSelection)); + // Delete selected items. + if (noteSelection && eventSelection) + { + performCommand(new RemoveItemsCommand(currentNoteSelection, currentEventSelection)); + } + else if (noteSelection) + { + performCommand(new RemoveNotesCommand(currentNoteSelection)); + } + else if (eventSelection) + { + performCommand(new RemoveEventsCommand(currentEventSelection)); + } } } @@ -6145,9 +6322,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState fadeInWelcomeMusic(WELCOME_MUSIC_FADE_IN_DELAY, WELCOME_MUSIC_FADE_IN_DURATION); // Reapply the volume. - var instTargetVolume:Float = menubarItemVolumeInstrumental.value ?? 1.0; - var vocalPlayerTargetVolume:Float = menubarItemVolumeVocalsPlayer.value ?? 1.0; - var vocalOpponentTargetVolume:Float = menubarItemVolumeVocalsOpponent.value ?? 1.0; + var instTargetVolume:Float = menubarItemVolumeInstrumental.value / 100.0 ?? 1.0; + var vocalPlayerTargetVolume:Float = menubarItemVolumeVocalsPlayer.value / 100.0 ?? 1.0; + var vocalOpponentTargetVolume:Float = menubarItemVolumeVocalsOpponent.value / 100.0 ?? 1.0; if (audioInstTrack != null) { @@ -6188,6 +6365,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // TODO: Only update the notes that have changed. notePreview.erase(); notePreview.addNotes(currentSongChartNoteData, Std.int(songLengthInMs)); + notePreview.addOverlappingNotes(currentOverlappingNotes, Std.int(songLengthInMs)); notePreview.addSelectedNotes(currentNoteSelection, Std.int(songLengthInMs)); notePreview.addEvents(currentSongChartEventData, Std.int(songLengthInMs)); } @@ -6372,6 +6550,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState return note != null && currentNoteSelection.indexOf(note) != -1; } + function doesNoteStack(note:Null, curStackedNotes:Array):Bool + { + return note != null && curStackedNotes.contains(note); + } + override function destroy():Void { super.destroy(); @@ -6521,6 +6704,16 @@ typedef ChartEditorParams = * If non-null, load this song immediately instead of the welcome screen. */ var ?targetSongId:String; + + /** + * If non-null, load this difficulty immediately instead of the default difficulty. + */ + var ?targetSongDifficulty:String; + + /** + * If non-null, load this variation immediately instead of the default variation. + */ + var ?targetSongVariation:String; }; /** diff --git a/source/funkin/ui/debug/charting/commands/ChangeStartingBPMCommand.hx b/source/funkin/ui/debug/charting/commands/ChangeStartingBPMCommand.hx index a3728f33742..5995cf58918 100644 --- a/source/funkin/ui/debug/charting/commands/ChangeStartingBPMCommand.hx +++ b/source/funkin/ui/debug/charting/commands/ChangeStartingBPMCommand.hx @@ -37,7 +37,6 @@ class ChangeStartingBPMCommand implements ChartEditorCommand state.noteDisplayDirty = true; state.notePreviewDirty = true; state.notePreviewViewportBoundsDirty = true; - state.scrollPositionInPixels = 0; Conductor.instance.mapTimeChanges(state.currentSongMetadata.timeChanges); @@ -61,7 +60,6 @@ class ChangeStartingBPMCommand implements ChartEditorCommand state.noteDisplayDirty = true; state.notePreviewDirty = true; state.notePreviewViewportBoundsDirty = true; - state.scrollPositionInPixels = 0; Conductor.instance.mapTimeChanges(state.currentSongMetadata.timeChanges); diff --git a/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx b/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx index 257db94b45a..528704f829a 100644 --- a/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx +++ b/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx @@ -4,6 +4,8 @@ import funkin.data.song.SongData.SongEventData; import funkin.data.song.SongData.SongNoteData; import funkin.data.song.SongDataUtils; import funkin.data.song.SongDataUtils.SongClipboardItems; +import funkin.data.song.SongNoteDataUtils; +import funkin.ui.debug.charting.ChartEditorState; /** * A command which inserts the contents of the clipboard into the chart editor. @@ -13,9 +15,11 @@ import funkin.data.song.SongDataUtils.SongClipboardItems; class PasteItemsCommand implements ChartEditorCommand { var targetTimestamp:Float; - // Notes we added with this command, for undo. + // Notes we added and removed with this command, for undo. var addedNotes:Array = []; var addedEvents:Array = []; + var removedNotes:Array = []; + var isRedo:Bool = false; public function new(targetTimestamp:Float) { @@ -41,7 +45,10 @@ class PasteItemsCommand implements ChartEditorCommand addedEvents = SongDataUtils.offsetSongEventData(currentClipboard.events, Std.int(targetTimestamp)); addedEvents = SongDataUtils.clampSongEventData(addedEvents, 0.0, msCutoff); - state.currentSongChartNoteData = state.currentSongChartNoteData.concat(addedNotes); + removedNotes.clear(); + var mergedNotes:Array = SongNoteDataUtils.concatOverwrite(state.currentSongChartNoteData, addedNotes, removedNotes); + + state.currentSongChartNoteData = mergedNotes; state.currentSongChartEventData = state.currentSongChartEventData.concat(addedEvents); state.currentNoteSelection = addedNotes.copy(); state.currentEventSelection = addedEvents.copy(); @@ -52,29 +59,49 @@ class PasteItemsCommand implements ChartEditorCommand state.sortChartData(); - state.success('Paste Successful', 'Successfully pasted clipboard contents.'); + var title = isRedo ? 'Redone Paste Successfully' : 'Paste Successful'; + var msgType = removedNotes.length > 0 ? 'warning' : 'success'; + var msg = if (removedNotes.length == 1) + { + 'But 1 overlapped note was overwritten.'; + } + else if (removedNotes.length > 1) + { + 'But ${removedNotes.length} overlapped notes were overwritten.'; + } + else if (isRedo) + { + 'Successfully placed pasted note(s) back.'; + } + else 'Successfully pasted clipboard contents.'; + + Reflect.callMethod(null, Reflect.field(ChartEditorNotificationHandler, msgType), [state, title, msg]); + + isRedo = false; } public function undo(state:ChartEditorState):Void { state.playSound(Paths.sound('chartingSounds/undo')); - state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, addedNotes); + state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, addedNotes).concat(removedNotes); state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, addedEvents); - state.currentNoteSelection = []; state.currentEventSelection = []; + state.performCommand(new SelectItemsCommand(removedNotes.copy()), false); state.saveDataDirty = true; state.noteDisplayDirty = true; state.notePreviewDirty = true; state.sortChartData(); + + isRedo = true; } public function shouldAddToHistory(state:ChartEditorState):Bool { // This command is undoable. Add to the history if we actually performed an action. - return (addedNotes.length > 0 || addedEvents.length > 0); + return (addedNotes.length > 0 || addedEvents.length > 0 || removedNotes.length > 0); } public function toString():String diff --git a/source/funkin/ui/debug/charting/commands/RemoveStackedNotesCommand.hx b/source/funkin/ui/debug/charting/commands/RemoveStackedNotesCommand.hx new file mode 100644 index 00000000000..8add3026b6c --- /dev/null +++ b/source/funkin/ui/debug/charting/commands/RemoveStackedNotesCommand.hx @@ -0,0 +1,82 @@ +package funkin.ui.debug.charting.commands; + +import funkin.data.song.SongData.SongNoteData; +import funkin.data.song.SongDataUtils; +import funkin.data.song.SongNoteDataUtils; + +/** + * Deletes the given notes from the current chart in the chart editor if any overlap another. + * Use when ONLY notes are being deleted. + */ +@:nullSafety +@:access(funkin.ui.debug.charting.ChartEditorState) +class RemoveStackedNotesCommand implements ChartEditorCommand +{ + var notes:Null>; + var overlappedNotes:Array; + var removedNotes:Array; + + public function new(?notes:Array) + { + this.notes = notes; + this.overlappedNotes = []; + this.removedNotes = []; + } + + public function execute(state:ChartEditorState):Void + { + var isSelection:Bool = notes != null; + var notes:Array = notes ?? state.currentSongChartNoteData; + + if (notes.length == 0) return; + + overlappedNotes.clear(); + removedNotes = SongNoteDataUtils.listStackedNotes(notes, ChartEditorState.stackedNoteThreshold, false, overlappedNotes); + if (removedNotes.length == 0) return; + + state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, removedNotes); + state.currentNoteSelection = isSelection ? overlappedNotes.copy() : []; + state.currentEventSelection = []; + + state.playSound(Paths.sound('chartingSounds/noteErase')); + + state.saveDataDirty = true; + state.noteDisplayDirty = true; + state.notePreviewDirty = true; + + state.sortChartData(); + } + + public function undo(state:ChartEditorState):Void + { + if (removedNotes.length == 0) return; + + state.currentSongChartNoteData = state.currentSongChartNoteData.concat(removedNotes); + state.currentNoteSelection = overlappedNotes.concat(removedNotes).copy(); + state.currentEventSelection = []; + state.playSound(Paths.sound('chartingSounds/undo')); + + state.saveDataDirty = true; + state.noteDisplayDirty = true; + state.notePreviewDirty = true; + + state.sortChartData(); + } + + public function shouldAddToHistory(state:ChartEditorState):Bool + { + // This command is undoable. Add to the history if we actually performed an action. + return removedNotes.length > 0; + } + + public function toString():String + { + if (removedNotes.length == 1 && removedNotes[0] != null) + { + var dir:String = removedNotes[0].getDirectionName(); + return 'Remove $dir Stacked Note'; + } + + return 'Remove ${removedNotes.length} Stacked Notes'; + } +} diff --git a/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx index 645e51625b5..dc7ad45d70b 100644 --- a/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx +++ b/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx @@ -123,7 +123,6 @@ class ChartEditorEventSprite extends FlxSprite public function correctAnimationName(name:String):String { if (this.animation.exists(name)) return name; - trace('Warning: Invalid animation name "${name}" for song event. Using "${DEFAULT_EVENT}"'); return DEFAULT_EVENT; } diff --git a/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx index e4012d5525d..00d87e9fd4a 100644 --- a/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx +++ b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx @@ -37,6 +37,30 @@ class ChartEditorHoldNoteSprite extends SustainTrail return value; } + public var overrideStepTime(default, set):Null = null; + + function set_overrideStepTime(value:Null):Null + { + if (overrideStepTime == value) return overrideStepTime; + + overrideStepTime = value; + updateHoldNotePosition(); + return overrideStepTime; + } + + public var overrideData(default, set):Null = null; + + function set_overrideData(value:Null):Null + { + if (overrideData == value) return overrideData; + + overrideData = value; + if (overrideData != null) this.noteDirection = overrideData; + updateHoldNoteGraphic(); + updateHoldNotePosition(); + return overrideData; + } + public function new(parent:ChartEditorState) { var noteStyle = NoteStyleRegistry.instance.fetchDefault(); @@ -211,7 +235,7 @@ class ChartEditorHoldNoteSprite extends SustainTrail { if (this.noteData == null) return; - var cursorColumn:Int = this.noteData.data; + var cursorColumn:Int = (overrideData != null) ? overrideData : this.noteData.data; if (cursorColumn < 0) cursorColumn = 0; if (cursorColumn >= (ChartEditorState.STRUMLINE_SIZE * 2 + 1)) @@ -232,10 +256,11 @@ class ChartEditorHoldNoteSprite extends SustainTrail } this.x = cursorColumn * ChartEditorState.GRID_SIZE; + updateHoldNoteGraphic(); // Notes far in the song will start far down, but the group they belong to will have a high negative offset. // noteData.getStepTime() returns a calculated value which accounts for BPM changes - var stepTime:Float = + var stepTime:Float = (overrideStepTime != null) ? overrideStepTime : inline this.noteData.getStepTime(); if (stepTime >= 0) { diff --git a/source/funkin/ui/debug/charting/components/ChartEditorNotePreview.hx b/source/funkin/ui/debug/charting/components/ChartEditorNotePreview.hx index 8d9ec674375..4e4e5b31338 100644 --- a/source/funkin/ui/debug/charting/components/ChartEditorNotePreview.hx +++ b/source/funkin/ui/debug/charting/components/ChartEditorNotePreview.hx @@ -27,6 +27,7 @@ class ChartEditorNotePreview extends FlxSprite static final RIGHT_COLOR:FlxColor = 0xFFCC1111; static final EVENT_COLOR:FlxColor = 0xFF111111; static final SELECTED_COLOR:FlxColor = 0xFFFFFF00; + static final OVERLAPPING_COLOR:FlxColor = 0xFF640000; var previewHeight:Int; @@ -58,21 +59,22 @@ class ChartEditorNotePreview extends FlxSprite * @param note The data for the note. * @param songLengthInMs The total length of the song in milliseconds. */ - public function addNote(note:SongNoteData, songLengthInMs:Int, ?isSelection:Bool = false):Void + public function addNote(note:SongNoteData, songLengthInMs:Int, previewType:NotePreviewType = None):Void { var noteDir:Int = note.getDirection(); var mustHit:Bool = note.getStrumlineIndex() == 0; - drawNote(noteDir, mustHit, Std.int(note.time), songLengthInMs, isSelection); + drawNote(noteDir, mustHit, Std.int(note.time), songLengthInMs, previewType); } /** * Add a song event to the preview. * @param event The data for the event. * @param songLengthInMs The total length of the song in milliseconds. + * @param isSelection If current event is selected, which then it's forced to be yellow. */ - public function addEvent(event:SongEventData, songLengthInMs:Int, ?isSelection:Bool = false):Void + public function addEvent(event:SongEventData, songLengthInMs:Int, isSelection:Bool = false):Void { - drawNote(-1, false, Std.int(event.time), songLengthInMs, isSelection); + drawNote(-1, false, Std.int(event.time), songLengthInMs, isSelection ? Selection : None); } /** @@ -84,7 +86,7 @@ class ChartEditorNotePreview extends FlxSprite { for (note in notes) { - addNote(note, songLengthInMs, false); + addNote(note, songLengthInMs, None); } } @@ -97,7 +99,20 @@ class ChartEditorNotePreview extends FlxSprite { for (note in notes) { - addNote(note, songLengthInMs, true); + addNote(note, songLengthInMs, Selection); + } + } + + /** + * Add an array of overlapping notes to the preview. + * @param notes The data for the notes + * @param songLengthInMs The total length of the song in milliseconds. + */ + public function addOverlappingNotes(notes:Array, songLengthInMs:Int):Void + { + for (note in notes) + { + addNote(note, songLengthInMs, Overlapping); } } @@ -133,9 +148,9 @@ class ChartEditorNotePreview extends FlxSprite * @param mustHit False if opponent, true if player. * @param strumTimeInMs Time in milliseconds to strum the note. * @param songLengthInMs Length of the song in milliseconds. - * @param isSelection If current note is selected note, which then it's forced to be green + * @param previewType If the note should forcibly be colored as selected or overlapping. */ - public function drawNote(dir:Int, mustHit:Bool, strumTimeInMs:Int, songLengthInMs:Int, ?isSelection:Bool = false):Void + public function drawNote(dir:Int, mustHit:Bool, strumTimeInMs:Int, songLengthInMs:Int, previewType:NotePreviewType = None):Void { var color:FlxColor = switch (dir) { @@ -148,10 +163,15 @@ class ChartEditorNotePreview extends FlxSprite var noteHeight:Int = NOTE_HEIGHT; - if (isSelection != null && isSelection) + switch (previewType) { - color = SELECTED_COLOR; - noteHeight += 1; + case Selection: + color = SELECTED_COLOR; + noteHeight += 1; + case Overlapping: + color = OVERLAPPING_COLOR; + noteHeight += 2; + default: } var noteX:Float = NOTE_WIDTH * dir; @@ -178,3 +198,10 @@ class ChartEditorNotePreview extends FlxSprite FlxSpriteUtil.drawRect(this, noteX, noteY, width, height, color); } } + +enum NotePreviewType +{ + None; + Selection; + Overlapping; +} diff --git a/source/funkin/ui/debug/charting/dialogs/ChartEditorCharacterIconSelectorMenu.hx b/source/funkin/ui/debug/charting/dialogs/ChartEditorCharacterIconSelectorMenu.hx index 2c9de5c6254..01b7c59ac48 100644 --- a/source/funkin/ui/debug/charting/dialogs/ChartEditorCharacterIconSelectorMenu.hx +++ b/source/funkin/ui/debug/charting/dialogs/ChartEditorCharacterIconSelectorMenu.hx @@ -22,7 +22,8 @@ class ChartEditorCharacterIconSelectorMenu extends ChartEditorBaseMenu public var charSelectScroll:ScrollView; public var charIconName:Label; - var currentCharButton:Button; + var currentCharButton:Null