From 903b3fc59905a70802618a1cd67407722ea956ed Mon Sep 17 00:00:00 2001 From: Kolo <67389779+JustKolosaki@users.noreply.github.com> Date: Sat, 5 Oct 2024 10:29:26 +0200 Subject: [PATCH 01/44] fix: Don't restart the FreeplayState song preview when changing the difficulty within the same variation pulled from the wrong branch, oops! last update i hope wrong spellings im gonna kms --- source/funkin/ui/freeplay/FreeplayState.hx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index 507c44758..351de6b97 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -1689,6 +1689,7 @@ class FreeplayState extends MusicBeatSubState function changeDiff(change:Int = 0, force:Bool = false):Void { touchTimer = 0; + var previousVariation:String = currentVariation; // Available variations for current character. We get this since bf is usually `default` variation, and `pico` is `pico` // but sometimes pico can be the default variation (weekend 1 songs), and bf can be `bf` variation (darnell) @@ -1784,7 +1785,7 @@ class FreeplayState extends MusicBeatSubState } // Reset the song preview in case we changed variations (normal->erect etc) - playCurSongPreview(); + if (currentVariation != previousVariation) playCurSongPreview(); } // Set the album graphic and play the animation if relevant. From 3267c612587017298ab9f2e7d368331e9ac59285 Mon Sep 17 00:00:00 2001 From: Abnormal <86753001+AbnormalPoof@users.noreply.github.com> Date: Sun, 6 Oct 2024 18:25:04 -0500 Subject: [PATCH 02/44] blacklist one additional class --- source/funkin/modding/PolymodHandler.hx | 1 + 1 file changed, 1 insertion(+) diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx index f3d9273b2..eb0e77fc5 100644 --- a/source/funkin/modding/PolymodHandler.hx +++ b/source/funkin/modding/PolymodHandler.hx @@ -288,6 +288,7 @@ class PolymodHandler Polymod.blacklistImport('openfl.utils.Assets'); Polymod.blacklistImport('openfl.Lib'); Polymod.blacklistImport('openfl.system.ApplicationDomain'); + Polymod.blacklistImport('funkin.util.FunkinTypeResolver'); // `openfl.desktop.NativeProcess` // Can load native processes on the host operating system. From 9695e791349cb8f72814d69e6bf9c34148296fce Mon Sep 17 00:00:00 2001 From: Hundrec Date: Fri, 4 Oct 2024 21:20:45 -0700 Subject: [PATCH 03/44] Label me Docs --- .github/labeler.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/labeler.yml b/.github/labeler.yml index e8250b4e7..9cf784f4c 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,11 +1,11 @@ -# Add Documentation tag to PR's changing markdown files, or anyhting in the docs folder +# Add Documentation tag to PR's changing markdown files, or anything in the docs folder Documentation: - changed-files: - any-glob-to-any-file: - docs/* - '**/*.md' -# Adds Haxe tag to PR's changing haxe code files +# Add Haxe tag to PR's changing haxe code files Haxe: - changed-files: - any-glob-to-any-file: '**/*.hx' From f6a5a9d8ce45ea3ec358df874f05053ad35734bd Mon Sep 17 00:00:00 2001 From: Hundrec Date: Fri, 4 Oct 2024 21:27:34 -0700 Subject: [PATCH 04/44] Wait now it should do it --- docs/COMPILING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/COMPILING.md b/docs/COMPILING.md index ce3d89e7b..f2b59acbc 100644 --- a/docs/COMPILING.md +++ b/docs/COMPILING.md @@ -34,7 +34,7 @@ There are several useful build flags you can add to a build to affect how it wor - `-DGITHUB_BUILD` will enable in-game debug functions (such as the ability to time travel in a song by pressing `PgUp`/`PgDn`), without enabling the other stuff - `-DFEATURE_POLYMOD_MODS` or `-DNO_FEATURE_POLYMOD_MODS` to forcibly enable or disable modding support. - `-DREDIRECT_ASSETS_FOLDER` or `-DNO_REDIRECT_ASSETS_FOLDER` to forcibly enable or disable asset redirection. - - This feature causes the game to load exported assets from the project's assets folder rather than the exported one. Great for fast iteration, but the game + - This feature causes the game to load exported assets from the project's assets folder rather than the exported one. Great for fast iteration, but the game _(finish this sentence Eric)_ - `-DFEATURE_DISCORD_RPC` or `-DNO_FEATURE_DISCORD_RPC` to forcibly enable or disable support for Discord Rich Presence. - `-DFEATURE_VIDEO_PLAYBACK` or `-DNO_FEATURE_VIDEO_PLAYBACK` to forcibly enable or disable video cutscene support. - `-DFEATURE_CHART_EDITOR` or `-DNO_FEATURE_CHART_EDITOR` to forcibly enable or disable the chart editor in the Debug menu. From 5d10bb9f5489f1e693a113880fd31ab2042e3765 Mon Sep 17 00:00:00 2001 From: Hundrec Date: Fri, 4 Oct 2024 21:56:49 -0700 Subject: [PATCH 05/44] Rename troubleshooting.md to fix link in COMPILING.md --- docs/{troubleshooting.md => TROUBLESHOOTING.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/{troubleshooting.md => TROUBLESHOOTING.md} (100%) diff --git a/docs/troubleshooting.md b/docs/TROUBLESHOOTING.md similarity index 100% rename from docs/troubleshooting.md rename to docs/TROUBLESHOOTING.md From 6848815e801e15064e4dfd9d3817a32eeae2f41a Mon Sep 17 00:00:00 2001 From: Hundrec Date: Fri, 4 Oct 2024 22:03:33 -0700 Subject: [PATCH 06/44] Adjust line count labeler comment --- .github/changed-lines-count-labeler.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/changed-lines-count-labeler.yml b/.github/changed-lines-count-labeler.yml index 6f890f534..1de00e62f 100644 --- a/.github/changed-lines-count-labeler.yml +++ b/.github/changed-lines-count-labeler.yml @@ -7,6 +7,6 @@ medium: min: 10 max: 99 -# Add 'large' to any changes for more than 100 lines +# Add 'large' to any changes of at least 100 lines large: min: 100 From 7d5d80d515715501f8d2178539addfa16962bfba Mon Sep 17 00:00:00 2001 From: Hundrec Date: Fri, 4 Oct 2024 22:13:02 -0700 Subject: [PATCH 07/44] Fix typos in CODESTYLE.md --- CODESTYLE.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CODESTYLE.md b/CODESTYLE.md index 2641febfa..2b6333258 100644 --- a/CODESTYLE.md +++ b/CODESTYLE.md @@ -4,14 +4,14 @@ Code style is enforced using Visual Studio Code extensions. ## .hx Formatting is handled by the `nadako.vshaxe` extension, which includes Haxe Formatter. -Haxe Formatter automatically resolves issues such as intentation style and line breaks, and can be configured in `hxformat.json`. +Haxe Formatter automatically resolves issues such as indentation style and line breaks, and can be configured in `hxformat.json`. Code Quality is handled by the `vshaxe.haxe-checkstyle` extension, which includes Haxe Checkstyle. ### Haxe Checkstyle Notes -* Checks can be escalated to display as different serverities in the Problems window. +* Checks can be escalated to display as different severities in the Problems window. * Checks can be disabled by setting the severity to `IGNORE`. -* `IndentationCharacter` checks what is used to indent, `Indentation` checks how deep the intentation is. +* `IndentationCharacter` checks what is used to indent, `Indentation` checks how deep the indentation is. * `CommentedOutCode` check is in place because old code should be retrieved via Git history. * TODO items: Enable these one-by-one and fix them to improve the overall code quality. - Reconfigure `MethodLength` From 880615e707accf8a5ce8a2b7fcf128c1fb0222c8 Mon Sep 17 00:00:00 2001 From: Hundrec Date: Fri, 4 Oct 2024 22:54:28 -0700 Subject: [PATCH 08/44] Remove extra asterisks in Funkin' Debug Hotkeys.md Those stars might have been indicators for something, but I don't see any note related to them. --- docs/Funkin' Debug Hotkeys.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/Funkin' Debug Hotkeys.md b/docs/Funkin' Debug Hotkeys.md index 1287d5a1b..416c89714 100644 --- a/docs/Funkin' Debug Hotkeys.md +++ b/docs/Funkin' Debug Hotkeys.md @@ -15,8 +15,8 @@ Most of this functionality is only available on debug builds of the game! - `2`: ***GAIN HEALTH***: Debug function, add 10% to the player's health. - `3`: ***LOSE HEALTH***: Debug function, subtract 5% to the player's health. - `9`: NEATO! -- `PAGEUP` (MacOS: `Fn-Up`): ***FORWARDS TIME TRAVEL****: Move forward by 2 sections. Hold SHIFT to move forward by 20 sections instead. -- `PAGEDOWN` (MacOS: `Fn-Down`): ***BACKWARDS TIME TRAVEL****: Move backward by 2 sections. Hold SHIFT to move backward by 20 sections instead. +- `PAGEUP` (MacOS: `Fn-Up`): ***FORWARDS TIME TRAVEL***: Move forward by 2 sections. Hold SHIFT to move forward by 20 sections instead. +- `PAGEDOWN` (MacOS: `Fn-Down`): ***BACKWARDS TIME TRAVEL***: Move backward by 2 sections. Hold SHIFT to move backward by 20 sections instead. ## **Freeplay State** - `F` (Freeplay Menu) - Move to Favorites @@ -27,5 +27,5 @@ Most of this functionality is only available on debug builds of the game! - `Y` - WOAH ## **Main Menu** -- `~`: ***DEBUG****: Opens a menu to access the Chart Editor and other work-in-progress editors. Rebindable in the options menu. +- `~`: ***DEBUG***: Opens a menu to access the Chart Editor and other work-in-progress editors. Rebindable in the options menu. - `CTRL-ALT-SHIFT-W`: ***ALL ACCESS***: Unlocks all songs in Freeplay. Only available on debug builds. From 31d1b3e2464120c2608995e968357ef85c004535 Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 8 Oct 2024 14:12:55 -0400 Subject: [PATCH 09/44] Finish the sentence. --- docs/COMPILING.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/COMPILING.md b/docs/COMPILING.md index f2b59acbc..6df1232b1 100644 --- a/docs/COMPILING.md +++ b/docs/COMPILING.md @@ -34,10 +34,11 @@ There are several useful build flags you can add to a build to affect how it wor - `-DGITHUB_BUILD` will enable in-game debug functions (such as the ability to time travel in a song by pressing `PgUp`/`PgDn`), without enabling the other stuff - `-DFEATURE_POLYMOD_MODS` or `-DNO_FEATURE_POLYMOD_MODS` to forcibly enable or disable modding support. - `-DREDIRECT_ASSETS_FOLDER` or `-DNO_REDIRECT_ASSETS_FOLDER` to forcibly enable or disable asset redirection. - - This feature causes the game to load exported assets from the project's assets folder rather than the exported one. Great for fast iteration, but the game _(finish this sentence Eric)_ + - This feature causes the game to load exported assets from the project's assets folder rather than the exported one. Great for fast iteration, but the game will break if you try to zip it up and send it to someone, so it's disabled for release builds. - `-DFEATURE_DISCORD_RPC` or `-DNO_FEATURE_DISCORD_RPC` to forcibly enable or disable support for Discord Rich Presence. - `-DFEATURE_VIDEO_PLAYBACK` or `-DNO_FEATURE_VIDEO_PLAYBACK` to forcibly enable or disable video cutscene support. - `-DFEATURE_CHART_EDITOR` or `-DNO_FEATURE_CHART_EDITOR` to forcibly enable or disable the chart editor in the Debug menu. +- `-DFEATURE_SCREENSHOTS` or `-DNO_FEATURE_SCREENSHOTS` to forcibly enable or disable the screenshots feature. - `-DFEATURE_STAGE_EDITOR` to forcibly enable the experimental stage editor. - `-DFEATURE_GHOST_TAPPING` to forcibly enable an experimental gameplay change to the anti-mash system. From e66290c55f7141402223644f06ec8a69edeee089 Mon Sep 17 00:00:00 2001 From: Kn1ghtNight Date: Tue, 18 Jun 2024 18:25:00 -0400 Subject: [PATCH 10/44] fix: anti alias / smooth the volume sound tray --- source/funkin/ui/options/FunkinSoundTray.hx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/source/funkin/ui/options/FunkinSoundTray.hx b/source/funkin/ui/options/FunkinSoundTray.hx index b2fb7fc04..5a5bf1d6c 100644 --- a/source/funkin/ui/options/FunkinSoundTray.hx +++ b/source/funkin/ui/options/FunkinSoundTray.hx @@ -33,6 +33,7 @@ class FunkinSoundTray extends FlxSoundTray var bg:Bitmap = new Bitmap(Assets.getBitmapData(Paths.image("soundtray/volumebox"))); bg.scaleX = graphicScale; bg.scaleY = graphicScale; + bg.smoothing = true; addChild(bg); y = -height; @@ -44,6 +45,7 @@ class FunkinSoundTray extends FlxSoundTray backingBar.y = 5; backingBar.scaleX = graphicScale; backingBar.scaleY = graphicScale; + backingBar.smoothing = true; addChild(backingBar); backingBar.alpha = 0.4; @@ -60,6 +62,7 @@ class FunkinSoundTray extends FlxSoundTray bar.y = 5; bar.scaleX = graphicScale; bar.scaleY = graphicScale; + bar.smoothing = true; addChild(bar); _bars.push(bar); } From b2647fe09f5281ce7074b26d47bc1524764168ee Mon Sep 17 00:00:00 2001 From: lemz1 Date: Wed, 9 Oct 2024 20:50:13 +0200 Subject: [PATCH 11/44] fix: disable flickering when changing FPS in the options menu --- source/funkin/ui/options/items/EnumPreferenceItem.hx | 2 ++ source/funkin/ui/options/items/NumberPreferenceItem.hx | 2 ++ 2 files changed, 4 insertions(+) diff --git a/source/funkin/ui/options/items/EnumPreferenceItem.hx b/source/funkin/ui/options/items/EnumPreferenceItem.hx index 02a273353..0054d3efe 100644 --- a/source/funkin/ui/options/items/EnumPreferenceItem.hx +++ b/source/funkin/ui/options/items/EnumPreferenceItem.hx @@ -46,6 +46,8 @@ class EnumPreferenceItem extends TextMenuItem } lefthandText = new AtlasText(15, y, formatted(defaultValue), AtlasFont.DEFAULT); + + this.fireInstantly = true; } override function update(elapsed:Float):Void diff --git a/source/funkin/ui/options/items/NumberPreferenceItem.hx b/source/funkin/ui/options/items/NumberPreferenceItem.hx index f3cd3cd46..e53951df7 100644 --- a/source/funkin/ui/options/items/NumberPreferenceItem.hx +++ b/source/funkin/ui/options/items/NumberPreferenceItem.hx @@ -58,6 +58,8 @@ class NumberPreferenceItem extends TextMenuItem this.precision = precision; this.onChangeCallback = callback; this.valueFormatter = valueFormatter; + + this.fireInstantly = true; } override function update(elapsed:Float):Void From 0a5419d7fc7725f27a6e1802cc34298983f6d455 Mon Sep 17 00:00:00 2001 From: Kolo <67389779+JustKolosaki@users.noreply.github.com> Date: Sun, 29 Sep 2024 19:07:09 +0200 Subject: [PATCH 12/44] the stage editor shit --- source/funkin/data/stage/StageData.hx | 30 + source/funkin/play/stage/Stage.hx | 4 + source/funkin/save/Save.hx | 135 ++ source/funkin/ui/debug/DebugMenuSubState.hx | 3 +- .../ui/debug/stageeditor/StageEditorObject.hx | 119 ++ .../ui/debug/stageeditor/StageEditorState.hx | 1486 +++++++++++++++++ .../stageeditor/components/AboutDialog.hx | 6 + .../components/BackupAvailableDialog.hx | 80 + .../components/ExitConfirmDialog.hx | 31 + .../stageeditor/components/FindObjDialog.hx | 105 ++ .../components/LoadFromUrlDialog.hx | 70 + .../stageeditor/components/NewObjDialog.hx | 90 + .../stageeditor/components/UserGuideDialog.hx | 6 + .../stageeditor/components/WelcomeDialog.hx | 146 ++ .../stageeditor/handlers/AssetDataHandler.hx | 208 +++ .../stageeditor/handlers/StageDataHandler.hx | 365 ++++ .../stageeditor/handlers/UndoRedoHandler.hx | 191 +++ source/funkin/ui/debug/stageeditor/import.hx | 7 + .../toolboxes/StageEditorCharacterToolbox.hx | 242 +++ .../toolboxes/StageEditorDefaultToolbox.hx | 44 + .../toolboxes/StageEditorObjectToolbox.hx | 581 +++++++ .../toolboxes/StageEditorStageToolbox.hx | 60 + source/funkin/ui/transition/LoadingState.hx | 12 +- source/funkin/util/FileUtil.hx | 7 + 24 files changed, 4025 insertions(+), 3 deletions(-) create mode 100644 source/funkin/ui/debug/stageeditor/StageEditorObject.hx create mode 100644 source/funkin/ui/debug/stageeditor/StageEditorState.hx create mode 100644 source/funkin/ui/debug/stageeditor/components/AboutDialog.hx create mode 100644 source/funkin/ui/debug/stageeditor/components/BackupAvailableDialog.hx create mode 100644 source/funkin/ui/debug/stageeditor/components/ExitConfirmDialog.hx create mode 100644 source/funkin/ui/debug/stageeditor/components/FindObjDialog.hx create mode 100644 source/funkin/ui/debug/stageeditor/components/LoadFromUrlDialog.hx create mode 100644 source/funkin/ui/debug/stageeditor/components/NewObjDialog.hx create mode 100644 source/funkin/ui/debug/stageeditor/components/UserGuideDialog.hx create mode 100644 source/funkin/ui/debug/stageeditor/components/WelcomeDialog.hx create mode 100644 source/funkin/ui/debug/stageeditor/handlers/AssetDataHandler.hx create mode 100644 source/funkin/ui/debug/stageeditor/handlers/StageDataHandler.hx create mode 100644 source/funkin/ui/debug/stageeditor/handlers/UndoRedoHandler.hx create mode 100644 source/funkin/ui/debug/stageeditor/import.hx create mode 100644 source/funkin/ui/debug/stageeditor/toolboxes/StageEditorCharacterToolbox.hx create mode 100644 source/funkin/ui/debug/stageeditor/toolboxes/StageEditorDefaultToolbox.hx create mode 100644 source/funkin/ui/debug/stageeditor/toolboxes/StageEditorObjectToolbox.hx create mode 100644 source/funkin/ui/debug/stageeditor/toolboxes/StageEditorStageToolbox.hx diff --git a/source/funkin/data/stage/StageData.hx b/source/funkin/data/stage/StageData.hx index 1e9172b00..5ac22ca90 100644 --- a/source/funkin/data/stage/StageData.hx +++ b/source/funkin/data/stage/StageData.hx @@ -20,6 +20,10 @@ class StageData @:optional public var cameraZoom:Null; + @:default("shared") + @:optional + public var directory:Null; + public function new() { this.version = StageRegistry.STAGE_DATA_VERSION; @@ -198,6 +202,32 @@ typedef StageDataProp = @:default("sparrow") @:optional var animType:String; + + /** + * The angle of the prop, as a float. + * @default 1.0 + */ + @:optional + @:default(0.0) + var angle:Float; + + /** + * The blend mode of the prop, as a string. + * Just like in photoshop. + * @default Nothing. + */ + @:default("") + @:optional + var blend:String; + + /** + * The color of the prop overlay, as a hex string. + * White overlays, or the ones with the value #FFFFFF, do not appear. + * @default `#FFFFFF` + */ + @:default("#FFFFFF") + @:optional + var color:String; }; typedef StageDataCharacter = diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx index 62295a717..88e19b191 100644 --- a/source/funkin/play/stage/Stage.hx +++ b/source/funkin/play/stage/Stage.hx @@ -256,6 +256,10 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements propSprite.scrollFactor.x = dataProp.scroll[0]; 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); + propSprite.zIndex = dataProp.zIndex; propSprite.flipX = dataProp.flipX; diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx index 47467cd45..8403a1a77 100644 --- a/source/funkin/save/Save.hx +++ b/source/funkin/save/Save.hx @@ -10,6 +10,7 @@ import funkin.save.migrator.SaveDataMigrator; import funkin.save.migrator.SaveDataMigrator; import funkin.ui.debug.charting.ChartEditorState.ChartEditorLiveInputStyle; import funkin.ui.debug.charting.ChartEditorState.ChartEditorTheme; +import funkin.ui.debug.stageeditor.StageEditorState.StageEditorTheme; import funkin.util.SerializerUtil; import thx.semver.Version; import thx.semver.Version; @@ -146,6 +147,14 @@ class Save hitsoundVolumeOpponent: 1.0, themeMusic: true }, + + optionsStageEditor: + { + previousFiles: [], + moveStep: "1px", + angleStep: 5, + theme: StageEditorTheme.Light + } }; } @@ -428,6 +437,91 @@ class Save return data.unlocks.oldChar; } + public var stageEditorPreviousFiles(get, set):Array; + + function get_stageEditorPreviousFiles():Array + { + if (data.optionsStageEditor.previousFiles == null) data.optionsStageEditor.previousFiles = []; + + return data.optionsStageEditor.previousFiles; + } + + function set_stageEditorPreviousFiles(value:Array):Array + { + // Set and apply. + data.optionsStageEditor.previousFiles = value; + flush(); + return data.optionsStageEditor.previousFiles; + } + + public var stageEditorHasBackup(get, set):Bool; + + function get_stageEditorHasBackup():Bool + { + if (data.optionsStageEditor.hasBackup == null) data.optionsStageEditor.hasBackup = false; + + return data.optionsStageEditor.hasBackup; + } + + function set_stageEditorHasBackup(value:Bool):Bool + { + // Set and apply. + data.optionsStageEditor.hasBackup = value; + flush(); + return data.optionsStageEditor.hasBackup; + } + + public var stageEditorMoveStep(get, set):String; + + function get_stageEditorMoveStep():String + { + if (data.optionsStageEditor.moveStep == null) data.optionsStageEditor.moveStep = "1px"; + + return data.optionsStageEditor.moveStep; + } + + function set_stageEditorMoveStep(value:String):String + { + // Set and apply. + data.optionsStageEditor.moveStep = value; + flush(); + return data.optionsStageEditor.moveStep; + } + + public var stageEditorAngleStep(get, set):Float; + + function get_stageEditorAngleStep():Float + { + if (data.optionsStageEditor.angleStep == null) data.optionsStageEditor.angleStep = 5; + + return data.optionsStageEditor.angleStep; + } + + function set_stageEditorAngleStep(value:Float):Float + { + // Set and apply. + data.optionsStageEditor.angleStep = value; + flush(); + return data.optionsStageEditor.angleStep; + } + + public var stageEditorTheme(get, set):StageEditorTheme; + + function get_stageEditorTheme():StageEditorTheme + { + if (data.optionsStageEditor.theme == null) data.optionsStageEditor.theme = StageEditorTheme.Light; + + return data.optionsStageEditor.theme; + } + + function set_stageEditorTheme(value:StageEditorTheme):StageEditorTheme + { + // Set and apply. + data.optionsStageEditor.theme = value; + flush(); + return data.optionsStageEditor.theme; + } + /** * When we've seen a character unlock, add it to the list of characters seen. * @param character @@ -1068,6 +1162,11 @@ typedef RawSaveData = * The user's preferences specific to the Chart Editor. */ var optionsChartEditor:SaveDataChartEditorOptions; + + /** + * The user's preferences specific to the Stage Editor. + */ + var optionsStageEditor:SaveDataStageEditorOptions; }; typedef SaveApiData = @@ -1441,3 +1540,39 @@ typedef SaveDataChartEditorOptions = */ var ?playbackSpeed:Float; }; + +typedef SaveDataStageEditorOptions = +{ + // a lot of these things were copied from savedatacharteditoroptions + + /** + * Whether the Stage Editor created a backup the last time it closed. + * Prompt the user to load it, then set this back to `false`. + * @default `false` + */ + var ?hasBackup:Bool; + + /** + * Previous files opened in the Stage Editor. + * @default `[]` + */ + var ?previousFiles:Array; + + /** + * The Step at which an Object or Character is moved. + * @default `1px` + */ + var ?moveStep:String; + + /** + * The Step at which an Object is rotated. + * @default `5` + */ + var ?angleStep:Float; + + /** + * Theme in the Stage Editor. + * @default `StageEditorTheme.Light` + */ + var ?theme:StageEditorTheme; +}; diff --git a/source/funkin/ui/debug/DebugMenuSubState.hx b/source/funkin/ui/debug/DebugMenuSubState.hx index fc5f3aa37..cc6a2426e 100644 --- a/source/funkin/ui/debug/DebugMenuSubState.hx +++ b/source/funkin/ui/debug/DebugMenuSubState.hx @@ -60,7 +60,7 @@ class DebugMenuSubState extends MusicBeatSubState // createItem("Input Offset Testing", openInputOffsetTesting); createItem("CHARACTER SELECT", openCharSelect, true); createItem("ANIMATION EDITOR", openAnimationEditor); - // createItem("STAGE EDITOR", openStageEditor); + createItem("STAGE EDITOR", openStageEditor); // createItem("TEST STICKERS", testStickers); #if sys createItem("OPEN CRASH LOG FOLDER", openLogFolder); @@ -125,6 +125,7 @@ class DebugMenuSubState extends MusicBeatSubState function openStageEditor() { trace('Stage Editor'); + FlxG.switchState(() -> new funkin.ui.debug.stageeditor.StageEditorState()); } #if sys diff --git a/source/funkin/ui/debug/stageeditor/StageEditorObject.hx b/source/funkin/ui/debug/stageeditor/StageEditorObject.hx new file mode 100644 index 000000000..6fa7db6be --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/StageEditorObject.hx @@ -0,0 +1,119 @@ +package funkin.ui.debug.stageeditor; + +import funkin.data.animation.AnimationData; +import funkin.graphics.FunkinSprite; +import funkin.modding.events.ScriptEvent; + +/** + * Contains all the Logic needed for Stage Editor. Only for Stage Editor, as in the gameplay StageProps and Boppers will be used. + */ +class StageEditorObject extends FunkinSprite +{ + /** + * The internal Name of the Object. + */ + public var name:String = "Unnamed"; + + /** + * What animation to play upon starting. + */ + public var startingAnimation:String = ""; + + public var animDatas:Map = []; + + override public function new() + { + super(); + } + + /** + * Whether the Object is currently being modified in the Stage Editor. + */ + public var isDebugged(default, set):Bool = true; + + function set_isDebugged(value:Bool) + { + this.isDebugged = value; + + if (value == false) // plays upon starting yippee!!! + playAnim(startingAnimation, true); + else + { + if (animation.curAnim != null) + { + animation.stop(); + offset.set(); + updateHitbox(); + } + } + + return value; + } + + public function playAnim(name:String, restart:Bool = false, reversed:Bool = false) + { + if (!animation.getNameList().contains(name)) return; + + animation.play(name, restart, reversed, 0); + + if (animDatas.exists(name)) offset.set(animDatas[name].offsets[0], animDatas[name].offsets[1]); + else + offset.set(); + } + + /** + * On which beat should it dance? + */ + public var danceEvery:Float = 0; + + /** + * Internal, handles danceLeft and danceRight. + */ + var _danced:Bool = true; + + public function dance(restart:Bool = false) + { + if (isDebugged) return; + + var idle = animation.getNameList().contains("idle"); + var dancing = animation.getNameList().contains("danceLeft") && animation.getNameList().contains("danceRight"); + + if (!idle && !dancing) return; + + if (dancing) + { + if (_danced) playAnim("danceRight", restart); + else + playAnim("danceLeft", restart); + + _danced = !_danced; + } + else if (idle) + { + playAnim("idle", restart); + } + } + + public function addAnim(name:String, prefix:String, offsets:Array, indices:Array, frameRate:Int = 24, looped:Bool = true, flipX:Bool = false, + flipY:Bool = false) + { + if (indices.length > 0) animation.addByIndices(name, prefix, indices, "", frameRate, looped, flipX, flipY); + else + animation.addByPrefix(name, prefix, frameRate, looped, flipX, flipY); + + if (animation.getNameList().contains(name)) // sometimes the animation doesnt add + { + animDatas.set(name, + { + name: name, + prefix: prefix, + offsets: offsets, + looped: looped, + frameRate: frameRate, + flipX: flipX, + flipY: flipY, + frameIndices: indices + }); + } + } +} diff --git a/source/funkin/ui/debug/stageeditor/StageEditorState.hx b/source/funkin/ui/debug/stageeditor/StageEditorState.hx new file mode 100644 index 000000000..e6d2e1f8a --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/StageEditorState.hx @@ -0,0 +1,1486 @@ +package funkin.ui.debug.stageeditor; + +import flixel.math.FlxPoint; +import flixel.text.FlxText; +import openfl.display.BitmapData; +import flixel.util.FlxTimer; +import flixel.FlxCamera; +import flixel.addons.display.shapes.FlxShapeCircle; +import flixel.graphics.FlxGraphic; +import flixel.FlxSprite; +import flixel.util.FlxColor; +import flixel.group.FlxGroup.FlxTypedGroup; +import flixel.addons.display.FlxGridOverlay; +import funkin.play.character.BaseCharacter; +import funkin.play.character.BaseCharacter.CharacterType; +import funkin.play.character.CharacterData.CharacterDataParser; +import funkin.save.Save; +import funkin.input.Cursor; +import haxe.ui.backend.flixel.UIState; +import haxe.ui.containers.menus.MenuItem; +import haxe.ui.containers.menus.Menu; +import haxe.ui.containers.menus.MenuSeparator; +import haxe.ui.containers.menus.MenuBar; +import haxe.ui.containers.menus.MenuOptionBox; +import haxe.ui.containers.menus.MenuCheckBox; +import funkin.util.FileUtil; +import funkin.ui.debug.stageeditor.handlers.AssetDataHandler; +import funkin.ui.debug.stageeditor.handlers.AssetDataHandler.StageEditorObjectData; +import funkin.ui.debug.stageeditor.handlers.StageDataHandler; +import funkin.ui.debug.stageeditor.handlers.UndoRedoHandler.UndoAction; +import funkin.ui.debug.stageeditor.toolboxes.*; +import funkin.ui.debug.stageeditor.components.*; +import haxe.ui.containers.dialogs.Dialogs; +import haxe.ui.containers.dialogs.Dialog; +import haxe.ui.containers.dialogs.MessageBox.MessageBoxType; +import haxe.ui.containers.Box; +import haxe.ui.containers.VBox; // vbucks +import haxe.ui.components.Button; +import haxe.ui.containers.windows.WindowList; +import haxe.ui.containers.windows.WindowManager; +import haxe.ui.containers.windows.Window; +import flixel.FlxObject; +import haxe.ui.components.Label; +import funkin.ui.mainmenu.MainMenuState; +import flixel.system.debug.interaction.tools.Pointer.GraphicCursorCross; +import haxe.ui.focus.FocusManager; +import haxe.ui.core.Screen; +import funkin.util.WindowUtil; +import funkin.audio.FunkinSound; +import haxe.ui.notifications.NotificationType; +import haxe.ui.notifications.NotificationManager; +import funkin.util.logging.CrashHandler; + +/** + * Da Stage Editor woo!! + * made by Kolo NEVER FORGET + */ +@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/stage-editor/main-view.xml")) +class StageEditorState extends UIState +{ + // i aint documenting allat + // the uh finals + public static final BACKUPS_PATH:String = "./stagebackups/"; + public static final LIGHT_MODE_COLORS:Array = [0xFFE7E6E6, 0xFFF8F8F8]; + public static final DARK_MODE_COLORS:Array = [0xFF181919, 0xFF202020]; + + public static final DEFAULT_POSITIONS:Map> = [ + CharacterType.BF => [989.5, 885], + CharacterType.GF => [751.5, 787], + CharacterType.DAD => [335, 885] + ]; + + public static final DEFAULT_CAMERA_OFFSETS:Map> = [ + CharacterType.BF => [-100, -100], + CharacterType.GF => [0, 0], + CharacterType.DAD => [150, -100] + ]; + + public static final MAX_Z_INDEX:Int = 10000; + public static final CHARACTER_COLORS:Array = [FlxColor.RED, FlxColor.PURPLE, FlxColor.CYAN]; // FCUK IVE TURNED INTO AN AMERICAN + public static final TIME_BEFORE_ANIM_STOP:Float = 3.0; + + public static var instance:StageEditorState = null; // unused lol + + // the other shit:tm: + var menubar:MenuBar; + + var menubarMenuFile:Menu; + var menubarItemNewStage:MenuItem; // new + var menubarItemOpenStage:MenuItem; // open + var menubarItemOpenRecent:Menu; // open recent submenu + var menubarItemSaveStage:MenuItem; // save + var menubarItemSaveStageAs:MenuItem; // save as + var menubarItemClearAssets:MenuItem; // clear assets + var menubarItemExit:MenuItem; // exit + + var menubarMenuEdit:Menu; + var menubarItemUndo:MenuItem; // undo + var menubarItemRedo:MenuItem; // redo + var menubarItemCopy:MenuItem; // copy + var menubarItemCut:MenuItem; // cut + var menubarItemPaste:MenuItem; // paste + var menubarItemDelete:MenuItem; // delete + var menubarItemNewObj:MenuItem; // new + var menubarItemFindObj:MenuItem; // find + var menubarItemMoveStep:Menu; // move step submenu + + var menubarMenuView:Menu; + var menubarItemThemeLight:MenuOptionBox; // light mode option + var menubarItemThemeDark:MenuOptionBox; // dark mode option + var menubarItemViewChars:MenuCheckBox; // view chars check + var menubarItemViewNameText:MenuCheckBox; // view name text check + var menubarItemViewFloorLines:MenuCheckBox; // view floor lines check + var menubarItemViewPosMarkers:MenuCheckBox; // view pos markers check + var menubarItemViewCamBounds:MenuCheckBox; // view cam bounds check + + var menubarMenuWindow:Menu; + var menubarItemWindowObject:MenuCheckBox; + var menubarItemWindowCharacter:MenuCheckBox; + var menubarItemWindowStage:MenuCheckBox; + + var menubarMenuHelp:Menu; + var menubarItemUserGuide:MenuItem; + var menubarItemGoToBackupsFolder:MenuItem; + var menubarItemAbout:MenuItem; + + var menubarButtonText:Button; // test stage button + var windowList:WindowList; + + var bottomBarModeText:Label; + var bottomBarSelectText:Label; + var bottomBarMoveStepText:Label; + var bottomBarAngleStepText:Label; + + var bg:FlxSprite; + + public var selectedSprite(default, set):StageEditorObject = null; + + function set_selectedSprite(value:StageEditorObject) + { + this.selectedSprite = value; + updateDialog(StageEditorDialogType.OBJECT); + + if (selectedSprite != null) + { + spriteMarker.setGraphicSize(Std.int(selectedSprite.width), Std.int(selectedSprite.height)); + spriteMarker.updateHitbox(); + } + + return selectedSprite; + } + + public var selectedChar(default, set):BaseCharacter = null; + + function set_selectedChar(value:BaseCharacter) + { + this.selectedChar = value; + updateDialog(StageEditorDialogType.CHARACTER); + return selectedChar; + } + + var isCursorOverHaxeUI(get, never):Bool; + + function get_isCursorOverHaxeUI():Bool + { + return Screen.instance.hasSolidComponentUnderPoint(Screen.instance.currentMouseX, Screen.instance.currentMouseY); + } + + public var spriteMarker:FlxSprite; + public var spriteArray:Array = []; + public var camMarker:FlxSprite; + + public var copiedSprite:StageEditorObjectData = null; + + public var stageZoom:Float = 1.0; + public var stageName:String = "Unnamed"; + public var stageFolder:String = "shared"; + + public var autoSaveTimer:FlxTimer = new FlxTimer(); + + public var saved(default, set):Bool = true; + public var currentFile(default, set):String = ""; + + function set_saved(value:Bool) + { + saved = value; + + updateWindowTitle(); + + if (!autoSaveTimer.finished) + { + autoSaveTimer.cancel(); + } + + if (!saved) + { + autoSaveTimer.start(Constants.AUTOSAVE_TIMER_DELAY_SEC, function(tmr:FlxTimer) { + FileUtil.createDirIfNotExists(BACKUPS_PATH); + + var data = this.packShitToZip(); + var path = haxe.io.Path.join([ + BACKUPS_PATH, + 'stage-editor-${funkin.util.DateUtil.generateTimestamp()}.${FileUtil.FILE_EXTENSION_INFO_FNFS.extension}' + ]); + + FileUtil.writeBytesToPath(path, data); + saved = true; + + Save.instance.stageEditorHasBackup = true; + Save.instance.flush(); + + notifyChange("Auto-Save", "A Backup of this Stage has been made."); + }); + } + + return value; + } + + function set_currentFile(value:String) + { + currentFile = value; + + updateWindowTitle(); + + if (currentFile != "") updateRecentFiles(); + + reloadRecentFiles(); + + return value; + } + + public var undoArray:Array = []; + public var redoArray:Array = []; + + public var nameTxt:FlxText; + + public var gf(get, never):BaseCharacter; + public var bf(get, never):BaseCharacter; + public var dad(get, never):BaseCharacter; + + function get_gf() + return charGroups[CharacterType.GF].getFirst(StageDataHandler.checkForCharacter); + + function get_bf() + return charGroups[CharacterType.BF].getFirst(StageDataHandler.checkForCharacter); + + function get_dad() + return charGroups[CharacterType.DAD].getFirst(StageDataHandler.checkForCharacter); + + public var charGroups:Map> = []; + + public var charCamOffsets:Map> = DEFAULT_CAMERA_OFFSETS.copy(); + public var charPos:Map> = DEFAULT_POSITIONS.copy(); + + public var bitmaps:Map = []; // used for optimizing the file size!!! + + var floorLines:Array = []; + var posCircles:Array = []; + var camFields:FlxTypedGroup; + var camHUD:FlxCamera; + var camGame:FlxCamera; + + public var camFollow:FlxObject; + public var moveOffset:Array = []; + public var moveStep:Int = 1; + public var moveMode:String = "assets"; + public var infoSelection:String = "None"; + public var dialogs:Map = []; + + var allowInput(get, never):Bool; + + function get_allowInput() + { + return FocusManager.instance.focus == null; + } + + var testingMode:Bool = false; + + var showChars(default, set):Bool = true; + + function set_showChars(value:Bool) + { + this.showChars = value; + + for (cooldude in getCharacters()) + cooldude.visible = showChars; + + return value; + } + + override public function create() + { + WindowManager.instance.reset(); + instance = this; + FlxG.sound.music.stop(); + WindowUtil.setWindowTitle("Friday Night Funkin\' Stage Editor"); + + AssetDataHandler.init(this); + + camGame = new FlxCamera(); + camHUD = new FlxCamera(); + camHUD.bgColor.alpha = 0; + + FlxG.cameras.reset(camGame); + FlxG.cameras.add(camHUD, false); + FlxG.cameras.setDefaultDrawTarget(camGame, true); + + persistentUpdate = false; + + // FlxG.worldBounds.set(0, 0, FlxG.width, FlxG.height); + + bg = FlxGridOverlay.create(10, 10); + bg.scrollFactor.set(); + add(bg); + + updateBGColors(); + + super.create(); + root.scrollFactor.set(); + root.cameras = [camHUD]; + root.width = FlxG.width; + root.height = FlxG.height; + + menubar.height = 35; + WindowManager.instance.container = root; + Screen.instance.addComponent(root); + + // group shit + assets + var gf = CharacterDataParser.fetchCharacter("gf", true); + gf.characterType = CharacterType.GF; + var dad = CharacterDataParser.fetchCharacter("dad", true); + dad.characterType = CharacterType.DAD; + var bf = CharacterDataParser.fetchCharacter("bf", true); + bf.characterType = CharacterType.BF; + + bf.flipX = !bf.getDataFlipX(); + gf.flipX = gf.getDataFlipX(); + dad.flipX = dad.getDataFlipX(); + + gf.updateHitbox(); + dad.updateHitbox(); + bf.updateHitbox(); + + // only one char !!! + charGroups = [ + CharacterType.BF => new FlxTypedGroup(1), + CharacterType.GF => new FlxTypedGroup(1), + CharacterType.DAD => new FlxTypedGroup(1) + ]; + + // this is the part where the stage generate function comes up + // apparently no, said the future me + // back to the regular program + + gf.x = charPos[CharacterType.GF][0] - gf.characterOrigin.x + gf.globalOffsets[0]; + gf.y = charPos[CharacterType.GF][1] - gf.characterOrigin.y + gf.globalOffsets[1]; + dad.x = charPos[CharacterType.DAD][0] - dad.characterOrigin.x + dad.globalOffsets[0]; + dad.y = charPos[CharacterType.DAD][1] - dad.characterOrigin.y + dad.globalOffsets[1]; + bf.x = charPos[CharacterType.BF][0] - bf.characterOrigin.x + bf.globalOffsets[0]; + bf.y = charPos[CharacterType.BF][1] - bf.characterOrigin.y + bf.globalOffsets[1]; + + selectedChar = bf; + + charGroups[CharacterType.GF].add(gf); + charGroups[CharacterType.DAD].add(dad); + charGroups[CharacterType.BF].add(bf); + + add(charGroups[CharacterType.GF]); + add(charGroups[CharacterType.DAD]); + add(charGroups[CharacterType.BF]); + + // ui + spriteMarker = new FlxSprite().makeGraphic(1, 1, FlxColor.CYAN); + spriteMarker.alpha = 0.3; + spriteMarker.zIndex = MAX_Z_INDEX + CHARACTER_COLORS.length + 3; // PLEASE + add(spriteMarker); + + camFields = new FlxTypedGroup(); + camFields.visible = false; + camFields.zIndex = MAX_Z_INDEX + CHARACTER_COLORS.length + 1; + + for (i in 0...CHARACTER_COLORS.length) + { + var floorLine = new FlxSprite().makeGraphic(FlxG.width * 10, 15, CHARACTER_COLORS[i]); + floorLine.screenCenter(X); + + var pointer = new FlxShapeCircle(0, 0, 30, cast {thickness: 2, color: CHARACTER_COLORS[i]}, CHARACTER_COLORS[i]); + + var field = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, CHARACTER_COLORS[i]); + + pointer.alpha = floorLine.alpha = field.alpha = 0.35; + pointer.ID = floorLine.ID = field.ID = i; + pointer.visible = floorLine.visible = false; + pointer.zIndex = floorLine.zIndex = MAX_Z_INDEX + 1 + i; + + add(floorLine); + add(pointer); + + floorLines.push(floorLine); + posCircles.push(pointer); + + camFields.add(field); + } + + camMarker = new FlxSprite().loadGraphic(FlxGraphic.fromClass(GraphicCursorCross)); + camMarker.setGraphicSize(80, 80); + camMarker.updateHitbox(); + camMarker.zIndex = MAX_Z_INDEX + CHARACTER_COLORS.length + 2; + + updateMarkerPos(); + + add(camFields); + add(camMarker); + + nameTxt = new FlxText(0, 0, 0, "", 24); + nameTxt.setFormat(Paths.font("vcr.ttf"), 24, FlxColor.WHITE, LEFT, FlxTextBorderStyle.OUTLINE, FlxColor.BLACK); + nameTxt.cameras = [camHUD]; + add(nameTxt); + + camFollow = new FlxObject(0, 0, 2, 2); + camFollow.screenCenter(); + add(camFollow); + + camGame.follow(camFollow); + + addUI(); + + findObjDialog = new FindObjDialog(this, selectedSprite == null ? "" : selectedSprite.name); + + FlxG.stage.window.onDropFile.add(function(path:String):Void { + if (!allowInput || welcomeDialog != null) return; + + var data = BitmapData.fromFile(path); + + if (data != null) + { + objNameDialog = new NewObjDialog(this, data); + objNameDialog.showDialog(); + + objNameDialog.onDialogClosed = function(_) { + objNameDialog = null; + } + + return; + } + }); + + onMenuItemClick("new stage"); + welcomeDialog.closable = false; + + #if sys + if (Save.instance.stageEditorHasBackup) + { + FileUtil.createDirIfNotExists(BACKUPS_PATH); + + var files = sys.FileSystem.readDirectory(BACKUPS_PATH); + + if (files.length > 0) + { + // ensures that the top most file is a backup + files.sort(funkin.util.SortUtil.alphabetically); + + while (!files[files.length - 1].endsWith(FileUtil.FILE_EXTENSION_INFO_FNFS.extension) + || !files[files.length - 1].startsWith("stage-editor-")) + files.pop(); + } + + if (files.length != 0) new BackupAvailableDialog(this, haxe.io.Path.join([BACKUPS_PATH, files[files.length - 1]])).showDialog(true); + } + #end + + WindowUtil.windowExit.add(windowClose); + CrashHandler.errorSignal.add(autosavePerCrash); + CrashHandler.criticalErrorSignal.add(autosavePerCrash); + + Save.instance.stageEditorHasBackup = false; + + Cursor.show(); + FlxG.sound.playMusic(Paths.music('chartEditorLoop/chartEditorLoop')); + FlxG.sound.music.fadeIn(10, 0, 1); + } + + var curTestChar:Int = 0; + + override public function beatHit() + { + if (testingMode) + { + if (conductorInUse.currentBeat % 2 == 0) + { + for (char in getCharacters()) + char.dance(true); + } + + for (asset in spriteArray) + { + if (asset.danceEvery > 0 && conductorInUse.currentBeat % asset.danceEvery == 0) asset.dance(true); + } + + if (conductorInUse.currentBeat % 8 == 0 && !FlxG.keys.pressed.SHIFT) curTestChar++; + } + + return super.beatHit(); + } + + override public function update(elapsed:Float) + { + updateBGSize(); + conductorInUse.update(); + + super.update(elapsed); + + if (FlxG.mouse.justPressed || FlxG.mouse.justPressedRight) FunkinSound.playOnce(Paths.sound("chartingSounds/ClickDown")); + if (FlxG.mouse.justReleased || FlxG.mouse.justReleasedRight) FunkinSound.playOnce(Paths.sound("chartingSounds/ClickUp")); + + // testmode + menubarMenuFile.disabled = menubarMenuEdit.disabled = bottomBarModeText.disabled = menubarMenuWindow.disabled = testingMode; + menubarButtonText.selected = testingMode; + + if (testingMode) + { + for (char in getCharacters()) + char.alpha = 1; + + spriteMarker.visible = camMarker.visible = false; + findObjDialog.hideDialog(DialogButton.CANCEL); + + // cam + camGame.follow(camFollow, LOCKON, 0.04); + FlxG.camera.zoom = stageZoom; + + if (FlxG.keys.justPressed.TAB && !FlxG.keys.pressed.SHIFT) curTestChar++; + + if (curTestChar >= getCharacters().length) curTestChar = 0; + + bottomBarSelectText.text = Std.string(getCharacters()[curTestChar].characterType); + + var char = getCharacters()[curTestChar]; + camFollow.x = char.cameraFocusPoint.x + charCamOffsets.get(char.characterType)[0]; + camFollow.y = char.cameraFocusPoint.y + charCamOffsets.get(char.characterType)[1]; + + // EXIT + if (FlxG.keys.justPressed.ENTER) // so we dont accidentally get stuck (happened to me once, terrible experience) + onMenuItemClick("test stage"); + + return; + } + + // some misc + nameTxt.text = ""; + bottomBarModeText.text = (moveMode == "assets" ? "Objects" : "Characters"); + + camGame.follow(camFollow); + // camera movement + + if ((FlxG.mouse.wheel > 0 || (FlxG.mouse.wheel < 0 && camGame.zoom > 0.11)) + && !isCursorOverHaxeUI) // include the floating poing error thing + { + camGame.zoom += FlxG.mouse.wheel / 10; + updateBGSize(); + } + + // key shortcuts and inputs + if (allowInput) + { + if (FlxG.keys.pressed.CONTROL) + { + if (FlxG.keys.justPressed.Z) onMenuItemClick("undo"); + if (FlxG.keys.justPressed.Y) onMenuItemClick("redo"); + if (FlxG.keys.justPressed.C) onMenuItemClick("copy object"); + if (FlxG.keys.justPressed.V) onMenuItemClick("paste object"); + if (FlxG.keys.justPressed.X) onMenuItemClick("cut object"); + if (FlxG.keys.justPressed.S) FlxG.keys.pressed.SHIFT ? onMenuItemClick("save stage as") : onMenuItemClick("save stage"); + if (FlxG.keys.justPressed.F) onMenuItemClick("find object"); + if (FlxG.keys.justPressed.O) onMenuItemClick("open stage"); + } + + if (FlxG.keys.justPressed.TAB) onMenuItemClick("switch mode"); + if (FlxG.keys.justPressed.DELETE) onMenuItemClick("delete object"); + if (FlxG.keys.justPressed.ENTER) onMenuItemClick("test stage"); + if (FlxG.keys.justPressed.ESCAPE) onMenuItemClick("exit"); + if (FlxG.keys.justPressed.F1) onMenuItemClick("user guide"); + + if (FlxG.keys.justPressed.T) + { + camFollow.screenCenter(); + FlxG.camera.zoom = 1; + } + + if (FlxG.keys.pressed.W || FlxG.keys.pressed.S || FlxG.keys.pressed.A || FlxG.keys.pressed.D) + { + if (FlxG.keys.pressed.W) camFollow.velocity.y = -90 * (2 / FlxG.camera.zoom); + else if (FlxG.keys.pressed.S) camFollow.velocity.y = 90 * (2 / FlxG.camera.zoom); + else + camFollow.velocity.y = 0; + + if (FlxG.keys.pressed.A) camFollow.velocity.x = -90 * (2 / FlxG.camera.zoom); + else if (FlxG.keys.pressed.D) camFollow.velocity.x = 90 * (2 / FlxG.camera.zoom); + else + camFollow.velocity.x = 0; + } + else + { + camFollow.velocity.set(); + } + } + else + { + camFollow.velocity.set(); + } + + // movement handling + if (FlxG.mouse.justReleased && moveOffset.length > 0) moveOffset = []; + + if (moveMode == "assets") + { + for (spr in spriteArray) + { + spr.active = spr.isOnScreen(); + + if (FlxG.mouse.overlaps(spr)) + { + if (spr.visible && !FlxG.keys.pressed.SHIFT) nameTxt.text = spr.name; + + if (FlxG.mouse.justPressed && allowInput && spr.visible && !FlxG.keys.pressed.SHIFT && !isCursorOverHaxeUI) + { + selectedSprite = spr; + updateDialog(StageEditorDialogType.OBJECT); + } + } + + if (spr == selectedSprite) + { + infoSelection = spr.name; + + if (FlxG.keys.pressed.SHIFT) nameTxt.text = spr.name + " (LOCKED)"; + } + } + + if (FlxG.mouse.pressed && allowInput && selectedSprite != null && FlxG.mouse.overlaps(selectedSprite) && FlxG.mouse.justMoved && !isCursorOverHaxeUI) + { + saved = false; + updateDialog(StageEditorDialogType.OBJECT); + + if (moveOffset.length == 0) + { + this.createAndPushAction(OBJECT_MOVED); + + moveOffset = [ + FlxG.mouse.getWorldPosition().x - selectedSprite.x, + FlxG.mouse.getWorldPosition().y - selectedSprite.y + ]; + } + + var posBros = new FlxPoint(FlxG.mouse.getWorldPosition().x - moveOffset[0], FlxG.mouse.getWorldPosition().y - moveOffset[1]); + selectedSprite.x = (Math.floor(posBros.x) - Math.floor(posBros.x) % moveStep); + selectedSprite.y = (Math.floor(posBros.y) - Math.floor(posBros.y) % moveStep); + } + + if (selectedSprite != null && FlxG.keys.pressed.R) + { + if (FlxG.keys.justPressed.LEFT || FlxG.keys.justPressed.RIGHT) + { + saved = false; + this.createAndPushAction(OBJECT_ROTATED); + } + + if (FlxG.keys.justPressed.LEFT) selectedSprite.angle -= Save.instance.stageEditorAngleStep; + if (FlxG.keys.justPressed.RIGHT) selectedSprite.angle += Save.instance.stageEditorAngleStep; + } + + arrowMovement(selectedSprite); + + for (char in getCharacters()) + char.alpha = 1; + } + else + { + selectedChar.alpha = 1; + + for (char in getCharacters()) + { + if (char != selectedChar) char.alpha = 0.3; + + if (char != null && checkCharOverlaps(char)) // flxg.mouse.overlaps crashes the game + { + if (char.visible && !FlxG.keys.pressed.SHIFT) nameTxt.text = Std.string(char.characterType); + + if (FlxG.mouse.justPressed && allowInput && char.visible && !FlxG.keys.pressed.SHIFT && !isCursorOverHaxeUI) + { + selectedChar = char; + updateDialog(StageEditorDialogType.CHARACTER); + } + } + + if (selectedChar == char) + { + infoSelection = Std.string(char.characterType); + + if (FlxG.keys.pressed.SHIFT) nameTxt.text = Std.string(char.characterType) + " (LOCKED)"; + } + } + + if (FlxG.mouse.pressed && allowInput && checkCharOverlaps(selectedChar) && FlxG.mouse.justMoved && !isCursorOverHaxeUI) + { + saved = false; + updateDialog(StageEditorDialogType.CHARACTER); + + if (moveOffset.length == 0) + { + this.createAndPushAction(CHARACTER_MOVED); + + moveOffset = [ + FlxG.mouse.getWorldPosition().x - selectedChar.cornerPosition.x, + FlxG.mouse.getWorldPosition().y - selectedChar.cornerPosition.y + ]; + } + + var posBros = new FlxPoint(FlxG.mouse.getWorldPosition().x - moveOffset[0], FlxG.mouse.getWorldPosition().y - moveOffset[1]); + + selectedChar.cornerPosition = new FlxPoint(Math.floor(posBros.x) - Math.floor(posBros.x) % moveStep, + Math.floor(posBros.y) - Math.floor(posBros.y) % moveStep); + } + + arrowMovement(selectedChar); + updateMarkerPos(); + } + + if ((selectedSprite == null && moveMode == "assets") || (selectedChar == null && moveMode == "chars")) infoSelection = "None"; + bottomBarSelectText.text = infoSelection; + + // ui stuff + nameTxt.x = FlxG.mouse.getScreenPosition(camHUD).x; + nameTxt.y = FlxG.mouse.getScreenPosition(camHUD).y - nameTxt.height; + + spriteMarker.visible = (moveMode == "assets" && selectedSprite != null); + camMarker.visible = moveMode == "chars"; + if (selectedSprite != null) spriteMarker.setPosition(selectedSprite.x, selectedSprite.y); + + for (item in sprDependant) + item.disabled = !spriteMarker.visible; + + menubarItemPaste.disabled = copiedSprite == null; + menubarItemFindObj.disabled = !(moveMode == "assets"); + + if (moveMode == "chars") findObjDialog.hideDialog(DialogButton.CANCEL); + + menubarItemUndo.disabled = undoArray.length == 0; + menubarItemRedo.disabled = redoArray.length == 0; + } + + public function getCharacters() + { + return [gf, dad, bf]; + } + + function autosavePerCrash(message:String) + { + trace("fuuuucckkkkk we crashed, reason: " + message); + + if (!saved) + { + trace("dw we're making a backup!!!"); + autoSaveTimer.onComplete(autoSaveTimer); + } + } + + function windowClose(exitCode:Int) + { + trace("closing da window ye"); + + if (!saved) + { + trace("dum dum why no save >:["); + autoSaveTimer.onComplete(autoSaveTimer); + } + } + + public function updateRecentFiles() + { + var files = Save.instance.stageEditorPreviousFiles; + files.remove(currentFile); + files.unshift(currentFile); + + while (files.length > Constants.MAX_PREVIOUS_WORKING_FILES) + files.pop(); + + Save.instance.stageEditorPreviousFiles = files; + Save.instance.flush(); + } + + public function updateMarkerPos() + { + for (i in 0...getCharacters().length) + { + var char = getCharacters()[i]; + var type = char.characterType; + + charPos.set(type, [ + char.feetPosition.x - char.globalOffsets[0], + char.feetPosition.y - char.globalOffsets[1] + ]); + + floorLines[i].y = charPos.get(type)[1] - floorLines[i].height / 2; + + posCircles[i].y = charPos.get(type)[1] - posCircles[i].height / 2; + posCircles[i].x = charPos.get(type)[0] - posCircles[i].width / 2; + + camFields.members[i].scale.set(1 / stageZoom, 1 / stageZoom); + camFields.members[i].updateHitbox(); + + camFields.members[i].x = char.cameraFocusPoint.x + charCamOffsets.get(type)[0] - camFields.members[i].width / 2; + camFields.members[i].y = char.cameraFocusPoint.y + charCamOffsets.get(type)[1] - camFields.members[i].height / 2; + + if (char == selectedChar) + { + camMarker.x = camFields.members[i].getMidpoint().x - camMarker.width / 2; + camMarker.y = camFields.members[i].getMidpoint().y - camMarker.height / 2; + } + } + } + + // made because characters have shitty hitboxes and often cause the game to straight up crash + // it comes from some flxobject/polymod error apparently and I have no idea why + function checkCharOverlaps(char:BaseCharacter) + { + var mouseX = FlxG.mouse.x >= char.x && FlxG.mouse.x <= char.x + char.width; + var mouseY = FlxG.mouse.y >= char.y && FlxG.mouse.y <= char.y + char.height; + + return mouseX && mouseY && !isCursorOverHaxeUI; + } + + var moveUndoed:Bool = false; + + // i wish there was a better way to do this this looks like an eyesore + // yanderedev fr + function arrowMovement(obj:FlxSprite) + { + if (obj == null) return; + if (FlxG.keys.pressed.R) return; // rotations + + if (allowInput) + { + if ((FlxG.keys.justPressed.UP || FlxG.keys.justPressed.DOWN || FlxG.keys.justPressed.LEFT || FlxG.keys.justPressed.RIGHT) + && !moveUndoed) + { + saved = false; + moveUndoed = true; + this.createAndPushAction(moveMode == "assets" ? OBJECT_MOVED : CHARACTER_MOVED); + } + + if ((FlxG.keys.justReleased.UP || FlxG.keys.justReleased.DOWN || FlxG.keys.justReleased.LEFT || FlxG.keys.justReleased.RIGHT) + && moveUndoed) + { + moveUndoed = false; + } + + if (FlxG.keys.pressed.SHIFT) + { + if (FlxG.keys.pressed.UP) obj.y--; + if (FlxG.keys.pressed.DOWN) obj.y++; + if (FlxG.keys.pressed.LEFT) obj.x--; + if (FlxG.keys.pressed.RIGHT) obj.x++; + } + else + { + if (FlxG.keys.justPressed.UP) obj.y -= moveStep; + if (FlxG.keys.justPressed.DOWN) obj.y += moveStep; + if (FlxG.keys.justPressed.LEFT) obj.x -= moveStep; + if (FlxG.keys.justPressed.RIGHT) obj.x += moveStep; + } + } + } + + public function updateArray() + { + spriteArray = []; + + for (thing in members) + { + if (Std.isOfType(thing, StageEditorObject)) spriteArray.push(cast thing); // characters do not extend stageeditorobject so we ball + } + + findObjDialog.updateIndicator(); + updateDialog(StageEditorDialogType.OBJECT); + } + + public function sortAssets() + { + sort(funkin.util.SortUtil.byZIndex, flixel.util.FlxSort.ASCENDING); + } + + public function updateDialog(type:StageEditorDialogType) + { + if (!dialogs.exists(type)) return; + + dialogs[type].refresh(); + } + + public function toggleDialog(type:StageEditorDialogType, show:Bool = true) + { + if (!dialogs.exists(type)) return; + + dialogs[type].toggle(show); + } + + public function updateWindowTitle() + { + var defaultTitle = "Friday Night Funkin\' Stage Editor"; + + if (currentFile == "") defaultTitle += " - New File" + else + defaultTitle += " - " + currentFile; + + if (!saved) defaultTitle += "*"; + + WindowUtil.setWindowTitle(defaultTitle); + } + + function resetWindowTitle():Void + { + WindowUtil.setWindowTitle('Friday Night Funkin\''); + } + + function updateBGColors() + { + var colArray = Save.instance.stageEditorTheme == StageEditorTheme.Dark ? DARK_MODE_COLORS : LIGHT_MODE_COLORS; + + var index = members.indexOf(bg); + bg.kill(); + remove(bg); + bg.destroy(); + + bg = FlxGridOverlay.create(10, 10, -1, -1, true, colArray[0], colArray[1]); + bg.scrollFactor.set(); + members.insert(index, bg); + } + + function updateBGSize() + { + bg.scale.set(1 / FlxG.camera.zoom, 1 / FlxG.camera.zoom); + bg.updateHitbox(); + bg.screenCenter(); + } + + function checkOverlaps(spr:FlxSprite) + { + if (FlxG.mouse.overlaps(spr) /*spr.overlapsPoint(FlxG.mouse.getWorldPosition(spr.camera), true, spr.camera) */ + && Screen.instance != null + && !Screen.instance.hasSolidComponentUnderPoint(FlxG.mouse.screenX, FlxG.mouse.screenY) + && WindowManager.instance.windows.length == 0) // ik its stupid but maybe I have other cases soon (i did) + return true; + + return false; + } + + var sprDependant:Array = []; + + function addUI() + { + menubarItemNewStage.onClick = function(_) onMenuItemClick("new stage"); + menubarItemOpenStage.onClick = function(_) onMenuItemClick("open stage"); + menubarItemSaveStage.onClick = function(_) onMenuItemClick("save stage"); + menubarItemSaveStageAs.onClick = function(_) onMenuItemClick("save stage as"); + menubarItemClearAssets.onClick = function(_) onMenuItemClick("clear assets"); + menubarItemExit.onClick = function(_) onMenuItemClick("exit"); + menubarItemUndo.onClick = function(_) onMenuItemClick("undo"); + menubarItemRedo.onClick = function(_) onMenuItemClick("redo"); + menubarItemCopy.onClick = function(_) onMenuItemClick("copy object"); + menubarItemCut.onClick = function(_) onMenuItemClick("cut object"); + menubarItemPaste.onClick = function(_) onMenuItemClick("paste stage"); + menubarItemDelete.onClick = function(_) onMenuItemClick("delete object"); + menubarItemNewObj.onClick = function(_) onMenuItemClick("new object"); + menubarItemFindObj.onClick = function(_) onMenuItemClick("find object"); + menubarButtonText.onClick = function(_) onMenuItemClick("test stage"); + menubarItemUserGuide.onClick = function(_) onMenuItemClick("user guide"); + menubarItemGoToBackupsFolder.onClick = function(_) onMenuItemClick("open folder"); + menubarItemAbout.onClick = function(_) onMenuItemClick("about"); + + bottomBarModeText.onClick = function(_) onMenuItemClick("switch mode"); + bottomBarSelectText.onClick = function(_) onMenuItemClick("switch focus"); + + var stepOptions = ["1px", "2px", "3px", "5px", "10px", "25px", "50px", "100px"]; + bottomBarMoveStepText.text = stepOptions.contains(Save.instance.stageEditorMoveStep) ? Save.instance.stageEditorMoveStep : "1px"; + + var changeStep = function(change:Int = 0) { + var id = stepOptions.indexOf(bottomBarMoveStepText.text); + id += change; + + if (id >= stepOptions.length) id = stepOptions.length - 1; + else if (id < 0) id = 0; + + bottomBarMoveStepText.text = Save.instance.stageEditorMoveStep = stepOptions[id]; + var shit = Std.parseInt(StringTools.replace(bottomBarMoveStepText.text, "px", "")); + moveStep = shit; + + updateDialog(StageEditorDialogType.OBJECT); + updateDialog(StageEditorDialogType.CHARACTER); + updateDialog(StageEditorDialogType.STAGE); + } + + bottomBarMoveStepText.onClick = function(_) changeStep(1); + bottomBarMoveStepText.onRightClick = function(_) changeStep(-1); + + changeStep(); // update + + var angleOptions = [0.5, 1, 2, 5, 10, 15, 45, 75, 90, 180]; + bottomBarAngleStepText.text = (angleOptions.contains(Save.instance.stageEditorAngleStep) ? Save.instance.stageEditorAngleStep : 5) + "°"; + + var changeAngle = function(change:Int = 0) { + var id = angleOptions.indexOf(Save.instance.stageEditorAngleStep); + id += change; + + if (id >= angleOptions.length) id = angleOptions.length - 1; + else if (id < 0) id = 0; + + Save.instance.stageEditorAngleStep = angleOptions[id]; + bottomBarAngleStepText.text = (angleOptions.contains(Save.instance.stageEditorAngleStep) ? Save.instance.stageEditorAngleStep : 5) + "°"; + + updateDialog(StageEditorDialogType.OBJECT); + } + + bottomBarAngleStepText.onClick = function(_) changeAngle(1); + bottomBarAngleStepText.onRightClick = function(_) changeAngle(-1); + + changeAngle(); // update + + dialogs.set(StageEditorDialogType.OBJECT, new StageEditorObjectToolbox(this)); + dialogs.set(StageEditorDialogType.CHARACTER, new StageEditorCharacterToolbox(this)); + dialogs.set(StageEditorDialogType.STAGE, new StageEditorStageToolbox(this)); + + menubarItemWindowObject.onChange = function(_) toggleDialog(StageEditorDialogType.OBJECT, menubarItemWindowObject.selected); + menubarItemWindowCharacter.onChange = function(_) toggleDialog(StageEditorDialogType.CHARACTER, menubarItemWindowCharacter.selected); + menubarItemWindowStage.onChange = function(_) toggleDialog(StageEditorDialogType.STAGE, menubarItemWindowStage.selected); + + menubarItemThemeLight.onClick = function(_) { + Save.instance.stageEditorTheme = StageEditorTheme.Light; + updateBGColors(); + } + + menubarItemThemeDark.onClick = function(_) { + Save.instance.stageEditorTheme = StageEditorTheme.Dark; + updateBGColors(); + } + + menubarItemThemeDark.selected = Save.instance.stageEditorTheme == StageEditorTheme.Dark; + menubarItemThemeLight.selected = Save.instance.stageEditorTheme == StageEditorTheme.Light; + + menubarItemViewChars.onChange = function(_) showChars = menubarItemViewChars.selected; + menubarItemViewNameText.onChange = function(_) nameTxt.visible = menubarItemViewNameText.selected; + menubarItemViewCamBounds.onChange = function(_) camFields.visible = menubarItemViewCamBounds.selected; + + menubarItemViewFloorLines.onChange = function(_) { + for (awesome in floorLines) + awesome.visible = menubarItemViewFloorLines.selected; + } + + menubarItemViewPosMarkers.onChange = function(_) { + for (coolbeans in posCircles) + coolbeans.visible = menubarItemViewPosMarkers.selected; + } + + sprDependant = [menubarItemCopy, menubarItemCut, menubarItemDelete]; + reloadRecentFiles(); + } + + function reloadRecentFiles() + { + for (a in menubarItemOpenRecent.childComponents) + menubarItemOpenRecent.removeComponent(a); + + for (file in Save.instance.stageEditorPreviousFiles) + { + var filePath = new haxe.io.Path(file); + var item = new MenuItem(); + item.text = filePath.file + "." + filePath.ext; + item.disabled = !FileUtil.doesFileExist(file); + + var load = function(file:String) { + currentFile = file; + + this.unpackShitFromZip(FileUtil.readBytesFromPath(file)); + + reloadRecentFiles(); + } + + item.onClick = function(_) { + if (!saved) + { + Dialogs.messageBox("Opening a new Stage will reset all your progress for this Stage.\n\nAre you sure you want to proceed?", "Open Stage", + MessageBoxType.TYPE_YESNO, true, function(btn:DialogButton) { + if (btn == DialogButton.YES) + { + saved = true; + load(file); + } + }); + } + else + { + load(file); + } + } + + menubarItemOpenRecent.addComponent(item); + } + } + + public var objNameDialog:NewObjDialog; + public var findObjDialog:FindObjDialog; + public var welcomeDialog:WelcomeDialog; + public var userGuideDialog:UserGuideDialog; + public var aboutDialog:AboutDialog; + public var loadUrlDialog:LoadFromUrlDialog; + + public function onMenuItemClick(item:String) + { + switch (item.toLowerCase()) + { + case "undo" | "redo": + this.performLastAction(item.toLowerCase() == "redo"); + + case "save stage as": + var bytes = this.packShitToZip(); + + if (bytes == null) + { + notifyChange("Stage Save", "Problem Saving a Stage. Please try again later.", true); + return; + } + + FileUtil.saveFile(bytes, [FileUtil.FILE_FILTER_FNFS], function(path:String) { + saved = true; + currentFile = path; + }, null, stageName + "." + FileUtil.FILE_EXTENSION_INFO_FNFS.extension); + + bitmaps.clear(); + + case "save stage": + if (currentFile == "") + { + onMenuItemClick("save stage as"); // ah I love coding shortcuts + return; + } + + var bytes = this.packShitToZip(); + + if (bytes == null) + { + notifyChange("Stage Save", "Problem Saving a Stage. Please try again later.", true); + return; + } + + FileUtil.writeBytesToPath(currentFile, bytes, Force); // mhm + + saved = true; + + updateRecentFiles(); + bitmaps.clear(); + + case "open stage": + if (!saved) + { + Dialogs.messageBox("Opening a new Stage will reset all your progress for this Stage.\n\nAre you sure you want to proceed?", "Open Stage", + MessageBoxType.TYPE_YESNO, true, function(btn:DialogButton) { + if (btn == DialogButton.YES) + { + saved = true; + onMenuItemClick("open stage"); // ough + } + }); + + return; + } + + FileUtil.browseForSaveFile([FileUtil.FILE_FILTER_FNFS], function(path:String) { + clearAssets(); + + currentFile = path; + this.unpackShitFromZip(FileUtil.readBytesFromPath(path)); + + reloadRecentFiles(); + }, null, null, "Open Stage Data"); + + case "exit": + if (!saved) + { + Dialogs.messageBox("You are about to leave the Editor without Saving.\n\nAre you sure? ", "Leave Editor", MessageBoxType.TYPE_YESNO, true, + function(btn:DialogButton) { + if (btn == DialogButton.YES) + { + saved = true; + onMenuItemClick("exit"); + } + }); + + return; + } + + resetWindowTitle(); + + WindowUtil.windowExit.remove(windowClose); + CrashHandler.errorSignal.remove(autosavePerCrash); + CrashHandler.criticalErrorSignal.remove(autosavePerCrash); + + Cursor.hide(); + FlxG.switchState(() -> new DebugMenuSubState()); + FlxG.sound.music.stop(); + + case "switch mode": + if (!testingMode) moveMode = (moveMode == "assets" ? "chars" : "assets"); + + case "switch focus": + if (testingMode) + { + curTestChar++; + } + else + { + if (moveMode == "chars") + { + var chars = getCharacters(); + var index = chars.indexOf(selectedChar); + index++; + + if (index >= chars.length) index = 0; + + selectedChar = chars[index]; + } + else + { + if (selectedSprite == null) return; + + var index = spriteArray.indexOf(selectedSprite); + index++; + + if (index >= spriteArray.length) index = 0; + + selectedSprite = spriteArray[index]; + } + } + + case "new object": + findObjDialog.hideDialog(DialogButton.CANCEL); + + trace("aignt we making a new object baby"); + + objNameDialog = new NewObjDialog(this); + objNameDialog.showDialog(); + + objNameDialog.onDialogClosed = function(_) { + objNameDialog = null; + } + + case "find object": + findObjDialog.hideDialog(DialogButton.CANCEL); + findObjDialog = new FindObjDialog(this, selectedSprite == null ? "" : selectedSprite.name); + findObjDialog.showDialog(false); + + case "about": + aboutDialog = new AboutDialog(); + aboutDialog.showDialog(); + + case "user guide": + userGuideDialog = new UserGuideDialog(); + userGuideDialog.showDialog(); + + case "open folder": + #if sys + var absoluteBackupsPath:String = haxe.io.Path.join([Sys.getCwd(), BACKUPS_PATH]); + WindowUtil.openFolder(absoluteBackupsPath); + #end + + case "test stage": + if (!allowInput) return; + + camFollow.velocity.set(); + + for (a in spriteArray) + { + a.active = true; + a.isDebugged = testingMode; + } + + if (!testingMode) menubarItemWindowObject.selected = menubarItemWindowCharacter.selected = menubarItemWindowStage.selected = false; + + testingMode = !testingMode; + + case "clear assets": + Dialogs.messageBox("This will destroy all the Objects in this Stage.\n\nAre you sure? This cannot be undone.", "Clear Assets", + MessageBoxType.TYPE_YESNO, true, function(btn:DialogButton) { + if (btn == DialogButton.YES) + { + clearAssets(); + saved = false; + + updateDialog(StageEditorDialogType.OBJECT); + } + }); + + case "center on screen": + if (selectedSprite != null && moveMode == "assets") + { + selectedSprite.screenCenter(); + updateDialog(StageEditorDialogType.OBJECT); + saved = false; + } + + if (selectedChar != null && moveMode == "chars") + { + selectedChar.screenCenter(); + updateDialog(StageEditorDialogType.CHARACTER); + saved = false; + } + + case "delete object": + this.createAndPushAction(OBJECT_DELETED); + + spriteArray.remove(selectedSprite); + + selectedSprite.kill(); + remove(selectedSprite, true); + selectedSprite.destroy(); + selectedSprite = null; + + updateArray(); + sortAssets(); + + case "copy object": + if (selectedSprite == null) return; + + copiedSprite = selectedSprite.toData(true); + + case "paste object": + if (copiedSprite == null) return; + + saved = false; + var spr = new StageEditorObject().fromData(copiedSprite); + + var objNames = [for (a in spriteArray) a.name]; + + if (objNames.contains(spr.name)) + { + var i = 1; + while (objNames.contains(spr.name + " (" + i + ")")) + i++; + + spr.name += " (" + i + ")"; + } + + selectedSprite = spr; + updateArray(); + + case "cut object": // rofl + onMenuItemClick("copy object"); + onMenuItemClick("delete object"); // already changes the saved var + + case "new stage": + if (menubarItemWindowObject.selected) menubarItemWindowObject.selected = false; + if (menubarItemWindowCharacter.selected) menubarItemWindowCharacter.selected = false; + if (menubarItemWindowStage.selected) menubarItemWindowStage.selected = false; + + welcomeDialog = new WelcomeDialog(this); + welcomeDialog.showDialog(); + welcomeDialog.closable = true; + welcomeDialog.onDialogClosed = function(_) { + updateWindowTitle(); + welcomeDialog = null; + + updateDialog(StageEditorDialogType.OBJECT); + updateDialog(StageEditorDialogType.CHARACTER); + updateDialog(StageEditorDialogType.STAGE); + } + } + } + + public function clearAssets() + { + selectedSprite = null; + + while (spriteArray.length > 0) + { + var spr = spriteArray.pop(); + spr.kill(); + remove(spr, true); + spr.destroy(); + spr = null; + } + + undoArray = []; + redoArray = []; + + updateArray(); + sortAssets(); + removeUnusedBitmaps(); + } + + public function removeUnusedBitmaps() + { + var usedBitmaps:Array = []; + + for (asset in spriteArray) + { + var data = asset.toData(false); + if (data.assetPath.startsWith("#")) continue; // the simple graphics + + usedBitmaps.push(data.assetPath); + } + + for (name => bit in bitmaps) + { + if (usedBitmaps.contains(name)) continue; + bitmaps.remove(name); + } + } + + public function addBitmap(newBitmap:BitmapData):String + { + // first we check for existing bitmaps so we dont like add an extra one + for (name => bitmap in bitmaps) + { + if (bitmap == newBitmap) return name; + } + + var id:Int = 0; + while (bitmaps.exists("image" + id)) + id++; + + bitmaps.set("image" + id, newBitmap); + return "image" + id; + } + + public function notifyChange(change:String, notif:String, isError:Bool = false) + { + NotificationManager.instance.addNotification( + { + title: change, + body: notif, + type: isError ? NotificationType.Error : NotificationType.Info + }); + } + + public function createURLDialog(onComplete:lime.utils.Bytes->Void = null, onFail:String->Void = null) + { + loadUrlDialog = new LoadFromUrlDialog(onComplete, onFail); + loadUrlDialog.onDialogClosed = function(_) { + loadUrlDialog = null; + } + + loadUrlDialog.showDialog(); + } +} + +/** + * Available themes for the stage editor state. + */ +enum StageEditorTheme +{ + /** + * The default theme for the stage editor. + */ + Light; + + /** + * A theme which introduces stage colors. + */ + Dark; +} + +enum StageEditorDialogType +{ + /** + * The Stage Options Dialog. + */ + STAGE; + + /** + * The Character Options Dialog. + */ + CHARACTER; + + /** + * The Object Options Dialog. + */ + OBJECT; +} diff --git a/source/funkin/ui/debug/stageeditor/components/AboutDialog.hx b/source/funkin/ui/debug/stageeditor/components/AboutDialog.hx new file mode 100644 index 000000000..e419b964c --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/components/AboutDialog.hx @@ -0,0 +1,6 @@ +package funkin.ui.debug.stageeditor.components; + +import haxe.ui.containers.dialogs.Dialog; + +@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/dialogs/about.xml")) +class AboutDialog extends Dialog {} diff --git a/source/funkin/ui/debug/stageeditor/components/BackupAvailableDialog.hx b/source/funkin/ui/debug/stageeditor/components/BackupAvailableDialog.hx new file mode 100644 index 000000000..6c3041e28 --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/components/BackupAvailableDialog.hx @@ -0,0 +1,80 @@ +package funkin.ui.debug.stageeditor.components; + +import haxe.ui.containers.dialogs.Dialog; +import haxe.ui.containers.dialogs.Dialog.DialogButton; +import funkin.util.FileUtil; +import haxe.io.Path; +import funkin.util.DateUtil; +import funkin.util.WindowUtil; + +using StringTools; + +@:xml(' + + + +') +class BackupAvailableDialog extends Dialog +{ + override public function new(state:StageEditorState, filePath:String) + { + super(); + + if (!FileUtil.doesFileExist(filePath)) return; + + // time text + var fileDate = Path.withoutExtension(Path.withoutDirectory(filePath)); + var dateParts = fileDate.split("-"); + + while (dateParts.length < 8) + dateParts.push("0"); + + var year:Int = Std.parseInt(dateParts[2]) ?? 0; // copied parts from ChartEditorImportExportHandler.hx + var month:Int = Std.parseInt(dateParts[3]) ?? 1; + var day:Int = Std.parseInt(dateParts[4]) ?? 0; + var hour:Int = Std.parseInt(dateParts[5]) ?? 0; + var minute:Int = Std.parseInt(dateParts[6]) ?? 0; + var second:Int = Std.parseInt(dateParts[7]) ?? 0; + + backupTimeLabel.text = DateUtil.generateCleanTimestamp(new Date(year, month - 1, day, hour, minute, second)); + + // button callbacks + dialogCancel.onClick = function(_) hideDialog(DialogButton.CANCEL); + + buttonGoToFolder.onClick = function(_) { + // :[ + #if sys + var absoluteBackupsPath:String = Path.join([Sys.getCwd(), StageEditorState.BACKUPS_PATH]); + WindowUtil.openFolder(absoluteBackupsPath); + #end + } + + buttonOpenBackup.onClick = function(_) { + if (FileUtil.doesFileExist(filePath) && state.welcomeDialog != null) // doing a check in case a sleezy FUCK decides to delete the backup file AFTER dialog opens + { + state.welcomeDialog.loadFromFilePath(filePath); + } + hideDialog(DialogButton.APPLY); + } + + // uhhh + onDialogClosed = function(event) { + if (event.button == DialogButton.APPLY) + { + if (state.welcomeDialog != null) state.welcomeDialog.hideDialog(DialogButton.APPLY); + } + }; + } +} diff --git a/source/funkin/ui/debug/stageeditor/components/ExitConfirmDialog.hx b/source/funkin/ui/debug/stageeditor/components/ExitConfirmDialog.hx new file mode 100644 index 000000000..91669dc4a --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/components/ExitConfirmDialog.hx @@ -0,0 +1,31 @@ +package funkin.ui.debug.stageeditor.components; + +import haxe.ui.containers.dialogs.Dialog; + +@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/dialogs/exit-confirm.xml")) +class ExitConfirmDialog extends Dialog +{ + var onComplete:Void->Void = null; + + override public function new(onComp:Void->Void) + { + super(); + + onComplete = onComp; + + buttons = DialogButton.CANCEL | "{{Proceed}}"; + defaultButton = "{{Proceed}}"; + + destroyOnClose = true; + } + + public override function validateDialog(button:DialogButton, fn:Bool->Void) + { + if (button == "{{Proceed}}" && onComplete != null) + { + onComplete(); + } + + fn(true); + } +} diff --git a/source/funkin/ui/debug/stageeditor/components/FindObjDialog.hx b/source/funkin/ui/debug/stageeditor/components/FindObjDialog.hx new file mode 100644 index 000000000..affb862fe --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/components/FindObjDialog.hx @@ -0,0 +1,105 @@ +package funkin.ui.debug.stageeditor.components; + +import haxe.ui.containers.dialogs.Dialog; +import funkin.play.stage.StageProp; +import funkin.ui.debug.stageeditor.handlers.AssetDataHandler; +import haxe.ui.components.TextField; +import haxe.ui.components.CheckBox; + +@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/dialogs/find-object.xml")) +class FindObjDialog extends Dialog +{ + var stageEditorState:StageEditorState; + + var assets:Array = []; + var curSelected:Int = 0; + var field:TextField; + + var checkWord:CheckBox; + var checkCaps:CheckBox; + + override public function new(state:StageEditorState, searchFor:String = "") + { + super(); + + stageEditorState = state; + nameField.text = searchFor; + + this.field = nameField; + this.checkWord = wordCheck; + this.checkCaps = capsCheck; + + field.onChange = function(_) updateIndicator(); + indicator.hide(); + + top = 20; + left = FlxG.width - width - 20; + + buttons = DialogButton.CANCEL | "{{Find Next}}"; + defaultButton = "{{Find Next}}"; + } + + public function updateIndicator() + { + var prevObjCheck = assets[curSelected]; + + assets = []; + + for (ass in stageEditorState.spriteArray) + { + var name = ass.name; + var checkFor = field.text; + + if (!checkCaps.selected) + { + name = name.toLowerCase(); + checkFor = checkFor.toLowerCase(); + } + + if (((name.contains(checkFor) && !checkWord.selected) || (name == checkFor && checkWord.selected)) && ass.visible) assets.push(ass); + } + + if (assets.length > 0 && prevObjCheck == null) + { + stageEditorState.selectedSprite = assets[0]; + } + + if (assets.length > 0) + { + indicator.text = "Selected: " + (assets.indexOf(stageEditorState.selectedSprite) + 1) + " / " + assets.length; + } + else + { + indicator.text = "No Matches Found"; + } + + if (field.text != "" && field.text != null) indicator.show(); + else + indicator.hide(); + } + + public override function validateDialog(button:DialogButton, fn:Bool->Void) + { + var done = true; + + if (button == "{{Find Next}}") + { + done = false; + + if (assets.length > 0) + { + curSelected = assets.indexOf(stageEditorState.selectedSprite); + curSelected++; + + if (curSelected >= assets.length) curSelected = 0; + + stageEditorState.selectedSprite = assets[curSelected]; + indicator.text = "Selected: " + (assets.indexOf(stageEditorState.selectedSprite) + 1) + " / " + assets.length; + + stageEditorState.camFollow.x = assets[curSelected].getMidpoint().x; + stageEditorState.camFollow.y = assets[curSelected].getMidpoint().y; + } + } + fn(done); + } +} diff --git a/source/funkin/ui/debug/stageeditor/components/LoadFromUrlDialog.hx b/source/funkin/ui/debug/stageeditor/components/LoadFromUrlDialog.hx new file mode 100644 index 000000000..18f16a70d --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/components/LoadFromUrlDialog.hx @@ -0,0 +1,70 @@ +package funkin.ui.debug.stageeditor.components; + +import haxe.ui.containers.dialogs.Dialog; +import lime.utils.Bytes; +import haxe.ui.components.TextField; +import openfl.net.URLLoader; +import openfl.net.URLRequest; +import openfl.events.Event; +import openfl.events.IOErrorEvent; +import openfl.events.ProgressEvent; +import openfl.events.SecurityErrorEvent; +import openfl.utils.ByteArray; + +@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/dialogs/load-url.xml")) +class LoadFromUrlDialog extends Dialog +{ + var urlField:TextField; + var loader:URLLoader; + + override public function new(successCallback:Bytes->Void = null, failCallback:String->Void = null) + { + super(); + destroyOnClose = true; + + loader = new URLLoader(); + loader.dataFormat = BINARY; + + urlField.text = ""; + + loader.addEventListener(Event.COMPLETE, function(event:Event) { + var bytes:Bytes = cast(loader.data, ByteArray); + + if (successCallback != null) successCallback(bytes); + + trace("loaded the image and did success callback"); + + @:privateAccess + loader.__removeAllListeners(); + + hideDialog(DialogButton.CANCEL); + }); + + loader.addEventListener(IOErrorEvent.IO_ERROR, function(event:IOErrorEvent) { + if (failCallback != null) failCallback(urlField.text); + + trace("error with this shit"); + }); + + loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, function(event:SecurityErrorEvent) { + if (failCallback != null) failCallback(urlField.text); + + trace("error with this shit"); + }); + + buttons = DialogButton.CANCEL | "{{Load}}"; + defaultButton = "{{Load}}"; + } + + override public function validateDialog(button:DialogButton, fn:Bool->Void) + { + if (button == DialogButton.CANCEL) + { + fn(true); + } + else + { + loader.load(new URLRequest(urlField.text)); + } + } +} diff --git a/source/funkin/ui/debug/stageeditor/components/NewObjDialog.hx b/source/funkin/ui/debug/stageeditor/components/NewObjDialog.hx new file mode 100644 index 000000000..0ad831731 --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/components/NewObjDialog.hx @@ -0,0 +1,90 @@ +package funkin.ui.debug.stageeditor.components; + +import haxe.ui.containers.dialogs.Dialog; +import haxe.ui.components.Link; +import funkin.ui.debug.stageeditor.handlers.AssetDataHandler; +import funkin.save.Save; +import funkin.util.FileUtil; +import lime.ui.FileDialog; +import flixel.FlxG; +import openfl.display.BitmapData; +import haxe.ui.notifications.NotificationType; +import haxe.ui.notifications.NotificationManager; +import funkin.play.stage.StageProp; + +@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/dialogs/new-object.xml")) +class NewObjDialog extends Dialog +{ + var stageEditorState:StageEditorState; + var bitmap:BitmapData; + + override public function new(state:StageEditorState, img:BitmapData = null) + { + super(); + + stageEditorState = state; + bitmap = img; + + field.onChange = function(_) { + field.removeClasses(["invalid-value", "valid-value"]); + } + + buttons = DialogButton.CANCEL | "{{Create}}"; + defaultButton = "{{Create}}"; + + destroyOnClose = true; + } + + public override function validateDialog(button:DialogButton, fn:Bool->Void) + { + var done = true; + + if (button == "{{Create}}") + { + var objNames = [for (obj in StageEditorState.instance.spriteArray) obj.name]; + + if (field.text == "" || field.text == null || objNames.contains(field.text)) + { + field.swapClass("invalid-value", "valid-value"); + done = false; + NotificationManager.instance.addNotification( + { + title: "Problem Creating an Object", + body: objNames.contains(field.text) ? "Object with the Name " + field.text + " already exists!" : "Invalid Object Name!", + type: NotificationType.Error + }); + } + else + { + var spr = new StageEditorObject(); + + if (bitmap != null) + { + var bitToLoad = stageEditorState.addBitmap(bitmap); + spr.loadGraphic(stageEditorState.bitmaps[bitToLoad]); + } + else + spr.loadGraphic(AssetDataHandler.getDefaultGraphic()); + + spr.name = field.text; + spr.screenCenter(); + spr.zIndex = 0; + + stageEditorState.selectedSprite = spr; + stageEditorState.createAndPushAction(OBJECT_CREATED); + + stageEditorState.add(spr); + stageEditorState.updateArray(); + stageEditorState.saved = false; + + NotificationManager.instance.addNotification( + { + title: "Object Creating Successful", + body: "Successfully created an Object with the Name " + field.text + "!", + type: NotificationType.Success + }); + } + } + fn(done); + } +} diff --git a/source/funkin/ui/debug/stageeditor/components/UserGuideDialog.hx b/source/funkin/ui/debug/stageeditor/components/UserGuideDialog.hx new file mode 100644 index 000000000..190267184 --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/components/UserGuideDialog.hx @@ -0,0 +1,6 @@ +package funkin.ui.debug.stageeditor.components; + +import haxe.ui.containers.dialogs.Dialog; + +@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/dialogs/user-guide.xml")) +class UserGuideDialog extends Dialog {} diff --git a/source/funkin/ui/debug/stageeditor/components/WelcomeDialog.hx b/source/funkin/ui/debug/stageeditor/components/WelcomeDialog.hx new file mode 100644 index 000000000..c20cc5f8a --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/components/WelcomeDialog.hx @@ -0,0 +1,146 @@ +package funkin.ui.debug.stageeditor.components; + +import haxe.ui.containers.dialogs.Dialog; +import haxe.ui.containers.dialogs.Dialogs; +import haxe.ui.containers.dialogs.MessageBox.MessageBoxType; +import haxe.ui.components.Link; +import funkin.ui.debug.stageeditor.handlers.StageDataHandler; +import funkin.save.Save; +import funkin.util.FileUtil; +import lime.ui.FileDialog; +import flixel.FlxG; +import funkin.input.Cursor; +import funkin.data.stage.StageData; +import funkin.data.stage.StageRegistry; +import funkin.ui.debug.stageeditor.StageEditorState.StageEditorDialogType; + +using funkin.util.tools.FloatTools; + +@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/dialogs/welcome.xml")) +class WelcomeDialog extends Dialog +{ + var stageEditorState:StageEditorState; + + override public function new(state:StageEditorState) + { + super(); + + stageEditorState = state; + + buttonNew.onClick = function(_) { + stageEditorState.clearAssets(); + stageEditorState.loadDummyData(); + stageEditorState.currentFile = ""; + killDaDialog(); + } + + for (file in Save.instance.stageEditorPreviousFiles) + { + trace(file); + + if (!FileUtil.doesFileExist(file)) continue; // whats the point of loading something that doesnt exist + + var patj = new haxe.io.Path(file); + + var fileText = new Link(); + fileText.percentWidth = 100; + fileText.text = patj.file + "." + patj.ext; + fileText.onClick = function(_) loadFromFilePath(file); + + #if sys + var stat = sys.FileSystem.stat(file); + var sizeInMB = (stat.size / 1000000).round(2); + + fileText.tooltip = "Full Name: " + file + "\nLast Modified: " + stat.mtime.toString() + "\nSize: " + sizeInMB + "MB"; + #end + + contentRecent.addComponent(fileText); + } + + boxDrag.onClick = function(_) FileUtil.browseForSaveFile([FileUtil.FILE_FILTER_FNFS], loadFromFilePath, null, null, "Open Stage Data"); + + var defaultStages = StageRegistry.instance.listBaseGameStageIds(); + defaultStages.sort(funkin.util.SortUtil.alphabetically); + + for (stage in defaultStages) + { + var baseStage = StageRegistry.instance.parseEntryDataWithMigration(stage, StageRegistry.instance.fetchEntryVersion(stage)); + if (baseStage == null) continue; + + var link = new Link(); // this is how the legend of zelda started btw + link.percentWidth = 100; + link.text = baseStage.name; + + link.onClick = function(_) loadFromPreset(baseStage); + + contentPresets.addComponent(link); + } + + FlxG.stage.window.onDropFile.add(loadFromFilePath); + } + + public function loadFromPreset(data:StageData) + { + if (data == null) return; + + if (!stageEditorState.saved) + { + Dialogs.messageBox("This will destroy all of your Unsaved Work.\n\nAre you sure? This cannot be undone.", "Load Stage", MessageBoxType.TYPE_YESNO, true, + function(btn:DialogButton) { + if (btn == DialogButton.YES) + { + stageEditorState.saved = true; + loadFromPreset(data); + } + }); + + return; + } + + stageEditorState.clearAssets(); + stageEditorState.currentFile = ""; + stageEditorState.loadFromDataRaw(data); + killDaDialog(); + } + + public function loadFromFilePath(file:String) + { + if (!stageEditorState.saved) + { + Dialogs.messageBox("This will destroy all of your Unsaved Work.\n\nAre you sure? This cannot be undone.", "Load Stage", MessageBoxType.TYPE_YESNO, true, + function(btn:DialogButton) { + if (btn == DialogButton.YES) + { + stageEditorState.saved = true; + loadFromFilePath(file); + } + }); + + return; + } + + var bytes = FileUtil.readBytesFromPath(file); + + if (bytes == null) + { + stageEditorState.notifyChange("Problem Loading the Stage", "The Stage File could not be loaded.", true); + return; + } + + stageEditorState.clearAssets(); + stageEditorState.currentFile = file; + stageEditorState.unpackShitFromZip(bytes); + killDaDialog(); + } + + function killDaDialog() + { + stageEditorState.updateDialog(StageEditorDialogType.OBJECT); + stageEditorState.updateDialog(StageEditorDialogType.CHARACTER); + stageEditorState.updateDialog(StageEditorDialogType.STAGE); + + FlxG.stage.window.onDropFile.remove(loadFromFilePath); + hide(); + destroy(); + } +} diff --git a/source/funkin/ui/debug/stageeditor/handlers/AssetDataHandler.hx b/source/funkin/ui/debug/stageeditor/handlers/AssetDataHandler.hx new file mode 100644 index 000000000..a6f57bd7c --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/handlers/AssetDataHandler.hx @@ -0,0 +1,208 @@ +package funkin.ui.debug.stageeditor.handlers; + +import flixel.FlxG; +import openfl.display.BitmapData; +import flixel.FlxSprite; +import flixel.util.FlxColor; +import flixel.graphics.frames.FlxAtlasFrames; +import flixel.math.FlxRect; +import openfl.display.BlendMode; +import flixel.math.FlxPoint; +import funkin.data.stage.StageData.StageDataProp; + +using StringTools; + +/** + * Handles the Stage Props and Datas - being able to convert one to the other. + */ +class AssetDataHandler +{ + static var state:StageEditorState; + + public static function init(state:StageEditorState) + { + AssetDataHandler.state = state; + } + + /** + * Turns an Object into Data. + * @param obj the Object whose data to read. + * @param useBitmaps Whether to Save object's BitmapData directly. + * @return Data of the Object + */ + public static function toData(obj:StageEditorObject, useBitmaps:Bool = false):StageEditorObjectData + { + var outputData:StageEditorObjectData = + { + name: obj.name, + assetPath: "", + position: [obj.x, obj.y], + zIndex: obj.zIndex, + isPixel: !obj.antialiasing, + scale: obj.scale.x == obj.scale.y ? Left(obj.scale.x) : Right([obj.scale.x, obj.scale.y]), + alpha: obj.alpha, + danceEvery: obj.animation.getNameList().length > 0 ? obj.danceEvery : 0, + scroll: [obj.scrollFactor.x, obj.scrollFactor.y], + animations: [for (n => d in obj.animDatas) d], + startingAnimation: obj.startingAnimation, + animType: "sparrow", // automatically making sparrow atlases yeah + angle: obj.angle, + blend: obj.blend == null ? "" : Std.string(obj.blend), + color: obj.color.toWebString(), + xmlData: obj.generateXML() + } + + if (useBitmaps) + { + outputData.bitmap = obj.pixels.clone(); + return outputData; + } + + for (name => bit in state.bitmaps) + { + if (areTheseBitmapsEqual(bit, obj.pixels)) + { + outputData.assetPath = name; + return outputData; + } + } + + outputData.assetPath = "#FFFFFF"; + + return outputData; + } + + /** + * Modifies an Object based on the Data. + * @param object Object to modify. Set to null to create a new one. + * @param data The Data used for the Object. + */ + public static function fromData(object:StageEditorObject, data:StageEditorObjectData) + { + if (data.bitmap != null) + { + var bitToLoad = state.addBitmap(data.bitmap.clone()); + object.loadGraphic(state.bitmaps[bitToLoad]); + } + else + { + if (data.animations != null && data.animations.length > 0) // considering we're unpacking we might as well just do this instead of switch + { + object.frames = flixel.graphics.frames.FlxAtlasFrames.fromSparrow(state.bitmaps[data.assetPath].clone(), data.xmlData); + } + else if (data.assetPath.startsWith("#")) + { + object.loadGraphic(getDefaultGraphic()); + object.color = FlxColor.fromString(data.assetPath); + } + else + object.loadGraphic(state.bitmaps[data.assetPath].clone()); + } + + object.name = data.name; + object.setPosition(data.position[0], data.position[1]); + object.zIndex = data.zIndex; + object.antialiasing = !data.isPixel; + object.alpha = data.alpha; + object.danceEvery = data.danceEvery; + object.scrollFactor.set(data.scroll[0], data.scroll[1]); + object.startingAnimation = data.startingAnimation; + object.angle = data.angle; + object.blend = blendFromString(data.blend); + if (!data.assetPath.startsWith("#")) object.color = FlxColor.fromString(data.color); + + // yeah + object.pixelPerfectRender = data.isPixel; + object.pixelPerfectPosition = data.isPixel; + + for (anim in data.animations) + { + object.addAnim(anim.name, anim.prefix, anim.offsets ?? [0, 0], anim.frameIndices ?? [], anim.frameRate ?? 24, anim.looped ?? false, anim.flipX ?? false, + anim.flipY ?? false); + } + + if (object.animation.getNameList().contains(data.startingAnimation)) object.startingAnimation = data.startingAnimation; + + switch (data.scale) + { + case Left(value): + object.scale.set(value, value); + + case Right(values): + object.scale.set(values[0], values[1]); + } + object.updateHitbox(); + + object.playAnim(object.startingAnimation); + + flixel.util.FlxTimer.wait(StageEditorState.TIME_BEFORE_ANIM_STOP, function() { + if (object != null && object.animation.curAnim != null) object.animation.stop(); + }); + + return object; + } + + /** + * Returns a default BitmapData to be used for all the props. + * @return BitmapData + */ + public static function getDefaultGraphic():BitmapData + { + return new FlxSprite().makeGraphic(1, 1, FlxColor.WHITE).pixels.clone(); + } + + /** + * Returns OpenFL's BlendMode based on the Name. + * @param blend the BlendMode Name. + * @return BlendMode + */ + public static function blendFromString(blend:String):BlendMode + { + // originally this was a MASSIVE and I do mean MASSIVE switch case, though then I found out that blendmode already has one implemented + @:privateAccess + return BlendMode.fromString(blend.toLowerCase().trim()); + } + + public static function generateXML(obj:StageEditorObject) + { + // the last check is for if the only frame is the standard graphic frame + if (obj == null || obj.frames.frames.length == 0 || obj.frames.frames[0].name == null) return ""; + + var xml = [ + "", + '', + '' + ].join("\n"); + + for (daFrame in obj.frames.frames) + { + xml += ' \n'; + } + + xml += ""; + return xml; + } + + // I am aware OpenFL has it's own compare bitmap function, though I find this to be better ngl + static function areTheseBitmapsEqual(bitmap1:BitmapData, bitmap2:BitmapData) + { + if (bitmap1.width != bitmap2.width || bitmap1.height != bitmap2.height) return false; + + for (px in 0...bitmap1.width) + { + for (py in 0...bitmap1.height) + { + if (bitmap1.getPixel32(px, py) != bitmap2.getPixel32(px, py)) return false; + } + } + + return true; + } +} + +typedef StageEditorObjectData = +{ + > StageDataProp, + var xmlData:String; + var ?bitmap:BitmapData; +} diff --git a/source/funkin/ui/debug/stageeditor/handlers/StageDataHandler.hx b/source/funkin/ui/debug/stageeditor/handlers/StageDataHandler.hx new file mode 100644 index 000000000..56dc7a775 --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/handlers/StageDataHandler.hx @@ -0,0 +1,365 @@ +package funkin.ui.debug.stageeditor.handlers; + +import funkin.ui.debug.stageeditor.handlers.AssetDataHandler; +import haxe.io.Bytes; +import funkin.util.FileUtil; +import openfl.display.BitmapData; +import haxe.Json; +import haxe.zip.Entry; +import funkin.play.character.BaseCharacter.CharacterType; +import funkin.play.character.BaseCharacter; +import funkin.data.stage.StageData; +import funkin.data.stage.StageData.StageDataCharacter; +import funkin.data.stage.StageRegistry; +import openfl.utils.Assets as OpenFLAssets; +import lime.utils.Assets as LimeAssets; + +using StringTools; + +class StageDataHandler +{ + public static function checkForCharacter(char:BaseCharacter) + return char != null; + + public static function packShitToZip(state:StageEditorState) + { + // step 1: data + var endData:StageData = new StageData(); + endData.name = state.stageName; + endData.cameraZoom = state.stageZoom; + endData.directory = state.stageFolder; + + // step 1 phase 1: object data + var xmlMap:Map = []; + + for (obj in state.spriteArray) + { + var data = obj.toData(false); + endData.props.push( + { + name: data.name, + assetPath: data.assetPath.startsWith("#") ? data.color : data.assetPath, + position: data.position.copy(), + zIndex: data.zIndex, + isPixel: data.isPixel, + scale: data.scale, + alpha: data.alpha, + danceEvery: data.danceEvery, + scroll: data.scroll.copy(), + animations: data.animations, + startingAnimation: data.startingAnimation, + animType: data.animType, + angle: data.angle, + blend: data.blend, + color: data.assetPath.startsWith("#") ? "#FFFFFF" : data.color + }); + + if (!xmlMap.exists(data.assetPath) && data.xmlData != "") xmlMap.set(data.assetPath, data.xmlData); + } + + // step 1 phase 2: character data + endData.characters.bf.zIndex = state.charGroups[CharacterType.BF].zIndex; + endData.characters.dad.zIndex = state.charGroups[CharacterType.DAD].zIndex; + endData.characters.gf.zIndex = state.charGroups[CharacterType.GF].zIndex; + + endData.characters.bf.scale = state.bf.scale.x / state.bf.getBaseScale(); + endData.characters.dad.scale = state.dad.scale.x / state.dad.getBaseScale(); + endData.characters.gf.scale = state.gf.scale.x / state.gf.getBaseScale(); + + endData.characters.bf.cameraOffsets = state.charCamOffsets[CharacterType.BF].copy(); + endData.characters.gf.cameraOffsets = state.charCamOffsets[CharacterType.GF].copy(); + endData.characters.dad.cameraOffsets = state.charCamOffsets[CharacterType.DAD].copy(); + + endData.characters.bf.position = [ + state.bf.feetPosition.x - state.bf.globalOffsets[0], + state.bf.feetPosition.y - state.bf.globalOffsets[1] + ]; + + endData.characters.gf.position = [ + state.gf.feetPosition.x - state.gf.globalOffsets[0], + state.gf.feetPosition.y - state.gf.globalOffsets[1] + ]; + + endData.characters.dad.position = [ + state.dad.feetPosition.x - state.dad.globalOffsets[0], + state.dad.feetPosition.y - state.dad.globalOffsets[1] + ]; + + // step 2: saving everything to entryList + var entryList = new Array(); + + // step 2 phase 1: images + state.removeUnusedBitmaps(); + for (name => img in state.bitmaps) + { + var bytes = img?.image?.encode(PNG); + if (bytes == null) continue; + + var entry:Entry = + { + fileName: name + ".png", + fileSize: bytes.length, + fileTime: Date.now(), + compressed: false, + dataSize: bytes.length, + data: bytes, + crc32: null // apparently fileutil.hx does not like crc32, idk why but i dont even know what crc32 is + } + + entryList.push(entry); + } + + // step 2 phase 2: xmls + for (obj in endData.props) + { + if (!xmlMap.exists(obj.assetPath)) continue; // damn + + var bytes = Bytes.ofString(xmlMap[obj.assetPath]); + + var entry:Entry = + { + fileName: obj.assetPath + ".xml", + fileSize: bytes.length, + fileTime: Date.now(), + compressed: false, + dataSize: bytes.length, + data: bytes, + crc32: null + } + + entryList.push(entry); + } + + // step 2 phase 3: the main data + var stageBytes = Bytes.ofString(endData.serialize()); + entryList.push( + { + fileName: "yourstagename.json", + fileSize: stageBytes.length, + fileTime: Date.now(), + compressed: false, + dataSize: stageBytes.length, + data: stageBytes, + crc32: null + }); + + var zipFileBytes = FileUtil.createZIPFromEntries(entryList); + return zipFileBytes; + } + + public static function unpackShitFromZip(state:StageEditorState, zip:Bytes) + { + state.clearAssets(); + state.bitmaps.clear(); + + var entries = FileUtil.readZIPFromBytes(zip); + var stageData:StageData = new StageData(); + + var xmls:Map = []; + + for (stuff in entries) + { + var ext = stuff.fileName.split(".")[1]; + + switch (ext) + { + case "png": + var data = BitmapData.fromBytes(stuff.data); + state.bitmaps.set(stuff.fileName.replace(".png", ""), data); + + case "xml": + xmls.set(stuff.fileName.replace(".xml", ""), stuff.data.toString()); + + case "json": + stageData = StageRegistry.instance.parseEntryDataRaw(stuff.data.toString(), stuff.fileName); + } + } + + if (stageData == null) + { + // TODO: throw an error, then load a dummy data + loadDummyData(state); + return; + } + + // actual data unpacking + state.stageName = stageData.name; + state.stageZoom = stageData.cameraZoom; + state.stageFolder = stageData.directory ?? "shared"; + + // chars + state.loadCharDatas(stageData); + + // objects + for (objData in stageData.props) + { + // make the data and roll with it + var spr = new StageEditorObject(); + spr.fromData( + { + name: objData.name ?? "Unnamed", + assetPath: objData.assetPath, + animations: objData.animations.copy(), + scale: objData.scale, + position: objData.position, + alpha: objData.alpha, + angle: objData.angle, + zIndex: objData.zIndex, + danceEvery: objData.danceEvery, + isPixel: objData.isPixel, + scroll: objData.scroll.copy(), + color: objData.color, + blend: objData.blend, + startingAnimation: objData.startingAnimation, + xmlData: xmls[objData.assetPath] ?? "" + }); + + state.add(spr); + } + + state.updateArray(); + state.sortAssets(); + state.updateMarkerPos(); + } + + static function loadCharDatas(state:StageEditorState, data:StageData) + { + var chars = state.getCharacters(); + for (char in chars) + { + var charData:StageDataCharacter = null; + + switch (char.characterType) + { + case CharacterType.BF: + charData = data.characters.bf; + case CharacterType.GF: + charData = data.characters.gf; + case CharacterType.DAD: + charData = data.characters.dad; + default: // nothing rip + } + + char.resetCharacter(true); + + if (charData == null) continue; + + char.x = charData.position[0] - char.characterOrigin.x + char.globalOffsets[0]; + char.y = charData.position[1] - char.characterOrigin.y + char.globalOffsets[1]; + state.charGroups[char.characterType].zIndex = charData.zIndex; + + char.setScale(char.getBaseScale() * charData.scale); + char.cameraFocusPoint.x += charData.cameraOffsets[0]; + char.cameraFocusPoint.y += charData.cameraOffsets[1]; + + state.charCamOffsets[char.characterType] = charData.cameraOffsets.copy(); + } + } + + public static function loadFromDataRaw(state:StageEditorState, data:StageData) + { + state.clearAssets(); + state.bitmaps.clear(); + + if (data == null) + { + loadDummyData(state); + return; + } + @:privateAccess + if (!LimeAssets.libraryPaths.exists(data.directory)) + { + loadDummyData(state); + return; + } + + Paths.setCurrentLevel(data.directory); + + if (OpenFLAssets.getLibrary(data.directory) == null) + { + OpenFLAssets.loadLibrary(data.directory).onComplete(function(_) { + loadFromDataRaw(state, data); + }); + return; + } + + state.stageName = data.name; + state.stageZoom = data.cameraZoom; + state.stageFolder = data.directory ?? "shared"; + + state.loadCharDatas(data); + + for (objData in data.props) + { + var spr = new StageEditorObject(); + if (!objData.assetPath.startsWith("#")) state.bitmaps.set(objData.assetPath, Assets.getBitmapData(Paths.image(objData.assetPath))); + + spr.fromData( + { + name: objData.name ?? "Unnamed", + assetPath: objData.assetPath, + animations: objData.animations.copy(), + scale: objData.scale, + position: objData.position, + alpha: objData.alpha, + angle: objData.angle, + zIndex: objData.zIndex, + danceEvery: objData.danceEvery, + isPixel: objData.isPixel, + scroll: objData.scroll.copy(), + color: objData.color, + blend: objData.blend, + startingAnimation: objData.startingAnimation, + xmlData: Assets.exists(Paths.file("images/" + objData.assetPath + ".xml")) ? Assets.getText(Paths.file("images/" + objData.assetPath + ".xml")) : "" + }); + + state.add(spr); + } + + state.updateArray(); + state.sortAssets(); + state.updateMarkerPos(); + } + + public static function loadDummyData(state:StageEditorState) + { + state.clearAssets(); + + state.stageName = "Unnamed"; + state.stageZoom = 1.0; + state.stageFolder = "shared"; + + state.charCamOffsets = StageEditorState.DEFAULT_CAMERA_OFFSETS.copy(); + state.charPos = StageEditorState.DEFAULT_POSITIONS.copy(); + + state.gf.resetCharacter(true); + state.dad.resetCharacter(true); + state.bf.resetCharacter(true); + + state.charGroups[CharacterType.BF].zIndex = 300; + state.charGroups[CharacterType.DAD].zIndex = 200; + state.charGroups[CharacterType.GF].zIndex = 100; + + state.gf.x = state.charPos[CharacterType.GF][0] - state.gf.characterOrigin.x + state.gf.globalOffsets[0]; + state.gf.y = state.charPos[CharacterType.GF][1] - state.gf.characterOrigin.y + state.gf.globalOffsets[1]; + state.dad.x = state.charPos[CharacterType.DAD][0] - state.dad.characterOrigin.x + state.dad.globalOffsets[0]; + state.dad.y = state.charPos[CharacterType.DAD][1] - state.dad.characterOrigin.y + state.dad.globalOffsets[1]; + state.bf.x = state.charPos[CharacterType.BF][0] - state.bf.characterOrigin.x + state.bf.globalOffsets[0]; + state.bf.y = state.charPos[CharacterType.BF][1] - state.bf.characterOrigin.y + state.bf.globalOffsets[1]; + + state.gf.setScale(state.gf.getBaseScale()); + state.dad.setScale(state.dad.getBaseScale()); + state.bf.setScale(state.bf.getBaseScale()); + + state.gf.cameraFocusPoint.x += state.charCamOffsets[CharacterType.GF][0]; + state.gf.cameraFocusPoint.y += state.charCamOffsets[CharacterType.GF][1]; + state.dad.cameraFocusPoint.x += state.charCamOffsets[CharacterType.DAD][0]; + state.dad.cameraFocusPoint.y += state.charCamOffsets[CharacterType.DAD][1]; + state.bf.cameraFocusPoint.x += state.charCamOffsets[CharacterType.BF][0]; + state.bf.cameraFocusPoint.y += state.charCamOffsets[CharacterType.BF][1]; + + // no props :p + + state.updateMarkerPos(); + } +} diff --git a/source/funkin/ui/debug/stageeditor/handlers/UndoRedoHandler.hx b/source/funkin/ui/debug/stageeditor/handlers/UndoRedoHandler.hx new file mode 100644 index 000000000..947bb5cf3 --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/handlers/UndoRedoHandler.hx @@ -0,0 +1,191 @@ +package funkin.ui.debug.stageeditor.handlers; + +import funkin.play.character.BaseCharacter.CharacterType; +import funkin.ui.debug.stageeditor.handlers.AssetDataHandler.StageEditorObjectData; +import funkin.ui.debug.stageeditor.StageEditorState.StageEditorDialogType; + +class UndoRedoHandler +{ + public static function performLastAction(state:StageEditorState, redo:Bool = false) + { + if (state == null || (state.undoArray.length <= 0 && !redo) || (state.redoArray.length <= 0 && redo)) return; + var actionToDo = redo ? state.redoArray.pop() : state.undoArray.pop(); + + switch (actionToDo.type) + { + case CHARACTER_MOVED: + createAndPushAction(state, actionToDo.type, !redo); + + var type = actionToDo.data.type == null ? CharacterType.BF : actionToDo.data.type; + var pos = actionToDo.data.pos == null ? [0, 0] : actionToDo.data.pos; + + for (char in state.getCharacters()) + { + if (char.characterType == type) state.selectedChar = char; + } + + state.selectedChar.x = pos[0] - state.selectedChar.characterOrigin.x + state.selectedChar.globalOffsets[0]; + state.selectedChar.y = pos[1] - state.selectedChar.characterOrigin.y + state.selectedChar.globalOffsets[1]; + + state.updateMarkerPos(); + state.updateDialog(StageEditorDialogType.CHARACTER); + + case OBJECT_MOVED: + var id = actionToDo.data.ID ?? -1; + var pos = actionToDo.data.pos ?? [0, 0]; + + for (obj in state.spriteArray) + { + if (obj.ID == id) state.selectedSprite = obj; + } + + if (state.selectedSprite != null) + { + createAndPushAction(state, actionToDo.type, !redo); + + state.selectedSprite.x = pos[0]; + state.selectedSprite.y = pos[1]; + + state.updateDialog(StageEditorDialogType.OBJECT); + } + + case OBJECT_CREATED: // this removes the object + var id = actionToDo.data.ID ?? -1; + + for (obj in state.spriteArray) + { + if (obj.ID == id) + { + state.selectedSprite = obj; + createAndPushAction(state, OBJECT_DELETED, !redo); + + state.selectedSprite = null; + + obj.kill(); + state.remove(obj, true); + obj.destroy(); + + state.updateArray(); + state.updateDialog(StageEditorDialogType.OBJECT); + trace("found object"); + + continue; + } + } + + case OBJECT_DELETED: // this creates the object + if (actionToDo.data.data == null) return; + + var id = actionToDo.data.ID ?? -1; + var data:StageEditorObjectData = cast actionToDo.data.data; + + var obj = new StageEditorObject().fromData(data); + obj.ID = id; + state.selectedSprite = obj; + + createAndPushAction(state, OBJECT_CREATED, !redo); + state.add(obj); + + state.updateDialog(StageEditorDialogType.OBJECT); + state.updateArray(); + + case OBJECT_ROTATED: // primarily copied from OBJECT_MOVED + var id = actionToDo.data.ID ?? -1; + var angle = actionToDo.data.angle ?? 0; + + for (obj in state.spriteArray) + { + if (obj.ID == id) state.selectedSprite = obj; + } + + if (state.selectedSprite != null) + { + createAndPushAction(state, actionToDo.type, !redo); + state.selectedSprite.angle = angle; + state.updateDialog(StageEditorDialogType.OBJECT); + } + + default: // do nothing dumbass + } + } + + public static function createAndPushAction(state:StageEditorState, action:UndoActionType, redo:Bool = false) + { + if (state == null) return; + + var finalAction:UndoAction = {type: action, data: null}; + + if (!redo && state.redoArray.length > 0) state.redoArray = []; // incorporate resetting as well + + switch (action) + { + case CHARACTER_MOVED: + var char = state.selectedChar.characterType; + finalAction.data = {type: char, pos: state.charPos[char].copy()}; + + case OBJECT_MOVED: + finalAction.data = {ID: state.selectedSprite.ID, pos: [state.selectedSprite.x, state.selectedSprite.y]} + + case OBJECT_CREATED: + finalAction.data = {ID: state.selectedSprite.ID} + + case OBJECT_DELETED: + finalAction.data = + { + ID: state.selectedSprite.ID, + data: state.selectedSprite.toData(true) + } + + case OBJECT_ROTATED: + finalAction.data = {ID: state.selectedSprite.ID, angle: state.selectedSprite.angle} + + default: // nop + } + + if (finalAction.data == null) return; + + if (redo) state.redoArray.push(finalAction); + else if (!redo) state.undoArray.push(finalAction); + } +} + +typedef UndoAction = +{ + /** + * The Type of Undo Action to store. + */ + var type:UndoActionType; + + /** + * The added Data of the Action. + */ + var data:Dynamic; +} + +enum abstract UndoActionType(String) from String +{ + /** + * Triggerred when an Object is deleted. + */ + var OBJECT_DELETED = "object_deleted"; + + /** + * Triggerred when an Object is created. + */ + var OBJECT_CREATED = "object_created"; + + /** + * Triggerred when an Object is moved. + */ + var OBJECT_MOVED = "object_moved"; + + /** + * Triggerred when a Character is moved. + */ + var CHARACTER_MOVED = "character_moved"; + + /** + * Triggerred when an Object is rotated. + */ + var OBJECT_ROTATED = "object_rotated"; +} diff --git a/source/funkin/ui/debug/stageeditor/import.hx b/source/funkin/ui/debug/stageeditor/import.hx new file mode 100644 index 000000000..194f501b6 --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/import.hx @@ -0,0 +1,7 @@ +package funkin.ui.debug.stageeditor; + +#if !macro +using funkin.ui.debug.stageeditor.handlers.StageDataHandler; +using funkin.ui.debug.stageeditor.handlers.AssetDataHandler; +using funkin.ui.debug.stageeditor.handlers.UndoRedoHandler; +#end diff --git a/source/funkin/ui/debug/stageeditor/toolboxes/StageEditorCharacterToolbox.hx b/source/funkin/ui/debug/stageeditor/toolboxes/StageEditorCharacterToolbox.hx new file mode 100644 index 000000000..cd01f1fca --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/toolboxes/StageEditorCharacterToolbox.hx @@ -0,0 +1,242 @@ +package funkin.ui.debug.stageeditor.toolboxes; + +import haxe.ui.components.NumberStepper; +import funkin.play.character.BaseCharacter; +import funkin.play.character.BaseCharacter.CharacterType; +import funkin.play.character.CharacterData.CharacterDataParser; +import funkin.util.SortUtil; +import haxe.ui.data.ArrayDataSource; +import haxe.ui.components.DropDown; +import haxe.ui.components.Button; +import haxe.ui.components.Slider; +import haxe.ui.components.Label; +import funkin.ui.debug.stageeditor.handlers.StageDataHandler; +import haxe.ui.containers.menus.Menu; +import haxe.ui.containers.ScrollView; +import haxe.ui.core.Screen; +import flixel.tweens.FlxTween; +import flixel.tweens.FlxEase; +import haxe.ui.containers.Grid; +import funkin.play.character.CharacterData; + +using StringTools; + +@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/toolboxes/character-properties.xml")) +class StageEditorCharacterToolbox extends StageEditorDefaultToolbox +{ + var characterPosXStepper:NumberStepper; + var characterPosYStepper:NumberStepper; + var characterPosReset:Button; + + var characterZIdxStepper:NumberStepper; + var characterZIdxReset:Button; + + var characterCamXStepper:NumberStepper; + var characterCamYStepper:NumberStepper; + var characterCamReset:Button; + + var characterScaleSlider:Slider; + var characterScaleReset:Button; + + var characterTypeButton:Button; + var charMenu:StageEditorCharacterMenu; + + override public function new(state:StageEditorState) + { + super(state); + + // position + characterPosXStepper.onChange = characterPosYStepper.onChange = function(_) { + repositionCharacter(); + state.saved = false; + } + + characterPosReset.onClick = function(_) { + if (!StageEditorState.DEFAULT_POSITIONS.exists(state.selectedChar.characterType)) return; + + var oldPositions = StageEditorState.DEFAULT_POSITIONS[state.selectedChar.characterType]; + characterPosXStepper.pos = oldPositions[0]; + characterPosYStepper.pos = oldPositions[1]; + } + + // zidx + characterZIdxStepper.max = StageEditorState.MAX_Z_INDEX; + characterZIdxStepper.onChange = function(_) { + state.charGroups[state.selectedChar.characterType].zIndex = Std.int(characterZIdxStepper.pos); + state.saved = false; + state.sortAssets(); + } + + characterZIdxReset.onClick = function(_) { + var thingies = [CharacterType.GF, CharacterType.DAD, CharacterType.BF]; + var thingIdxies = thingies.indexOf(state.selectedChar.characterType); + + characterZIdxStepper.pos = (thingIdxies * 100); + } + + // camera + characterCamXStepper.onChange = characterCamYStepper.onChange = function(_) { + state.charCamOffsets[state.selectedChar.characterType] = [characterCamXStepper.pos, characterCamYStepper.pos]; + state.updateMarkerPos(); + state.saved = false; + } + + characterCamReset.onClick = function(_) characterCamXStepper.pos = characterCamYStepper.pos = 0; // lol + + // scale + characterScaleSlider.onChange = function(_) { + state.selectedChar.setScale(state.selectedChar.getBaseScale() * characterScaleSlider.pos); + repositionCharacter(); + state.saved = false; + } + + characterScaleReset.onChange = function(_) characterScaleSlider.pos = 1; + + // character button + characterTypeButton.onClick = function(_) { + charMenu = new StageEditorCharacterMenu(state, this); + Screen.instance.addComponent(charMenu); + } + + refresh(); + } + + override public function refresh() + { + var name = stageEditorState.selectedChar.characterType; + + characterPosXStepper.step = characterPosYStepper.step = stageEditorState.moveStep; + characterCamXStepper.step = characterCamYStepper.step = stageEditorState.moveStep; + + if (characterPosXStepper.pos != stageEditorState.charPos[name][0]) characterPosXStepper.pos = stageEditorState.charPos[name][0]; + if (characterPosYStepper.pos != stageEditorState.charPos[name][1]) characterPosYStepper.pos = stageEditorState.charPos[name][1]; + + if (characterZIdxStepper.pos != stageEditorState.charGroups[stageEditorState.selectedChar.characterType].zIndex) + characterZIdxStepper.pos = stageEditorState.charGroups[stageEditorState.selectedChar.characterType].zIndex; + + if (characterCamXStepper.pos != stageEditorState.charCamOffsets[name][0]) characterCamXStepper.pos = stageEditorState.charCamOffsets[name][0]; + if (characterCamYStepper.pos != stageEditorState.charCamOffsets[name][1]) characterCamYStepper.pos = stageEditorState.charCamOffsets[name][1]; + + if (characterScaleSlider.pos != stageEditorState.selectedChar.scale.x / stageEditorState.selectedChar.getBaseScale()) + characterScaleSlider.pos = stageEditorState.selectedChar.scale.x / stageEditorState.selectedChar.getBaseScale(); + + var prevText = characterTypeButton.text; + + var charData = CharacterDataParser.fetchCharacterData(stageEditorState.selectedChar.characterId); + characterTypeButton.icon = (charData == null ? null : CharacterDataParser.getCharPixelIconAsset(stageEditorState.selectedChar.characterId)); + characterTypeButton.text = (charData == null ? "None" : charData.name.length > 6 ? '${charData.name.substr(0, 6)}.' : '${charData.name}'); + + if (prevText != characterTypeButton.text) + { + Screen.instance.removeComponent(charMenu); + } + } + + public function repositionCharacter() + { + stageEditorState.selectedChar.x = characterPosXStepper.pos - stageEditorState.selectedChar.characterOrigin.x + + stageEditorState.selectedChar.globalOffsets[0]; + stageEditorState.selectedChar.y = characterPosYStepper.pos - stageEditorState.selectedChar.characterOrigin.y + + stageEditorState.selectedChar.globalOffsets[1]; + + stageEditorState.selectedChar.setScale(stageEditorState.selectedChar.getBaseScale() * characterScaleSlider.pos); + + stageEditorState.updateMarkerPos(); + } +} + +@:xml(' + + + + + +') +class StageEditorCharacterMenu extends Menu // copied from chart editor +{ + override public function new(state:StageEditorState, parent:StageEditorCharacterToolbox) + { + super(); + + this.x = Screen.instance.currentMouseX; + this.y = Screen.instance.currentMouseY; + + var charGrid = new Grid(); + charGrid.columns = 5; + charGrid.width = this.width; + charSelectScroll.addComponent(charGrid); + + var charIds = CharacterDataParser.listCharacterIds(); + charIds.sort(SortUtil.alphabetically); + + var defaultText:String = '(choose a character)'; + + for (charIndex => charId in charIds) + { + var charData:CharacterData = CharacterDataParser.fetchCharacterData(charId); + + var charButton = new haxe.ui.components.Button(); + charButton.width = 70; + charButton.height = 70; + charButton.padding = 8; + charButton.iconPosition = "top"; + + if (charId == state.selectedChar.characterId) + { + // Scroll to the character if it is already selected. + charSelectScroll.hscrollPos = Math.floor(charIndex / 5) * 80; + charButton.selected = true; + + defaultText = '${charData.name} [${charId}]'; + } + + var LIMIT = 6; + charButton.icon = CharacterDataParser.getCharPixelIconAsset(charId); + charButton.text = charData.name.length > LIMIT ? '${charData.name.substr(0, LIMIT)}.' : '${charData.name}'; + + charButton.onClick = _ -> { + var type = state.selectedChar.characterType; + if (state.selectedChar.characterId == charId) return; // saves on memory + + var group = state.charGroups[type]; + group.killMembers(); + for (member in group.members) + { + member.kill(); + group.remove(member, true); + member.destroy(); + } + group.clear(); + + // okay i think that was enough cleaning phew you can see how clean this group is now!!! + // anyways new character!!!! + + var newChar = CharacterDataParser.fetchCharacter(charId, true); + newChar.characterType = type; + + newChar.resetCharacter(true); + newChar.flipX = type == CharacterType.BF ? !newChar.getDataFlipX() : newChar.getDataFlipX(); + + state.selectedChar = newChar; + group.add(newChar); + + parent.repositionCharacter(); + }; + + charButton.onMouseOver = _ -> { + charIconName.text = '${charData.name} [${charId}]'; + }; + charButton.onMouseOut = _ -> { + charIconName.text = defaultText; + }; + charGrid.addComponent(charButton); + } + + charIconName.text = defaultText; + + this.alpha = 0; + this.y -= 10; + FlxTween.tween(this, {alpha: 1, y: this.y + 10}, 0.2, {ease: FlxEase.quartOut}); + } +} diff --git a/source/funkin/ui/debug/stageeditor/toolboxes/StageEditorDefaultToolbox.hx b/source/funkin/ui/debug/stageeditor/toolboxes/StageEditorDefaultToolbox.hx new file mode 100644 index 000000000..2e5b3048c --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/toolboxes/StageEditorDefaultToolbox.hx @@ -0,0 +1,44 @@ +package funkin.ui.debug.stageeditor.toolboxes; + +import haxe.ui.containers.dialogs.CollapsibleDialog; +import funkin.audio.FunkinSound; + +@:access(funkin.ui.debug.stageeditor.StageEditorState) +class StageEditorDefaultToolbox extends CollapsibleDialog +{ + var stageEditorState:StageEditorState; + + public var dialogVisible:Bool = false; + + private function new(stageEditorState:StageEditorState) + { + super(); + + this.stageEditorState = stageEditorState; + + closable = false; + modal = true; + destroyOnClose = false; + } + + /** + * Handles the Sound and Visibility + * @param on + */ + public function toggle(on:Bool) + { + if (!dialogVisible && on) FunkinSound.playOnce(Paths.sound('chartingSounds/openWindow')); + else if (dialogVisible && !on) FunkinSound.playOnce(Paths.sound('chartingSounds/exitWindow')); + + if (on) showDialog(false); + else + hide(); + + dialogVisible = on; + } + + /** + * Override to implement this. + */ + public function refresh() {} +} diff --git a/source/funkin/ui/debug/stageeditor/toolboxes/StageEditorObjectToolbox.hx b/source/funkin/ui/debug/stageeditor/toolboxes/StageEditorObjectToolbox.hx new file mode 100644 index 000000000..36dd127ec --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/toolboxes/StageEditorObjectToolbox.hx @@ -0,0 +1,581 @@ +package funkin.ui.debug.stageeditor.toolboxes; + +import haxe.ui.components.HorizontalSlider; +import haxe.ui.components.NumberStepper; +import haxe.ui.components.TextField; +import haxe.ui.components.TextArea; +import haxe.ui.components.Button; +import haxe.ui.components.Image; +import haxe.ui.containers.dialogs.Dialogs.FileDialogTypes; +import haxe.ui.ToolkitAssets; +import haxe.ui.containers.dialogs.Dialogs; +import funkin.ui.debug.stageeditor.handlers.AssetDataHandler; +import flixel.graphics.frames.FlxAtlasFrames; +import haxe.ui.components.DropDown; +import haxe.ui.containers.ListView; +import haxe.ui.components.CheckBox; +import haxe.ui.components.Switch; +import flixel.util.FlxTimer; +import haxe.ui.data.ArrayDataSource; +import haxe.ui.events.ItemEvent; +import haxe.ui.components.ColorPicker; +import flixel.util.FlxColor; +import haxe.ui.util.Color; +import flixel.graphics.frames.FlxFrame; +import flixel.animation.FlxAnimation; +import funkin.util.FileUtil; +import funkin.ui.debug.stageeditor.components.LoadFromUrlDialog; +import openfl.display.BitmapData; + +using StringTools; + +@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/toolboxes/object-properties.xml")) +class StageEditorObjectToolbox extends StageEditorDefaultToolbox +{ + var linkedObject:StageEditorObject = null; + + var objectImagePreview:Image; + var objectLoadImageButton:Button; + var objectLoadInternetButton:Button; + var objectDownloadImageButton:Button; + var objectResetImageButton:Button; + var objectZIdxStepper:NumberStepper; + var objectZIdxReset:Button; + + var objectPosXStepper:NumberStepper; + var objectPosYStepper:NumberStepper; + var objectPosResetButton:Button; + var objectAlphaSlider:HorizontalSlider; + var objectAlphaResetButton:Button; + var objectAngleSlider:HorizontalSlider; + var objectAngleResetButton:Button; + var objectScaleXStepper:NumberStepper; + var objectScaleYStepper:NumberStepper; + var objectScaleResetButton:Button; + var objectSizeXStepper:NumberStepper; + var objectSizeYStepper:NumberStepper; + var objectSizeResetButton:Button; + var objectScrollXSlider:HorizontalSlider; + var objectScrollYSlider:HorizontalSlider; + var objectScrollResetButton:Button; + + var objectFrameText:TextArea; + var objectFrameTextLoad:Button; + var objectFrameTextSparrow:Button; + var objectFrameTextPacker:Button; + var objectFrameImageWidth:NumberStepper; + var objectFrameImageHeight:NumberStepper; + var objectFrameImageSetter:Button; + var objectFrameReset:Button; + + var objectAnimDropdown:DropDown; + var objectAnimName:TextField; + var objectAnimFrameList:ListView; + var objectAnimPrefix:TextField; + var objectAnimFrames:TextField; + var objectAnimLooped:CheckBox; + var objectAnimFlipX:CheckBox; + var objectAnimFlipY:CheckBox; + var objectAnimFramerate:NumberStepper; + var objectAnimOffsetX:NumberStepper; + var objectAnimOffsetY:NumberStepper; + var objectAnimDanceBeat:NumberStepper; + var objectAnimDanceBeatReset:Button; + var objectAnimStart:TextField; + var objectAnimStartReset:Button; + + var objectMiscAntialias:CheckBox; + var objectMiscAntialiasReset:Button; + var objectMiscFlipReset:Button; + var objectMiscBlendDrop:DropDown; + var objectMiscBlendReset:Button; + var objectMiscColor:ColorPicker; + var objectMiscColorReset:Button; + + override public function new(state:StageEditorState) + { + super(state); + + // basic callbacks + objectLoadImageButton.onClick = function(_) { + if (linkedObject == null) return; + + Dialogs.openBinaryFile("Open Image File", FileDialogTypes.IMAGES, function(selectedFile) { + if (selectedFile == null) return; + + objectImagePreview.resource = null; + + ToolkitAssets.instance.imageFromBytes(selectedFile.bytes, function(imageInfo) { + if (imageInfo == null) return; + + objectImagePreview.resource = imageInfo.data; + + linkedObject.frame = imageInfo.data; + + var bit = linkedObject.updateFramePixels(); + var bitToLoad = state.addBitmap(bit); + + linkedObject.loadGraphic(state.bitmaps[bitToLoad]); + linkedObject.updateHitbox(); + + // update size stuff + objectSizeXStepper.pos = linkedObject.width; + objectSizeYStepper.pos = linkedObject.height; + + // remove unused bitmaps + state.removeUnusedBitmaps(); + }); + }); + } + + objectLoadInternetButton.onClick = function(_) { + if (linkedObject == null) return; + + state.createURLDialog(function(bytes:lime.utils.Bytes) { + linkedObject.loadGraphic(BitmapData.fromBytes(bytes)); + linkedObject.updateHitbox(); + refresh(); + }); + } + + objectDownloadImageButton.onClick = function(_) { + if (linkedObject == null) return; + + FileUtil.saveFile(linkedObject.pixels.image.encode(PNG), [FileUtil.FILE_FILTER_PNG], null, null, + linkedObject.name + "-graphic.png"); // i'on need any callbacks + } + + objectZIdxStepper.max = StageEditorState.MAX_Z_INDEX; + objectZIdxStepper.onChange = function(_) { + if (linkedObject != null) + { + linkedObject.zIndex = Std.int(objectZIdxStepper.pos); + state.sortAssets(); + } + } + + // numeric callbacks + objectPosXStepper.onChange = function(_) { + if (linkedObject != null) linkedObject.x = objectPosXStepper.pos; + }; + + objectPosYStepper.onChange = function(_) { + if (linkedObject != null) linkedObject.y = objectPosYStepper.pos; + }; + + objectAlphaSlider.onChange = function(_) { + if (linkedObject != null) linkedObject.alpha = objectAlphaSlider.pos; + }; + + objectAngleSlider.onChange = function(_) { + if (linkedObject != null) linkedObject.angle = objectAngleSlider.pos; + }; + + objectScaleXStepper.onChange = objectScaleYStepper.onChange = function(_) { + if (linkedObject != null) + { + linkedObject.scale.set(objectScaleXStepper.pos, objectScaleYStepper.pos); + linkedObject.updateHitbox(); + objectSizeXStepper.pos = linkedObject.width; + objectSizeYStepper.pos = linkedObject.height; + + linkedObject.playAnim(linkedObject.animation.name); // load offsets + } + }; + + objectSizeXStepper.onChange = objectSizeYStepper.onChange = function(_) { + if (linkedObject != null) + { + linkedObject.setGraphicSize(Std.int(objectSizeXStepper.pos), Std.int(objectSizeYStepper.pos)); + linkedObject.updateHitbox(); + objectScaleXStepper.pos = linkedObject.scale.x; + objectScaleYStepper.pos = linkedObject.scale.y; + + linkedObject.playAnim(linkedObject.animation.name); // load offsets + } + }; + + objectScrollXSlider.onChange = objectScrollYSlider.onChange = function(_) { + if (linkedObject != null) linkedObject.scrollFactor.set(objectScrollXSlider.pos, objectScrollYSlider.pos); + }; + + // frame callbacks + objectFrameTextLoad.onClick = function(_) { + Dialogs.openTextFile("Open Text File", FileDialogTypes.TEXTS, function(selectedFile) { + if (selectedFile.text == null || (!selectedFile.name.endsWith(".xml") && !selectedFile.name.endsWith(".txt"))) return; + + objectFrameText.text = selectedFile.text; + + state.notifyChange("Frame Text Loaded", "The Text File " + selectedFile.name + " has been loaded."); + }); + } + + objectFrameTextSparrow.onClick = function(_) { + if (linkedObject == null || objectFrameText.text == null || objectFrameText.text == "") return; + + try + { + linkedObject.frames = FlxAtlasFrames.fromSparrow(linkedObject.graphic, objectFrameText.text); + } + catch (e) + { + state.notifyChange("Frame Setup Error", e.toString(), true); + return; + } + + // might as well clear animations because frames SUCK + linkedObject.animDatas.clear(); + linkedObject.animation.destroyAnimations(); + linkedObject.updateHitbox(); + refresh(); + + state.notifyChange("Frame Setup Done", "Finished the Sparrow Frame Setup for the Object " + linkedObject.name + "."); + } + + objectFrameTextPacker.onClick = function(_) { + if (linkedObject == null || objectFrameText.text == null || objectFrameText.text == "") return; + + try // crash prevention + { + linkedObject.frames = FlxAtlasFrames.fromSpriteSheetPacker(linkedObject.graphic, objectFrameText.text); + } + catch (e) + { + state.notifyChange("Frame Setup Error", e.toString(), true); + return; + } + + // might as well clear animations because frames SUCK + linkedObject.animDatas.clear(); + linkedObject.animation.destroyAnimations(); + + linkedObject.updateHitbox(); + refresh(); + + state.notifyChange("Frame Setup Done", "Finished the Packer Frame Setup for the Object " + linkedObject.name + "."); + } + + objectFrameImageSetter.onClick = function(_) { + if (linkedObject == null) return; + + linkedObject.loadGraphic(linkedObject.graphic, true, Std.int(objectFrameImageWidth.pos), Std.int(objectFrameImageHeight.pos)); + linkedObject.updateHitbox(); + + // set da names + for (i in 0...linkedObject.frames.frames.length) + { + linkedObject.frames.framesHash.set("Frame" + i, linkedObject.frames.frames[i]); + linkedObject.frames.frames[i].name = "Frame" + i; + } + + // might as well clear animations because frames SUCK + linkedObject.animDatas.clear(); + linkedObject.animation.destroyAnimations(); + refresh(); + + state.notifyChange("Frame Setup Done", "Finished the Image Frame Setup for the Object " + linkedObject.name + "."); + } + + // animation + objectAnimDropdown.onChange = function(_) { + if (linkedObject == null) return; + + if (objectAnimDropdown.selectedIndex == -1) // RESET EVERYTHING INSTANTENEOUSLY + { + objectAnimName.text = ""; + objectAnimLooped.selected = objectAnimFlipX.selected = objectAnimFlipY.selected = false; + objectAnimFramerate.pos = 24; + objectAnimOffsetX.pos = objectAnimOffsetY.pos = 0; + objectAnimFrames.text = ""; + + return; + } + + var animData = linkedObject.animDatas[objectAnimDropdown.selectedItem.text]; + if (animData == null) return; + + objectAnimName.text = objectAnimDropdown.selectedItem.text; + objectAnimPrefix.text = animData.prefix ?? ""; + objectAnimFrames.text = (animData.frameIndices != null && animData.frameIndices.length > 0 ? animData.frameIndices.join(", ") : ""); + + objectAnimLooped.selected = animData.looped ?? false; + objectAnimFlipX.selected = animData.flipX ?? false; + objectAnimFlipY.selected = animData.flipY ?? false; + objectAnimFramerate.pos = animData.frameRate ?? 24; + + objectAnimOffsetX.pos = (animData.offsets != null && animData.offsets.length == 2 ? animData.offsets[0] : 0); + objectAnimOffsetY.pos = (animData.offsets != null && animData.offsets.length == 2 ? animData.offsets[1] : 0); + } + + objectAnimSave.onClick = function(_) { + if (linkedObject == null) return; + + if (objectAnimName.text == null || objectAnimName.text == "") + { + state.notifyChange("Animation Saving Error", "Invalid Animation Name!", true); + return; + } + + if (objectAnimPrefix.text == null || objectAnimPrefix.text == "") + { + state.notifyChange("Animation Saving Error", "Missing Animation Prefix!", true); + return; + } + + if (linkedObject.animation.getNameList().contains(objectAnimName.text)) linkedObject.animation.remove(objectAnimName.text); + + var indices = []; + + if (objectAnimFrames.text != null && objectAnimFrames.text != "") + { + var splitter = objectAnimFrames.text.replace(" ", "").split(","); + + for (num in splitter) + { + indices.push(Std.parseInt(num)); + } + } + + var shouldDoIndices:Bool = (indices.length > 0 && !indices.contains(null)); + + linkedObject.addAnim(objectAnimName.text, objectAnimPrefix.text, [objectAnimOffsetX.pos, objectAnimOffsetY.pos], (shouldDoIndices ? indices : []), + Std.int(objectAnimFramerate.pos), objectAnimLooped.selected, objectAnimFlipX.selected, objectAnimFlipY.selected); + + if (linkedObject.animation.getByName(objectAnimName.text) == null) + { + state.notifyChange("Animation Saving Error", "Invalid Frames!", true); + return; + } + + linkedObject.playAnim(objectAnimName.text); + + state.notifyChange("Animation Saving Done", "Animation " + objectAnimName.text + " has been saved to the Object " + linkedObject.name + "."); + updateAnimList(); + + // stops the animation preview if animation is looped for too long + FlxTimer.wait(StageEditorState.TIME_BEFORE_ANIM_STOP, function() { + if (linkedObject != null && linkedObject.animation.curAnim != null) + linkedObject.animation.stop(); // null check cuz if we stop an anim for a null object the game crashes :[ + }); + } + + objectAnimDelete.onClick = function(_) { + if (linkedObject == null || linkedObject.animation.getNameList().length <= 0 || objectAnimDropdown.selectedIndex < 0) return; + + linkedObject.animation.pause(); + linkedObject.animation.stop(); + linkedObject.animation.curAnim = null; + + var daAnim = linkedObject.animation.getNameList()[objectAnimDropdown.selectedIndex]; + + linkedObject.animation.remove(daAnim); + linkedObject.animDatas.remove(daAnim); + linkedObject.offset.set(); + + state.notifyChange("Animation Deletion Done", + "Animation " + + objectAnimDropdown.selectedItem.text + + " has been removed from the Object " + + linkedObject.name + + "."); + + updateAnimList(); + + objectAnimDropdown.selectedIndex = objectAnimDropdown.dataSource.size - 1; + } + + objectAnimDanceBeat.onChange = function(_) { + if (linkedObject != null) linkedObject.danceEvery = Std.int(objectAnimDanceBeat.pos); + } + + objectAnimStart.onChange = function(_) { + if (linkedObject != null) + { + if (linkedObject.animation.getNameList().contains(objectAnimStart.text)) objectAnimStart.styleString = "color: white"; + else + objectAnimStart.styleString = "color: indianred"; + + linkedObject.startingAnimation = objectAnimStart.text; + } + } + + // misc + objectMiscAntialias.onClick = function(_) { + if (linkedObject != null) linkedObject.antialiasing = objectMiscAntialias.selected; + } + + objectMiscBlendDrop.onChange = function(_) { + if (linkedObject != null) + linkedObject.blend = objectMiscBlendDrop.selectedItem.text == "NONE" ? null : AssetDataHandler.blendFromString(objectMiscBlendDrop.selectedItem.text); + } + + objectMiscColor.onChange = function(_) { + if (linkedObject != null) linkedObject.color = FlxColor.fromRGB(objectMiscColor.currentColor.r, objectMiscColor.currentColor.g, + objectMiscColor.currentColor.b); + } + + // reset button callbacks + objectResetImageButton.onClick = function(_) { + if (linkedObject != null) + { + linkedObject.loadGraphic(AssetDataHandler.getDefaultGraphic()); + linkedObject.updateHitbox(); + refresh(); + + // remove unused bitmaps + state.removeUnusedBitmaps(); + } + } + + objectZIdxReset.onClick = function(_) { + if (linkedObject != null) objectZIdxStepper.pos = 0; // corner cutting because onChange will activate with this + } + + objectPosResetButton.onClick = function(_) { + if (linkedObject != null) + { + linkedObject.screenCenter(); + objectPosXStepper.pos = linkedObject.x; + objectPosYStepper.pos = linkedObject.y; + } + } + + objectAlphaResetButton.onClick = function(_) { + if (linkedObject != null) linkedObject.alpha = objectAlphaSlider.pos = 1; + } + + objectAngleResetButton.onClick = function(_) { + if (linkedObject != null) linkedObject.angle = objectAngleSlider.pos = 0; + } + + objectScaleResetButton.onClick = objectSizeResetButton.onClick = function(_) // the corner cutting goes crazy + { + if (linkedObject != null) + { + linkedObject.scale.set(1, 1); + refresh(); // refreshes like multiple shit + } + } + + objectScrollResetButton.onClick = function(_) { + if (linkedObject != null) linkedObject.scrollFactor.x = linkedObject.scrollFactor.y = objectScrollXSlider.pos = objectScrollYSlider.pos = 1; + } + + objectFrameReset.onClick = function(_) { + if (linkedObject == null) return; + + linkedObject.loadGraphic(linkedObject.pixels); + linkedObject.animDatas.clear(); + linkedObject.animation.destroyAnimations(); + refresh(); + } + + objectMiscAntialiasReset.onClick = function(_) { + if (linkedObject != null) objectMiscAntialias.selected = true; + } + + objectMiscBlendReset.onClick = function(_) { + if (linkedObject != null) objectMiscBlendDrop.selectedItem = "NORMAL"; + } + + objectMiscColorReset.onClick = function(_) { + if (linkedObject != null) objectMiscColor.currentColor = Color.fromString("white"); + } + + objectAnimDanceBeatReset.onClick = function(_) { + if (linkedObject != null) objectAnimDanceBeat.pos = 0; + } + + objectAnimStartReset.onClick = function(_) { + if (linkedObject != null) objectAnimStart.text = ""; + } + + refresh(); + } + + var prevFrames:Array = []; + var prevAnims:Array = []; + + override public function refresh() + { + linkedObject = stageEditorState.selectedSprite; + + objectPosXStepper.step = stageEditorState.moveStep; + objectPosYStepper.step = stageEditorState.moveStep; + objectAngleSlider.step = funkin.save.Save.instance.stageEditorAngleStep; + + if (linkedObject == null) + { + updateFrameList(); + updateAnimList(); + return; + } + + // saving fps + if (objectImagePreview.resource != linkedObject.frame) objectImagePreview.resource = linkedObject.frame; + + if (objectZIdxStepper.pos != linkedObject.zIndex) objectZIdxStepper.pos = linkedObject.zIndex; + if (objectPosXStepper.pos != linkedObject.x) objectPosXStepper.pos = linkedObject.x; + if (objectPosYStepper.pos != linkedObject.y) objectPosYStepper.pos = linkedObject.y; + if (objectAlphaSlider.pos != linkedObject.alpha) objectAlphaSlider.pos = linkedObject.alpha; + if (objectAngleSlider.pos != linkedObject.angle) objectAngleSlider.pos = linkedObject.angle; + if (objectScaleXStepper.pos != linkedObject.scale.x) objectScaleXStepper.pos = linkedObject.scale.x; + if (objectScaleYStepper.pos != linkedObject.scale.y) objectScaleYStepper.pos = linkedObject.scale.y; + if (objectSizeXStepper.pos != linkedObject.width) objectSizeXStepper.pos = linkedObject.width; + if (objectSizeYStepper.pos != linkedObject.height) objectSizeYStepper.pos = linkedObject.height; + if (objectScrollXSlider.pos != linkedObject.scrollFactor.x) objectScrollXSlider.pos = linkedObject.scrollFactor.x; + if (objectScrollYSlider.pos != linkedObject.scrollFactor.y) objectScrollYSlider.pos = linkedObject.scrollFactor.y; + if (objectMiscAntialias.selected != linkedObject.antialiasing) objectMiscAntialias.selected = linkedObject.antialiasing; + + if (objectMiscColor.currentColor != Color.fromString(linkedObject.color.toHexString() ?? "white")) + objectMiscColor.currentColor = Color.fromString(linkedObject.color.toHexString()); + + if (objectAnimDanceBeat.pos != linkedObject.danceEvery) objectAnimDanceBeat.pos = linkedObject.danceEvery; + if (objectAnimStart.text != linkedObject.startingAnimation) objectAnimStart.text = linkedObject.startingAnimation; + + var objBlend = Std.string(linkedObject.blend) ?? "NONE"; + if (objectMiscBlendDrop.selectedItem != objBlend.toUpperCase()) objectMiscBlendDrop.selectedItem = objBlend.toUpperCase(); + + // ough the max + if (objectFrameImageWidth.max != linkedObject.pixels.width) objectFrameImageWidth.max = linkedObject.graphic.width; + if (objectFrameImageHeight.max != linkedObject.pixels.height) objectFrameImageHeight.max = linkedObject.graphic.height; + + // update some anim shit + if (prevFrames != linkedObject.frames.frames.copy()) updateFrameList(); + if (prevAnims != linkedObject.animation.getNameList().copy()) updateAnimList(); + } + + function updateFrameList() + { + prevFrames = []; + objectAnimFrameList.dataSource = new ArrayDataSource(); + + if (linkedObject == null) return; + + for (fname in linkedObject.frames.frames) + { + if (fname != null) objectAnimFrameList.dataSource.add({name: fname.name, tooltip: fname.name}); + + prevFrames.push(fname); + } + } + + function updateAnimList() + { + objectAnimDropdown.dataSource.clear(); + prevAnims = []; + if (linkedObject == null) return; + + for (aname in linkedObject.animation.getNameList()) + { + objectAnimDropdown.dataSource.add({text: aname}); + prevAnims.push(aname); + } + + if (linkedObject.animation.getNameList().contains(objectAnimStart.text)) objectAnimStart.styleString = "color: white"; + else + objectAnimStart.styleString = "color: indianred"; + + linkedObject.startingAnimation = objectAnimStart.text; + } +} diff --git a/source/funkin/ui/debug/stageeditor/toolboxes/StageEditorStageToolbox.hx b/source/funkin/ui/debug/stageeditor/toolboxes/StageEditorStageToolbox.hx new file mode 100644 index 000000000..5d7770c0c --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/toolboxes/StageEditorStageToolbox.hx @@ -0,0 +1,60 @@ +package funkin.ui.debug.stageeditor.toolboxes; + +import haxe.ui.components.NumberStepper; +import haxe.ui.components.TextField; +import haxe.ui.components.DropDown; +import funkin.util.SortUtil; + +@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/toolboxes/stage-settings.xml")) +class StageEditorStageToolbox extends StageEditorDefaultToolbox +{ + var stageNameText:TextField; + var stageZoomStepper:NumberStepper; + var stageLibraryDrop:DropDown; + + override public function new(state:StageEditorState) + { + super(state); + + stageNameText.onChange = function(_) { + state.stageName = stageNameText.text; + state.saved = false; + } + + stageZoomStepper.onChange = function(_) { + state.stageZoom = stageZoomStepper.pos; + state.updateMarkerPos(); + state.saved = false; + } + + final EXCLUDE_LIBS = ["art", "default", "vlc", "videos", "songs"]; + var allLibs = []; + + @:privateAccess + { + for (lib => idk in lime.utils.Assets.libraryPaths) + { + if (!EXCLUDE_LIBS.contains(lib)) allLibs.push(lib); + } + } + allLibs.sort(SortUtil.alphabetically); // this system is VERY stupid, it relies on the possibility that the future libraries will be named week(end)[x] + + for (lib in allLibs) + { + stageLibraryDrop.dataSource.add({text: lib}); + } + + stageLibraryDrop.onChange = function(_) { + state.stageFolder = stageLibraryDrop.selectedItem.text; + } + + refresh(); + } + + override public function refresh() + { + stageNameText.text = stageEditorState.stageName; + stageZoomStepper.pos = stageEditorState.stageZoom; + stageLibraryDrop.selectedItem = stageEditorState.stageFolder; + } +} diff --git a/source/funkin/ui/transition/LoadingState.hx b/source/funkin/ui/transition/LoadingState.hx index a4fee0f8d..26bc9e304 100644 --- a/source/funkin/ui/transition/LoadingState.hx +++ b/source/funkin/ui/transition/LoadingState.hx @@ -86,7 +86,7 @@ class LoadingState extends MusicBeatSubState } checkLibrary('shared'); - checkLibrary(PlayStatePlaylist.campaignId); + checkLibrary(stageDirectory); checkLibrary('tutorial'); var fadeTime:Float = 0.5; @@ -204,6 +204,8 @@ class LoadingState extends MusicBeatSubState return Paths.inst(PlayState.instance.currentSong.id); } + static var stageDirectory:String = "shared"; + /** * Starts the transition to a new `PlayState` to start a new song. * First switches to the `LoadingState` if assets need to be loaded. @@ -213,7 +215,13 @@ class LoadingState extends MusicBeatSubState */ public static function loadPlayState(params:PlayStateParams, shouldStopMusic = false, asSubState = false, ?onConstruct:PlayState->Void):Void { - Paths.setCurrentLevel(PlayStatePlaylist.campaignId); + var daChart = params.targetSong.getDifficulty(params.targetDifficulty ?? Constants.DEFAULT_DIFFICULTY, + params.targetVariation ?? Constants.DEFAULT_VARIATION); + + var daStage = funkin.data.stage.StageRegistry.instance.fetchEntry(daChart.stage); + stageDirectory = daStage?._data?.directory ?? "shared"; + Paths.setCurrentLevel(stageDirectory); + var playStateCtor:() -> PlayState = function() { return new PlayState(params); }; diff --git a/source/funkin/util/FileUtil.hx b/source/funkin/util/FileUtil.hx index 00a0a14b7..5b053bb08 100644 --- a/source/funkin/util/FileUtil.hx +++ b/source/funkin/util/FileUtil.hx @@ -22,6 +22,7 @@ class FileUtil public static final FILE_FILTER_JSON:FileFilter = new FileFilter("JSON Data File (.json)", "*.json"); public static final FILE_FILTER_ZIP:FileFilter = new FileFilter("ZIP Archive (.zip)", "*.zip"); public static final FILE_FILTER_PNG:FileFilter = new FileFilter("PNG Image (.png)", "*.png"); + public static final FILE_FILTER_FNFS:FileFilter = new FileFilter("Friday Night Funkin' Stage (.fnfs)", "*.fnfs"); public static final FILE_EXTENSION_INFO_FNFC:FileDialogExtensionInfo = { @@ -39,6 +40,12 @@ class FileUtil label: 'PNG Image', }; + public static final FILE_EXTENSION_INFO_FNFS:FileDialogExtensionInfo = + { + extension: 'fnfs', + label: 'Friday Night Funkin\' Stage', + }; + /** * Browses for a single file, then calls `onSelect(fileInfo)` when a file is selected. * Powered by HaxeUI, so it works on all platforms. From 8cd7f2a5b100cedf3d1f7c8af927f5076ffb4832 Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Mon, 30 Sep 2024 18:42:43 -0400 Subject: [PATCH 13/44] stage editor ui files --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index 7efad31cf..4abd6cc06 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 7efad31cf80a42600375bf08b397786df5c1037c +Subproject commit 4abd6cc06e56c6d56440fa858262932db118250c From 623985a2cc4e427c6fb37917fc163b5560bf81db Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Mon, 30 Sep 2024 19:03:11 -0400 Subject: [PATCH 14/44] mac keyboard shortcuts on stage editor --- source/funkin/ui/debug/stageeditor/StageEditorState.hx | 7 ++++--- .../ui/debug/stageeditor/handlers/UndoRedoHandler.hx | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/source/funkin/ui/debug/stageeditor/StageEditorState.hx b/source/funkin/ui/debug/stageeditor/StageEditorState.hx index e6d2e1f8a..54f574f2d 100644 --- a/source/funkin/ui/debug/stageeditor/StageEditorState.hx +++ b/source/funkin/ui/debug/stageeditor/StageEditorState.hx @@ -292,7 +292,7 @@ class StageEditorState extends UIState { WindowManager.instance.reset(); instance = this; - FlxG.sound.music.stop(); + FlxG.sound.music?.stop(); WindowUtil.setWindowTitle("Friday Night Funkin\' Stage Editor"); AssetDataHandler.init(this); @@ -503,7 +503,7 @@ class StageEditorState extends UIState return super.beatHit(); } - override public function update(elapsed:Float) + override public function update(elapsed:Float):Void { updateBGSize(); conductorInUse.update(); @@ -563,7 +563,8 @@ class StageEditorState extends UIState // key shortcuts and inputs if (allowInput) { - if (FlxG.keys.pressed.CONTROL) + // "WINDOWS" key code is the same keycode as COMMAND on mac + if (FlxG.keys.pressed.CONTROL || FlxG.keys.pressed.WINDOWS) { if (FlxG.keys.justPressed.Z) onMenuItemClick("undo"); if (FlxG.keys.justPressed.Y) onMenuItemClick("redo"); diff --git a/source/funkin/ui/debug/stageeditor/handlers/UndoRedoHandler.hx b/source/funkin/ui/debug/stageeditor/handlers/UndoRedoHandler.hx index 947bb5cf3..2fc38aab3 100644 --- a/source/funkin/ui/debug/stageeditor/handlers/UndoRedoHandler.hx +++ b/source/funkin/ui/debug/stageeditor/handlers/UndoRedoHandler.hx @@ -6,7 +6,7 @@ import funkin.ui.debug.stageeditor.StageEditorState.StageEditorDialogType; class UndoRedoHandler { - public static function performLastAction(state:StageEditorState, redo:Bool = false) + public static function performLastAction(state:StageEditorState, redo:Bool = false):Void { if (state == null || (state.undoArray.length <= 0 && !redo) || (state.redoArray.length <= 0 && redo)) return; var actionToDo = redo ? state.redoArray.pop() : state.undoArray.pop(); From 8890cbf2703f0d84500aada211839a522f96db05 Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Mon, 30 Sep 2024 19:15:03 -0400 Subject: [PATCH 15/44] pixel perfect mouse check rather than hitbox --- .../ui/debug/stageeditor/StageEditorObject.hx | 4 ++-- .../ui/debug/stageeditor/StageEditorState.hx | 22 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/source/funkin/ui/debug/stageeditor/StageEditorObject.hx b/source/funkin/ui/debug/stageeditor/StageEditorObject.hx index 6fa7db6be..e892159bd 100644 --- a/source/funkin/ui/debug/stageeditor/StageEditorObject.hx +++ b/source/funkin/ui/debug/stageeditor/StageEditorObject.hx @@ -50,7 +50,7 @@ class StageEditorObject extends FunkinSprite return value; } - public function playAnim(name:String, restart:Bool = false, reversed:Bool = false) + public function playAnim(name:String, restart:Bool = false, reversed:Bool = false):Void { if (!animation.getNameList().contains(name)) return; @@ -71,7 +71,7 @@ class StageEditorObject extends FunkinSprite */ var _danced:Bool = true; - public function dance(restart:Bool = false) + public function dance(restart:Bool = false):Void { if (isDebugged) return; diff --git a/source/funkin/ui/debug/stageeditor/StageEditorState.hx b/source/funkin/ui/debug/stageeditor/StageEditorState.hx index 54f574f2d..92a118c6d 100644 --- a/source/funkin/ui/debug/stageeditor/StageEditorState.hx +++ b/source/funkin/ui/debug/stageeditor/StageEditorState.hx @@ -181,7 +181,7 @@ class StageEditorState extends UIState public var saved(default, set):Bool = true; public var currentFile(default, set):String = ""; - function set_saved(value:Bool) + function set_saved(value:Bool):Bool { saved = value; @@ -216,7 +216,7 @@ class StageEditorState extends UIState return value; } - function set_currentFile(value:String) + function set_currentFile(value:String):String { currentFile = value; @@ -278,7 +278,7 @@ class StageEditorState extends UIState var showChars(default, set):Bool = true; - function set_showChars(value:Bool) + function set_showChars(value:Bool):Bool { this.showChars = value; @@ -288,7 +288,7 @@ class StageEditorState extends UIState return value; } - override public function create() + override public function create():Void { WindowManager.instance.reset(); instance = this; @@ -619,7 +619,7 @@ class StageEditorState extends UIState { spr.active = spr.isOnScreen(); - if (FlxG.mouse.overlaps(spr)) + if (spr.pixelsOverlapPoint(FlxG.mouse.getWorldPosition())) { if (spr.visible && !FlxG.keys.pressed.SHIFT) nameTxt.text = spr.name; @@ -923,7 +923,7 @@ class StageEditorState extends UIState WindowUtil.setWindowTitle('Friday Night Funkin\''); } - function updateBGColors() + function updateBGColors():Void { var colArray = Save.instance.stageEditorTheme == StageEditorTheme.Dark ? DARK_MODE_COLORS : LIGHT_MODE_COLORS; @@ -937,14 +937,14 @@ class StageEditorState extends UIState members.insert(index, bg); } - function updateBGSize() + function updateBGSize():Void { bg.scale.set(1 / FlxG.camera.zoom, 1 / FlxG.camera.zoom); bg.updateHitbox(); bg.screenCenter(); } - function checkOverlaps(spr:FlxSprite) + function checkOverlaps(spr:FlxSprite):Bool { if (FlxG.mouse.overlaps(spr) /*spr.overlapsPoint(FlxG.mouse.getWorldPosition(spr.camera), true, spr.camera) */ && Screen.instance != null @@ -957,7 +957,7 @@ class StageEditorState extends UIState var sprDependant:Array = []; - function addUI() + function addUI():Void { menubarItemNewStage.onClick = function(_) onMenuItemClick("new stage"); menubarItemOpenStage.onClick = function(_) onMenuItemClick("open stage"); @@ -1065,7 +1065,7 @@ class StageEditorState extends UIState reloadRecentFiles(); } - function reloadRecentFiles() + function reloadRecentFiles():Void { for (a in menubarItemOpenRecent.childComponents) menubarItemOpenRecent.removeComponent(a); @@ -1114,7 +1114,7 @@ class StageEditorState extends UIState public var aboutDialog:AboutDialog; public var loadUrlDialog:LoadFromUrlDialog; - public function onMenuItemClick(item:String) + public function onMenuItemClick(item:String):Void { switch (item.toLowerCase()) { From 097dbf5bb4346d431d8ca9f0ec4bc5b5e6f4523f Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Mon, 30 Sep 2024 19:40:02 -0400 Subject: [PATCH 16/44] feat: Added InverseDotsShader that emulates flash selections --- .../funkin/graphics/shaders/InverseDotsShader.hx | 14 ++++++++++++++ source/funkin/ui/title/TitleState.hx | 4 ++-- 2 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 source/funkin/graphics/shaders/InverseDotsShader.hx diff --git a/source/funkin/graphics/shaders/InverseDotsShader.hx b/source/funkin/graphics/shaders/InverseDotsShader.hx new file mode 100644 index 000000000..fa43d3cbd --- /dev/null +++ b/source/funkin/graphics/shaders/InverseDotsShader.hx @@ -0,0 +1,14 @@ +package funkin.graphics.shaders; + +import flixel.addons.display.FlxRuntimeShader; + +/** + * Create a little dotting effect. + */ +class InverseDotsShader extends FlxRuntimeShader +{ + public function new() + { + super(Assets.getText(Paths.frag("InverseDots"))); + } +} diff --git a/source/funkin/ui/title/TitleState.hx b/source/funkin/ui/title/TitleState.hx index 839992eea..f5277c7b9 100644 --- a/source/funkin/ui/title/TitleState.hx +++ b/source/funkin/ui/title/TitleState.hx @@ -148,9 +148,9 @@ class TitleState extends MusicBeatState // maskShader.frameUV = gfDance.frame.uv; // gfDance.shader = maskShader; - gfDance.shader = swagShader.shader; + // gfDance.shader = swagShader.shader; - // gfDance.shader = new TitleOutline(); + gfDance.shader = new TitleOutline(); add(logoBl); From 1189c226ee65a1ef86d08eef1a9792ce32a881c4 Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Mon, 30 Sep 2024 21:48:15 -0400 Subject: [PATCH 17/44] some selection shader stuff in progress --- .../graphics/shaders/InverseDotsShader.hx | 11 +++++++- .../ui/debug/stageeditor/StageEditorObject.hx | 8 +++++- .../ui/debug/stageeditor/StageEditorState.hx | 25 +++++++++++-------- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/source/funkin/graphics/shaders/InverseDotsShader.hx b/source/funkin/graphics/shaders/InverseDotsShader.hx index fa43d3cbd..d77808cf1 100644 --- a/source/funkin/graphics/shaders/InverseDotsShader.hx +++ b/source/funkin/graphics/shaders/InverseDotsShader.hx @@ -7,8 +7,17 @@ import flixel.addons.display.FlxRuntimeShader; */ class InverseDotsShader extends FlxRuntimeShader { - public function new() + public var amount:Float; + + public function new(amount:Float = 1.0) { super(Assets.getText(Paths.frag("InverseDots"))); + setAmount(amount); + } + + public function setAmount(value:Float):Void + { + this.amount = value; + this.setFloat("_amount", amount); } } diff --git a/source/funkin/ui/debug/stageeditor/StageEditorObject.hx b/source/funkin/ui/debug/stageeditor/StageEditorObject.hx index e892159bd..9580ad559 100644 --- a/source/funkin/ui/debug/stageeditor/StageEditorObject.hx +++ b/source/funkin/ui/debug/stageeditor/StageEditorObject.hx @@ -3,6 +3,7 @@ package funkin.ui.debug.stageeditor; import funkin.data.animation.AnimationData; import funkin.graphics.FunkinSprite; import funkin.modding.events.ScriptEvent; +import funkin.graphics.shaders.InverseDotsShader; /** * Contains all the Logic needed for Stage Editor. Only for Stage Editor, as in the gameplay StageProps and Boppers will be used. @@ -14,6 +15,8 @@ class StageEditorObject extends FunkinSprite */ public var name:String = "Unnamed"; + public var selectedShader:InverseDotsShader; + /** * What animation to play upon starting. */ @@ -24,6 +27,9 @@ class StageEditorObject extends FunkinSprite override public function new() { super(); + + selectedShader = new InverseDotsShader(0); + shader = selectedShader; } /** @@ -31,7 +37,7 @@ class StageEditorObject extends FunkinSprite */ public var isDebugged(default, set):Bool = true; - function set_isDebugged(value:Bool) + function set_isDebugged(value:Bool):Bool { this.isDebugged = value; diff --git a/source/funkin/ui/debug/stageeditor/StageEditorState.hx b/source/funkin/ui/debug/stageeditor/StageEditorState.hx index 92a118c6d..98190498f 100644 --- a/source/funkin/ui/debug/stageeditor/StageEditorState.hx +++ b/source/funkin/ui/debug/stageeditor/StageEditorState.hx @@ -143,10 +143,12 @@ class StageEditorState extends UIState if (selectedSprite != null) { - spriteMarker.setGraphicSize(Std.int(selectedSprite.width), Std.int(selectedSprite.height)); - spriteMarker.updateHitbox(); + // spriteMarker.setGraphicSize(Std.int(selectedSprite.width), Std.int(selectedSprite.height)); + // spriteMarker.updateHitbox(); } + selectedSprite?.selectedShader.setAmount(1); + return selectedSprite; } @@ -370,10 +372,10 @@ class StageEditorState extends UIState add(charGroups[CharacterType.BF]); // ui - spriteMarker = new FlxSprite().makeGraphic(1, 1, FlxColor.CYAN); - spriteMarker.alpha = 0.3; - spriteMarker.zIndex = MAX_Z_INDEX + CHARACTER_COLORS.length + 3; // PLEASE - add(spriteMarker); + // spriteMarker = new FlxSprite().makeGraphic(1, 1, FlxColor.CYAN); + // spriteMarker.alpha = 0.3; + // spriteMarker.zIndex = MAX_Z_INDEX + CHARACTER_COLORS.length + 3; // PLEASE + // add(spriteMarker); camFields = new FlxTypedGroup(); camFields.visible = false; @@ -522,7 +524,7 @@ class StageEditorState extends UIState for (char in getCharacters()) char.alpha = 1; - spriteMarker.visible = camMarker.visible = false; + // spriteMarker.visible = camMarker.visible = false; findObjDialog.hideDialog(DialogButton.CANCEL); // cam @@ -625,6 +627,7 @@ class StageEditorState extends UIState if (FlxG.mouse.justPressed && allowInput && spr.visible && !FlxG.keys.pressed.SHIFT && !isCursorOverHaxeUI) { + selectedSprite.selectedShader.setAmount(0); selectedSprite = spr; updateDialog(StageEditorDialogType.OBJECT); } @@ -734,12 +737,12 @@ class StageEditorState extends UIState nameTxt.x = FlxG.mouse.getScreenPosition(camHUD).x; nameTxt.y = FlxG.mouse.getScreenPosition(camHUD).y - nameTxt.height; - spriteMarker.visible = (moveMode == "assets" && selectedSprite != null); + // spriteMarker.visible = (moveMode == "assets" && selectedSprite != null); camMarker.visible = moveMode == "chars"; - if (selectedSprite != null) spriteMarker.setPosition(selectedSprite.x, selectedSprite.y); + // if (selectedSprite != null) spriteMarker.setPosition(selectedSprite.x, selectedSprite.y); - for (item in sprDependant) - item.disabled = !spriteMarker.visible; + // for (item in sprDependant) + // item.disabled = !spriteMarker.visible; menubarItemPaste.disabled = copiedSprite == null; menubarItemFindObj.disabled = !(moveMode == "assets"); From 19abcb50097c7dc8f54216badc4315a7a31663a2 Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Thu, 10 Oct 2024 13:58:11 -0400 Subject: [PATCH 18/44] bump version to 0.5.2 --- project.hxp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.hxp b/project.hxp index 5c04a2493..adecc2032 100644 --- a/project.hxp +++ b/project.hxp @@ -25,7 +25,7 @@ class Project extends HXProject { * REMEMBER TO CHANGE THIS WHEN THE GAME UPDATES! * You only have to change it here, the rest of the game will query this value. */ - static final VERSION:String = "0.5.1"; + static final VERSION:String = "0.5.2"; /** * The game's name. Used as the default window title. From 9c8ba2c0531df70be75820b9535d949531be406f Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Thu, 10 Oct 2024 16:29:05 -0400 Subject: [PATCH 19/44] shader fix for title screen doh --- source/funkin/ui/title/TitleState.hx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/funkin/ui/title/TitleState.hx b/source/funkin/ui/title/TitleState.hx index f5277c7b9..839992eea 100644 --- a/source/funkin/ui/title/TitleState.hx +++ b/source/funkin/ui/title/TitleState.hx @@ -148,9 +148,9 @@ class TitleState extends MusicBeatState // maskShader.frameUV = gfDance.frame.uv; // gfDance.shader = maskShader; - // gfDance.shader = swagShader.shader; + gfDance.shader = swagShader.shader; - gfDance.shader = new TitleOutline(); + // gfDance.shader = new TitleOutline(); add(logoBl); From 91b4544f7ebc51485e3e28c3d716ba6ee69ad885 Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Fri, 11 Oct 2024 18:51:47 -0400 Subject: [PATCH 20/44] docs(changelog): create a git cliff template for easier changelog stuff --- cliff.toml | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 cliff.toml diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 000000000..94c69f6d5 --- /dev/null +++ b/cliff.toml @@ -0,0 +1,88 @@ +# git-cliff ~ configuration file +# https://git-cliff.org/docs/configuration + +[remote.github] +owner = "FunkinCrew" +repo = "Funkin" + +# To bypass "you have reached your rate limit!", you can input a github token either as an environment variable +# or alongside the `git cliff` command like so `git cliff --github-token TOKEN_HERE` +# Personally I like to use the github cli `gh` tool to get a token +# `git cliff --github-token $(gh auth token)` + +[changelog] +# template for the changelog body +# https://keats.github.io/tera/docs/#introduction +body = """ +{%- macro remote_url() -%} + https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }} +{%- endmacro -%} + +{% if version -%} + ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} +{% else -%} + ## [Unreleased] +{% endif -%} + +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | upper_first }} + {%- for commit in commits %} + - {{ commit.message | split(pat="\n") | first | upper_first | trim }}\ + {% if commit.remote.username %} by @{{ commit.remote.username }} + {%- elif commit.author.name %} by {{ commit.author.name }} + {%- endif -%} + {% if commit.remote.pr_number %} in \ + [#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}) \ + {%- endif -%} + {% if commit.links | length != 0 %}\ + [#{{ commit.links }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}) \ + {%- endif -%} + {% endfor %} +{% endfor %} + +{%- if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %} + ## New Contributors +{%- endif -%} + +{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %} + * @{{ contributor.username }} made their first contribution + {%- if contributor.pr_number %} in \ + [#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \ + {%- endif %} +{%- endfor %}\n +""" + +# remove the leading and trailing whitespace from the templates +trim = true + +[git] +# parse the commits based on https://www.conventionalcommits.org +conventional_commits = true +# filter out the commits that are not conventional +filter_unconventional = true +# regex for preprocessing the commit messages +commit_preprocessors = [ + # remove issue numbers from commits + { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" }, +] +# regex for parsing and grouping commits +commit_parsers = [ + { message = "^[a|A]dd", group = "Added" }, + { message = "^[s|S]upport", group = "Added" }, + { message = "^[r|R]emove", group = "Removed" }, + { message = "^.*: add", group = "Added" }, + { message = "^.*: support", group = "Added" }, + { message = "^.*: remove", group = "Removed" }, + { message = "^.*: delete", group = "Removed" }, + { message = "^test", group = "Fixed" }, + { message = "^fix", group = "Fixed" }, + { message = "^.*: fix", group = "Fixed" }, + { message = "^.*", group = "Changed" }, +] +# filter out the commits that are not matched by commit parsers +filter_commits = false +# sort the tags topologically +topo_order = false +# sort the commits inside sections by oldest/newest order +sort_commits = "newest" + From a42240e6a595d33034f2c887bf38a350d1fa0f15 Mon Sep 17 00:00:00 2001 From: Abnormal <86753001+AbnormalPoof@users.noreply.github.com> Date: Fri, 11 Oct 2024 23:24:41 +0000 Subject: [PATCH 21/44] docs: Delete Modding.md since we have a separate modding documentation (#3651) --- Modding.md | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 Modding.md diff --git a/Modding.md b/Modding.md deleted file mode 100644 index 4b52fd8c5..000000000 --- a/Modding.md +++ /dev/null @@ -1,13 +0,0 @@ -# RIGHT NOW THE MODS FOLDER DOES NOT WORK ENTIRELY JUST YET!!! -## THIS IS WORK IN PROGRESS!!! - -# QUICK AND DIRTY MOD GUIDE - -With the 0.2.6 update, I added a bit of a slightly nicer mod support backend. - -It's POLYMOD, which is made by Lars Doucet: https://github.com/larsiusprime/polymod - -You may have noticed that there's a new folder in the assets. MODS. Within it you will see 2 files. modList.txt, and a folder called introMod. -modList.txt will load any folder into the game. Put the folder you want to load into a new line in modList.txt, and reboot the game. - -Now you may be wondering, what do I put in the folder? Well later down it'll get a bit more complicated, especially as I'll make the IN-GAME mod loader nicer. \ No newline at end of file From b4c48893bccbdf356735e61ab3ac01e1b2368702 Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Fri, 11 Oct 2024 22:01:19 -0400 Subject: [PATCH 22/44] more small little changelog template stuff --- cliff.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cliff.toml b/cliff.toml index 94c69f6d5..1a7d3d049 100644 --- a/cliff.toml +++ b/cliff.toml @@ -27,7 +27,8 @@ body = """ {% for group, commits in commits | group_by(attribute="group") %} ### {{ group | upper_first }} {%- for commit in commits %} - - {{ commit.message | split(pat="\n") | first | upper_first | trim }}\ + - {% if commit.scope %}({{commit.scope}}) {% endif %}{{ commit.message | split(pat="\n") | first | upper_first | trim }} \ + ([{{ commit.id | truncate(length=7, end="") }}]({{self::remote_url()}}/commit/{{ commit.id }})) -\ {% if commit.remote.username %} by @{{ commit.remote.username }} {%- elif commit.author.name %} by {{ commit.author.name }} {%- endif -%} @@ -67,6 +68,7 @@ commit_preprocessors = [ ] # regex for parsing and grouping commits commit_parsers = [ + { message = "^docs", group = "Changed", scope="docs" }, { message = "^[a|A]dd", group = "Added" }, { message = "^[s|S]upport", group = "Added" }, { message = "^[r|R]emove", group = "Removed" }, From 37465454522de5b24431c47a7c20367810d9d525 Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Fri, 11 Oct 2024 22:06:07 -0400 Subject: [PATCH 23/44] update changelog for 0.5.2 --- CHANGELOG.md | 41 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d727b223..44aa4a486 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,15 +4,50 @@ All notable changes will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.5.2] - 2024-10-?? +## [0.5.2] - 2024-10-11 + +### Changed +- (docs) Delete Modding.md since we have a separate modding documentation ([a42240e](https://github.com/FunkinCrew/Funkin/commit/a42240e6a595d33034f2c887bf38a350d1fa0f15)) - by @AbnormalPoof in [#3651](https://github.com/FunkinCrew/Funkin/pull/3651) +- (docs) Create a git cliff template for easier changelog stuff ([91b4544](https://github.com/FunkinCrew/Funkin/commit/91b4544f7ebc51485e3e28c3d716ba6ee69ad885)) - by @ninjamuffin99 in [#3652](https://github.com/FunkinCrew/Funkin/pull/3652) +- Added InverseDotsShader that emulates flash selections ([097dbf5](https://github.com/FunkinCrew/Funkin/commit/097dbf5bb4346d431d8ca9f0ec4bc5b5e6f4523f)) - by @ninjamuffin99 +- (docs) Add additional `variation` input parameter to `Save.hasBeatenSong()` to allow usage of the function by inputting a variation id ([4fa9a0d](https://github.com/FunkinCrew/Funkin/commit/4fa9a0daaa67e0977460b147bd1f74a118e3e2a5)) - by @ninjamuffin99 +- (docs) Added modding docs link in readme ([4b54118](https://github.com/FunkinCrew/Funkin/commit/4b54118731e26118111e06558ae4853c577fe4bb)) - by @Cartridge-Man +- Fix some misspellings and grammar in code documentation ([2175bea](https://github.com/FunkinCrew/Funkin/commit/2175beaa651e009332202985be4b7eb4ed36e5a4)) - by @Hundrec +- (docs) Improvements to Github Issues templates ([399869c](https://github.com/FunkinCrew/Funkin/commit/399869cdccc9c5ac27cecfbcdc33c3d7eb4b348c)) - by @Hundrec in [#3458](https://github.com/FunkinCrew/Funkin/pull/3458) +- Fix some misspellings and grammar in code documentation ([6df80ba](https://github.com/FunkinCrew/Funkin/commit/6df80ba69d0e24269f40471f83462cab7d5e13cf)) - by @Hundrec in [#3477](https://github.com/FunkinCrew/Funkin/pull/3477) +- (docs) Improvements to Github Issues templates ([67f7b63](https://github.com/FunkinCrew/Funkin/commit/67f7b638fb76840b868cbfa70a1c6063577984c5)) - by @Hundrec + ### Fixed -- Fixed an issue where exiting the Chart Editor would sometimes crash the game. -- Fixed an issue where holding down a direction key then selecting a character would select the locked character and crash the game (community fix by ACrazyTown) +- Disable flickering when changing FPS in the options menu ([b2647fe](https://github.com/FunkinCrew/Funkin/commit/b2647fe09f5281ce7074b26d47bc1524764168ee)) - by @lemz1 in [#3629](https://github.com/FunkinCrew/Funkin/pull/3629) +- Anti alias / smooth the volume sound tray ([e66290c](https://github.com/FunkinCrew/Funkin/commit/e66290c55f7141402223644f06ec8a69edeee089)) - by @Kn1ghtNight in [#2853](https://github.com/FunkinCrew/Funkin/pull/2853) +- Don't restart the FreeplayState song preview when changing the difficulty within the same variation ([903b3fc](https://github.com/FunkinCrew/Funkin/commit/903b3fc59905a70802618a1cd67407722ea956ed)) - by @JustKolosaki in [#3587](https://github.com/FunkinCrew/Funkin/pull/3587) +- Exiting the chart editor doesn't crash the game anymore ([f52472a](https://github.com/FunkinCrew/Funkin/commit/f52472a4767388b22cfbab0f5f7860f6e6762856)) - by @EliteMasterEric +- Character Select cursor moves properly at lower framerates ([ab5bda3](https://github.com/FunkinCrew/Funkin/commit/ab5bda3ee573a6e03595ec6941e6de38df851889)) - by @ninjamuffin99 in [#3507](https://github.com/FunkinCrew/Funkin/pull/3507) +- Stopped allowing F1 to create more than one help dialog window in the Charting Editor ([777978f](https://github.com/FunkinCrew/Funkin/commit/777978f5a544e1b7c89b47dcc365f734eb6d0df1)) - by @amyspark-ng +- Main menu music doesn't cut out when switching states anymore. ([711e0a6](https://github.com/FunkinCrew/Funkin/commit/711e0a6b7547eb04113e9318dab900f01ad576a5)) - by @EliteMasterEric in [#3530](https://github.com/FunkinCrew/Funkin/pull/3530) +- The dialog now shows up on the animation debugger view ([1fde59f](https://github.com/FunkinCrew/Funkin/commit/1fde59f999eac94eb10fc22094885de2f5310705)) - by @EliteMasterEric +- Center preloader 'fnf' and 'dsp' text so it doesn't clip anymore ([165ad60](https://github.com/FunkinCrew/Funkin/commit/165ad6015539a295e9eefdaef291c312e9566b26)) - by @Burgerballs in [#3567](https://github.com/FunkinCrew/Funkin/pull/3567) +- `Song.getFirstValidVariation()` now properly takes into account multiple variations/difficulty input ([d2e2987](https://github.com/FunkinCrew/Funkin/commit/d2e29879fe2acc6febfe0f335f655b741d630c34)) - by @ninjamuffin99 in [#3506](https://github.com/FunkinCrew/Funkin/pull/3506) +- (freeplay) Proper variation / difficulty loading for Freeplay Menu ([c0314c8](https://github.com/FunkinCrew/Funkin/commit/c0314c85ecd5116641aff3de8e9153f7fe48e79c)) - by @ninjamuffin99 +- Picos songs properly load on freeplay ([1d2bd61](https://github.com/FunkinCrew/Funkin/commit/1d2bd61119e5f418df7f11d7ef2a0fdedee17d3d)) - by @ninjamuffin99 +- (debug) No more fullscreening when typing "F" in the flixel debugger console ([29b6763](https://github.com/FunkinCrew/Funkin/commit/29b6763290df05d42039806f3d142740568c80f0)) - by @ninjamuffin99 +- Fix the user song offsets being applied incorrectly ([410cfe9](https://github.com/FunkinCrew/Funkin/commit/410cfe972d6df9de4d4d128375cf8380c4f06d92)) - by @JustKolosaki +- Fix crash in LatencyState when exiting / cleaning up state data ([39b1a42](https://github.com/FunkinCrew/Funkin/commit/39b1a42cfeafe2b7be8b66e2fe529e853d9ae197)) - by @lemz1 +- Add additional classes to Polymod Blacklist ([b0b73c8](https://github.com/FunkinCrew/Funkin/commit/b0b73c83994f33118c6a69550da9ec8ec1c07adc)) - by @EliteMasterEric +- Stop allowing inputs after selecting a character ([dbf66ac](https://github.com/FunkinCrew/Funkin/commit/dbf66ac250137262866d75f7c1387645b35d88d0)) - by @ACrazyTown - Fixed an issue where the player and girlfriend would disappear or overlap themselves in Character Select (community fix by gamerbross) - Fixed an issue where the game would show the wrong girlfriend in Character Select (community fix by gamerbross) - Fixed an issue where the cursor wouldn't update properly in Character Select (community fix by gamerbross) - Fixed an issue where the player would display double after entering character select or when spamming buttons (community fix by gamerbross) +## New Contributors for 0.5.2 +* @Kn1ghtNight made their first contribution in [#2853](https://github.com/FunkinCrew/Funkin/pull/2853) +* @DaWaterMalone made their first contribution +* @amyspark-ng made their first contribution +* @Cartridge-Man made their first contribution +* @afreetoplaynoob made their first contribution + + ## [0.5.1] - 2024-09-30 ### Added - Readded the Merch button to the main menu. From 55c2dabe84e6c8eb2b02e5b38209abe3c48e548c Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Fri, 11 Oct 2024 23:43:57 -0400 Subject: [PATCH 24/44] Bump the Stage data version to indicate that new values were added. --- source/funkin/data/stage/CHANGELOG.md | 6 ++++++ source/funkin/data/stage/StageRegistry.hx | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/source/funkin/data/stage/CHANGELOG.md b/source/funkin/data/stage/CHANGELOG.md index bf9d750cc..f83ce608d 100644 --- a/source/funkin/data/stage/CHANGELOG.md +++ b/source/funkin/data/stage/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.3] +### Added +- Added the `color` attribute on stage props to tint them. +- Added the `blend` attribute on stage props to apply blend modes. +- Added the `angle` attribute on stage props to apply a rotation to them. + ## [1.0.2] ### Added - Added the ability to specify `flipX` and `flipY` on stage props to horizontally or vertically flip, respectively. diff --git a/source/funkin/data/stage/StageRegistry.hx b/source/funkin/data/stage/StageRegistry.hx index e11166bdd..0c546d0ab 100644 --- a/source/funkin/data/stage/StageRegistry.hx +++ b/source/funkin/data/stage/StageRegistry.hx @@ -11,9 +11,9 @@ class StageRegistry extends BaseRegistry * Handle breaking changes by incrementing this value * and adding migration to the `migrateStageData()` function. */ - public static final STAGE_DATA_VERSION:thx.semver.Version = "1.0.2"; + public static final STAGE_DATA_VERSION:thx.semver.Version = "1.0.3"; - public static final STAGE_DATA_VERSION_RULE:thx.semver.VersionRule = ">=1.0.0 <=1.0.2"; + public static final STAGE_DATA_VERSION_RULE:thx.semver.VersionRule = ">=1.0.0 <=1.0.3"; public static var instance(get, never):StageRegistry; static var _instance:Null = null; From 4effd709ebadbe3898db504838a124ee77f47038 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Fri, 11 Oct 2024 23:48:19 -0400 Subject: [PATCH 25/44] Updated Save data version to indicate new values. --- source/funkin/save/Save.hx | 2 +- source/funkin/save/changelog.md | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx index 8403a1a77..0a788a90f 100644 --- a/source/funkin/save/Save.hx +++ b/source/funkin/save/Save.hx @@ -18,7 +18,7 @@ import thx.semver.Version; @:nullSafety class Save { - public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.4"; + public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.6"; public static final SAVE_DATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x"; // We load this version's saves from a new save path, to maintain SOME level of backwards compatibility. diff --git a/source/funkin/save/changelog.md b/source/funkin/save/changelog.md index 41d6e68ae..72fb7fc03 100644 --- a/source/funkin/save/changelog.md +++ b/source/funkin/save/changelog.md @@ -5,11 +5,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [2.0.4] - 2024-09-12 -Note to self: Only update to 2.1.0 when migration is needed. +## [2.0.6] - 2024-10-11 ### Added -- `unlocks.charactersSeen:Array` to `Save` -- `unlocks.oldChar:Bool` to `Save` +- `optionsStageEditor` to `Save` for storing user preferences for the stage editor. ## [2.0.5] - 2024-05-21 ### Fixed @@ -17,6 +15,8 @@ Note to self: Only update to 2.1.0 when migration is needed. ## [2.0.4] - 2024-05-21 ### Added +- `unlocks.charactersSeen:Array` to `Save` +- `unlocks.oldChar:Bool` to `Save` - `favoriteSongs:Array` to `Save` ## [2.0.3] - 2024-01-09 From ce85ee20a7d309b4898cc1b97b0b194f1cb0e80e Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Fri, 11 Oct 2024 23:52:07 -0400 Subject: [PATCH 26/44] Only display the Stage Editor in builds that enable it, and enable it by default (this re-enables the Stage Editor keybind). --- project.hxp | 2 +- source/funkin/ui/debug/DebugMenuSubState.hx | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/project.hxp b/project.hxp index adecc2032..38c5c4933 100644 --- a/project.hxp +++ b/project.hxp @@ -460,7 +460,6 @@ class Project extends HXProject { // Should be false unless explicitly requested. GITHUB_BUILD.apply(this, false); - FEATURE_STAGE_EDITOR.apply(this, false); FEATURE_NEWGROUNDS.apply(this, false); FEATURE_GHOST_TAPPING.apply(this, false); @@ -471,6 +470,7 @@ class Project extends HXProject { 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)); diff --git a/source/funkin/ui/debug/DebugMenuSubState.hx b/source/funkin/ui/debug/DebugMenuSubState.hx index cc6a2426e..45652095b 100644 --- a/source/funkin/ui/debug/DebugMenuSubState.hx +++ b/source/funkin/ui/debug/DebugMenuSubState.hx @@ -55,12 +55,14 @@ class DebugMenuSubState extends MusicBeatSubState // Create each menu item. // Call onMenuChange when the first item is created to move the camera . #if FEATURE_CHART_EDITOR - onMenuChange(createItem("CHART EDITOR", openChartEditor)); + createItem("CHART EDITOR", openChartEditor); + #end + createItem("ANIMATION EDITOR", openAnimationEditor); + #if FEATURE_STAGE_EDITOR + createItem("STAGE EDITOR", openStageEditor); #end // createItem("Input Offset Testing", openInputOffsetTesting); - createItem("CHARACTER SELECT", openCharSelect, true); - createItem("ANIMATION EDITOR", openAnimationEditor); - createItem("STAGE EDITOR", openStageEditor); + // createItem("CHARACTER SELECT", openCharSelect, true); // createItem("TEST STICKERS", testStickers); #if sys createItem("OPEN CRASH LOG FOLDER", openLogFolder); From 4dbb0baa173c5ad804d653d4626e24ea26d1a025 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Fri, 18 Oct 2024 16:00:06 -0400 Subject: [PATCH 27/44] Enable log messages on all builds by default after we got too many complaints. --- project.hxp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/project.hxp b/project.hxp index 38c5c4933..4e8dae4b8 100644 --- a/project.hxp +++ b/project.hxp @@ -474,7 +474,10 @@ class Project extends HXProject { // Should be true on debug builds or if GITHUB_BUILD is enabled. FEATURE_DEBUG_FUNCTIONS.apply(this, isDebug() || GITHUB_BUILD.isEnabled(this)); - FEATURE_LOG_TRACE.apply(this, isDebug()); + + // 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); // Should default to true on workspace builds and false on release builds. REDIRECT_ASSETS_FOLDER.apply(this, isDebug() && isDesktop()); From 24ad7f4a39abda85d4215afd2c2304cee4ceece6 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Fri, 18 Oct 2024 16:33:22 -0400 Subject: [PATCH 28/44] Remove unused custom type resolver. --- source/funkin/save/Save.hx | 5 +-- source/funkin/util/SerializerUtil.hx | 48 ---------------------------- 2 files changed, 1 insertion(+), 52 deletions(-) diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx index 0a788a90f..1d723d086 100644 --- a/source/funkin/save/Save.hx +++ b/source/funkin/save/Save.hx @@ -961,10 +961,7 @@ class Save */ static function loadFromSlot(slot:Int):Save { - trace("[SAVE] Loading save from slot " + slot + "..."); - - // Prevent crashes if the save data is corrupted. - SerializerUtil.initSerializer(); + trace('[SAVE] Loading save from slot $slot...'); FlxG.save.bind('$SAVE_NAME${slot}', SAVE_PATH); diff --git a/source/funkin/util/SerializerUtil.hx b/source/funkin/util/SerializerUtil.hx index fa602cc73..c87d3f6c0 100644 --- a/source/funkin/util/SerializerUtil.hx +++ b/source/funkin/util/SerializerUtil.hx @@ -63,31 +63,6 @@ class SerializerUtil } } - public static function initSerializer():Void - { - haxe.Unserializer.DEFAULT_RESOLVER = new FunkinTypeResolver(); - } - - /** - * Serialize a Haxe object using the built-in Serializer. - * @param input The object to serialize - * @return The serialized object as a string - */ - public static function fromHaxeObject(input:Dynamic):String - { - return haxe.Serializer.run(input); - } - - /** - * Convert a serialized Haxe object back into a Haxe object. - * @param input The serialized object as a string - * @return The deserialized object - */ - public static function toHaxeObject(input:String):Dynamic - { - return haxe.Unserializer.run(input); - } - /** * Customize how certain types are serialized when converting to JSON. */ @@ -115,26 +90,3 @@ class SerializerUtil return result; } } - -class FunkinTypeResolver -{ - public function new() - { - // Blank constructor. - } - - public function resolveClass(name:String):Class - { - if (name == 'Dynamic') - { - FlxG.log.warn('Found invalid class type in save data, indicates partial save corruption.'); - return null; - } - return Type.resolveClass(name); - }; - - public function resolveEnum(name:String):Enum - { - return Type.resolveEnum(name); - }; -} From dff4135fc941b2d991f2ffd66cf1db2fc78938ec Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Fri, 18 Oct 2024 17:11:09 -0400 Subject: [PATCH 29/44] Update save data format and error handling. --- hmm.json | 2 +- source/funkin/save/Save.hx | 83 +++++++++++++------ source/funkin/save/changelog.md | 8 ++ .../funkin/save/migrator/SaveDataMigrator.hx | 18 ++++ .../funkin/save/migrator/SaveData_v2_0_0.hx | 26 ++++++ .../ui/debug/charting/ChartEditorState.hx | 17 ++-- .../ui/debug/stageeditor/StageEditorState.hx | 6 +- 7 files changed, 122 insertions(+), 38 deletions(-) create mode 100644 source/funkin/save/migrator/SaveData_v2_0_0.hx diff --git a/hmm.json b/hmm.json index 8ff572b30..50879416b 100644 --- a/hmm.json +++ b/hmm.json @@ -11,7 +11,7 @@ "name": "flixel", "type": "git", "dir": null, - "ref": "f2b090d6c608471e730b051c8ee22b8b378964b1", + "ref": "ffa691cb2d2d81de35b900a4411e4062ac84ab58", "url": "https://github.com/FunkinCrew/flixel" }, { diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx index 1d723d086..52cf18b7f 100644 --- a/source/funkin/save/Save.hx +++ b/source/funkin/save/Save.hx @@ -1,25 +1,23 @@ package funkin.save; import flixel.util.FlxSave; -import funkin.util.FileUtil; import funkin.input.Controls.Device; import funkin.play.scoring.Scoring; import funkin.play.scoring.Scoring.ScoringRank; import funkin.save.migrator.RawSaveData_v1_0_0; import funkin.save.migrator.SaveDataMigrator; -import funkin.save.migrator.SaveDataMigrator; import funkin.ui.debug.charting.ChartEditorState.ChartEditorLiveInputStyle; import funkin.ui.debug.charting.ChartEditorState.ChartEditorTheme; import funkin.ui.debug.stageeditor.StageEditorState.StageEditorTheme; +import funkin.util.FileUtil; import funkin.util.SerializerUtil; import thx.semver.Version; -import thx.semver.Version; @:nullSafety class Save { - public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.6"; - public static final SAVE_DATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x"; + public static final SAVE_DATA_VERSION:thx.semver.Version = "2.1.0"; + public static final SAVE_DATA_VERSION_RULE:thx.semver.VersionRule = ">=2.1.0 <2.2.0"; // We load this version's saves from a new save path, to maintain SOME level of backwards compatibility. static final SAVE_PATH:String = 'FunkinCrew'; @@ -965,32 +963,57 @@ class Save FlxG.save.bind('$SAVE_NAME${slot}', SAVE_PATH); - if (FlxG.save.isEmpty()) + switch (FlxG.save.status) { - trace('[SAVE] Save data is empty, checking for legacy save data...'); - var legacySaveData = fetchLegacySaveData(); - if (legacySaveData != null) - { - trace('[SAVE] Found legacy save data, converting...'); - var gameSave = SaveDataMigrator.migrateFromLegacy(legacySaveData); + case EMPTY: + trace('[SAVE] Save data in slot ${slot} is empty, checking for legacy save data...'); + var legacySaveData = fetchLegacySaveData(); + if (legacySaveData != null) + { + trace('[SAVE] Found legacy save data, converting...'); + var gameSave = SaveDataMigrator.migrateFromLegacy(legacySaveData); + FlxG.save.mergeData(gameSave.data, true); + return gameSave; + } + else + { + trace('[SAVE] No legacy save data found.'); + var gameSave = new Save(); + FlxG.save.mergeData(gameSave.data, true); + return gameSave; + } + case ERROR(_): + return handleSaveDataError(slot); + case BOUND(_, _): + trace('[SAVE] Loaded existing save data in slot ${slot}.'); + var gameSave = SaveDataMigrator.migrate(FlxG.save.data); FlxG.save.mergeData(gameSave.data, true); + return gameSave; - } - else - { - trace('[SAVE] No legacy save data found.'); - var gameSave = new Save(); - FlxG.save.mergeData(gameSave.data, true); - return gameSave; - } + } + } + + /** + * Call this when there is an error loading the save data in slot X. + */ + static function handleSaveDataError(slot:Int):Save + { + var msg = 'There was an error loading your save data in slot ${slot}.'; + msg += '\nPlease report this issue to the developers.'; + lime.app.Application.current.window.alert(msg, "Save Data Failure"); + + // Don't touch that slot anymore. + // Instead, load the next available slot. + + var nextSlot = slot + 1; + + if (nextSlot < 1000) + { + return loadFromSlot(nextSlot); } else { - trace('[SAVE] Found existing save data.'); - var gameSave = SaveDataMigrator.migrate(FlxG.save.data); - FlxG.save.mergeData(gameSave.data, true); - - return gameSave; + throw "End of save data slots. Can't load any more."; } } @@ -1055,7 +1078,15 @@ class Save { var targetSaveData = new FlxSave(); targetSaveData.bind('$SAVE_NAME${slot}', SAVE_PATH); - return !targetSaveData.isEmpty(); + switch (targetSaveData.status) + { + case EMPTY: + return false; + case ERROR(_): + return false; + case BOUND(_, _): + return true; + } } /** diff --git a/source/funkin/save/changelog.md b/source/funkin/save/changelog.md index 72fb7fc03..aa98d3096 100644 --- a/source/funkin/save/changelog.md +++ b/source/funkin/save/changelog.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.1.0] - 2024-10-18 +This version introduces changes to save data loading in order to improve compatibility with older versions. +### Changed +- `optionsStageEditor.theme` converted from an Enum to a String to fix save data compatibility issues. + - In the future, Enum values should not be used in order to prevent incompatibilities caused by introducing new types to the save data that older versions cannot parse. +- `optionsChartEditor.theme` converted from an Enum to a String to fix save data compatibility issues. +- `optionsChartEditor.chartEditorLiveInputStyle` converted from an Enum to a String to fix save data compatibility issues. + ## [2.0.6] - 2024-10-11 ### Added - `optionsStageEditor` to `Save` for storing user preferences for the stage editor. diff --git a/source/funkin/save/migrator/SaveDataMigrator.hx b/source/funkin/save/migrator/SaveDataMigrator.hx index 7a929322a..228669f66 100644 --- a/source/funkin/save/migrator/SaveDataMigrator.hx +++ b/source/funkin/save/migrator/SaveDataMigrator.hx @@ -32,6 +32,10 @@ class SaveDataMigrator var save:Save = new Save(saveDataWithDefaults); return save; } + else if (VersionUtil.validateVersion(version, "2.0.x")) + { + return migrate_v2_0_0(inputData); + } else { var message:String = 'Error migrating save data, expected ${Save.SAVE_DATA_VERSION}.'; @@ -43,6 +47,20 @@ class SaveDataMigrator } } + static function migrate_v2_0_0(inputData:Dynamic):Save + { + // Import the structured data. + var saveDataWithDefaults:RawSaveData = cast thx.Objects.deepCombine(Save.getDefault(), inputData); + + // Reset these values to valid ones. + saveDataWithDefaults.optionsChartEditor.chartEditorLiveInputStyle = funkin.ui.debug.charting.ChartEditorLiveInputStyle.None; + saveDataWithDefaults.optionsChartEditor.theme = funkin.ui.debug.charting.ChartEditorTheme.Light; + saveDataWithDefaults.optionsStageEditor.theme = funkin.ui.debug.stageeditor.StageEditorTheme.Light; + + var save:Save = new Save(saveDataWithDefaults); + return save; + } + /** * Migrate from 1.x to the latest version. */ diff --git a/source/funkin/save/migrator/SaveData_v2_0_0.hx b/source/funkin/save/migrator/SaveData_v2_0_0.hx new file mode 100644 index 000000000..5369d8737 --- /dev/null +++ b/source/funkin/save/migrator/SaveData_v2_0_0.hx @@ -0,0 +1,26 @@ +package funkin.save.migrator; + +// Internal enums used to ensure old save data can be parsed by the default Haxe unserializer. +// In the future, only primitive types and abstract enums should be used in save data! + +@:native("funkin.ui.debug.stageeditor.StageEditorTheme") +enum StageEditorTheme +{ + Light; + Dark; +} + +@:native("funkin.ui.debug.charting.ChartEditorTheme") +enum ChartEditorTheme +{ + Light; + Dark; +} + +@:native("funkin.ui.debug.charting.ChartEditorLiveInputStyle") +enum ChartEditorLiveInputStyle +{ + None; + NumberKeys; + WASDKeys; +} diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 3fb63a4f1..c11f13342 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -5654,7 +5654,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState function handleHelpKeybinds():Void { // F1 = Open Help - if (FlxG.keys.justPressed.F1 && !isHaxeUIDialogOpen) { + if (FlxG.keys.justPressed.F1 && !isHaxeUIDialogOpen) + { this.openUserGuideDialog(); } } @@ -6543,22 +6544,22 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState /** * Available input modes for the chart editor state. Numbers/arrows/WASD available for other keybinds. */ -enum ChartEditorLiveInputStyle +enum abstract ChartEditorLiveInputStyle(String) { /** * No hotkeys to place notes at the playbar. */ - None; + var None; /** * 1/2/3/4 to place notes on opponent's side, 5/6/7/8 to place notes on player's side. */ - NumberKeys; + var NumberKeys; /** * WASD to place notes on opponent's side, Arrow keys to place notes on player's side. */ - WASDKeys; + var WASDKeys; } typedef ChartEditorParams = @@ -6577,15 +6578,15 @@ typedef ChartEditorParams = /** * Available themes for the chart editor state. */ -enum ChartEditorTheme +enum abstract ChartEditorTheme(String) { /** * The default theme for the chart editor. */ - Light; + var Light; /** * A theme which introduces darker colors. */ - Dark; + var Dark; } diff --git a/source/funkin/ui/debug/stageeditor/StageEditorState.hx b/source/funkin/ui/debug/stageeditor/StageEditorState.hx index 98190498f..a536a09ee 100644 --- a/source/funkin/ui/debug/stageeditor/StageEditorState.hx +++ b/source/funkin/ui/debug/stageeditor/StageEditorState.hx @@ -1458,17 +1458,17 @@ class StageEditorState extends UIState /** * Available themes for the stage editor state. */ -enum StageEditorTheme +enum abstract StageEditorTheme(String) { /** * The default theme for the stage editor. */ - Light; + var Light; /** * A theme which introduces stage colors. */ - Dark; + var Dark; } enum StageEditorDialogType From 5819a43522c640d7e271b1d61ff2902b54176fbc Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Fri, 18 Oct 2024 17:22:36 -0400 Subject: [PATCH 30/44] Fix a typo. --- source/funkin/save/migrator/SaveDataMigrator.hx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/source/funkin/save/migrator/SaveDataMigrator.hx b/source/funkin/save/migrator/SaveDataMigrator.hx index 228669f66..3dc012fce 100644 --- a/source/funkin/save/migrator/SaveDataMigrator.hx +++ b/source/funkin/save/migrator/SaveDataMigrator.hx @@ -53,9 +53,9 @@ class SaveDataMigrator var saveDataWithDefaults:RawSaveData = cast thx.Objects.deepCombine(Save.getDefault(), inputData); // Reset these values to valid ones. - saveDataWithDefaults.optionsChartEditor.chartEditorLiveInputStyle = funkin.ui.debug.charting.ChartEditorLiveInputStyle.None; - saveDataWithDefaults.optionsChartEditor.theme = funkin.ui.debug.charting.ChartEditorTheme.Light; - saveDataWithDefaults.optionsStageEditor.theme = funkin.ui.debug.stageeditor.StageEditorTheme.Light; + saveDataWithDefaults.optionsChartEditor.chartEditorLiveInputStyle = funkin.ui.debug.charting.ChartEditorState.ChartEditorLiveInputStyle.None; + saveDataWithDefaults.optionsChartEditor.theme = funkin.ui.debug.charting.ChartEditorState.ChartEditorTheme.Light; + saveDataWithDefaults.optionsStageEditor.theme = funkin.ui.debug.stageeditor.StageEditorState.StageEditorTheme.Light; var save:Save = new Save(saveDataWithDefaults); return save; From f46f57e23d83053010fc165a55349367f60d2155 Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Fri, 18 Oct 2024 19:51:06 -0400 Subject: [PATCH 31/44] proj hxp version bump --- project.hxp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.hxp b/project.hxp index 4e8dae4b8..2ec8ba445 100644 --- a/project.hxp +++ b/project.hxp @@ -25,7 +25,7 @@ class Project extends HXProject { * REMEMBER TO CHANGE THIS WHEN THE GAME UPDATES! * You only have to change it here, the rest of the game will query this value. */ - static final VERSION:String = "0.5.2"; + static final VERSION:String = "0.5.3"; /** * The game's name. Used as the default window title. From a0cefba9fda9195e279eee451935e68190f0af45 Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Tue, 22 Oct 2024 12:14:14 -0400 Subject: [PATCH 32/44] changelog yoink --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44aa4a486..c550557c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.5.3] - 2024-10-18 + +### Changed +- `optionsStageEditor.theme` converted from an Enum to a String to fix save data compatibility issues. +- In the future, Enum values should not be used in order to prevent incompatibilities caused by introducing new types to the save data that older versions cannot parse. +- `optionsChartEditor.theme` converted from an Enum to a String to fix save data compatibility issues. +- `optionsChartEditor.chartEditorLiveInputStyle` converted from an Enum to a String to fix save data compatibility issues. + ## [0.5.2] - 2024-10-11 ### Changed From 8cf346c3b0f5eb2813cf00b4913e17590c90090b Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 22 Oct 2024 20:36:10 -0400 Subject: [PATCH 33/44] Update CHANGELOG.md --- CHANGELOG.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c550557c9..42c30c00e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.5.3] - 2024-10-18 - -### Changed -- `optionsStageEditor.theme` converted from an Enum to a String to fix save data compatibility issues. -- In the future, Enum values should not be used in order to prevent incompatibilities caused by introducing new types to the save data that older versions cannot parse. -- `optionsChartEditor.theme` converted from an Enum to a String to fix save data compatibility issues. -- `optionsChartEditor.chartEditorLiveInputStyle` converted from an Enum to a String to fix save data compatibility issues. +This patch resolves a critical issue which could cause user's save data to become corrupted. It is recommended that users switch to this version immediately and avoid using version 0.5.2. +### Fixed +- Fixed a critical issue in which the Stage Editor theme value could not be parsed by older versions of the game, resulting in all save data being destroyed. + - Added a check which prevents save data from being loaded if it is corrupted rather than overriding it. +- `optionsStageEditor.theme` in the save data converted from an Enum to a String to fix save data compatibility issues. + - In the future, Enum values should not be used in order to prevent incompatibilities caused by introducing new types to the save data that older versions cannot parse. +- `optionsChartEditor.theme` in the save data converted from an Enum to a String to fix save data compatibility issues. +- `optionsChartEditor.chartEditorLiveInputStyle` in the save data converted from an Enum to a String to fix save data compatibility issues. +- Fixed an issue where some publicly distributed release builds of the game were not ## [0.5.2] - 2024-10-11 From 62de2a0345d929cf8932a8ea8a0e9e0385c009ec Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 23 Oct 2024 13:40:35 -0400 Subject: [PATCH 34/44] ...sandwiches. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42c30c00e..2fb4f02d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ This patch resolves a critical issue which could cause user's save data to becom - In the future, Enum values should not be used in order to prevent incompatibilities caused by introducing new types to the save data that older versions cannot parse. - `optionsChartEditor.theme` in the save data converted from an Enum to a String to fix save data compatibility issues. - `optionsChartEditor.chartEditorLiveInputStyle` in the save data converted from an Enum to a String to fix save data compatibility issues. -- Fixed an issue where some publicly distributed release builds of the game were not +- Fixed an issue where some publicly distributed builds of the game were debug builds instead of release builds. ## [0.5.2] - 2024-10-11 From 7159cad2c17e61193c0cd6e79364e91583870248 Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Sat, 26 Oct 2024 15:51:38 -0400 Subject: [PATCH 35/44] style: remove a buncho unused imports from PlayState.hx --- source/funkin/play/PlayState.hx | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index dbacf3fa9..9f32e7245 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -1,17 +1,13 @@ package funkin.play; -import flixel.addons.display.FlxPieDial; import flixel.addons.transition.FlxTransitionableState; import flixel.addons.transition.Transition; import flixel.FlxCamera; import flixel.FlxObject; -import flixel.FlxState; import flixel.FlxSubState; import flixel.math.FlxMath; import flixel.math.FlxPoint; -import flixel.math.FlxRect; import flixel.text.FlxText; -import flixel.tweens.FlxEase; import flixel.tweens.FlxTween; import flixel.ui.FlxBar; import flixel.util.FlxColor; @@ -22,7 +18,6 @@ import funkin.audio.FunkinSound; import funkin.audio.VoicesGroup; import funkin.data.dialogue.conversation.ConversationRegistry; import funkin.data.event.SongEventRegistry; -import funkin.data.notestyle.NoteStyleData; import funkin.data.notestyle.NoteStyleRegistry; import funkin.data.song.SongData.SongCharacterData; import funkin.data.song.SongData.SongEventData; @@ -45,7 +40,6 @@ import funkin.play.cutscene.VanillaCutscenes; import funkin.play.cutscene.VideoCutscene; import funkin.play.notes.NoteDirection; import funkin.play.notes.notekind.NoteKindManager; -import funkin.play.notes.NoteSplash; import funkin.play.notes.NoteSprite; import funkin.play.notes.notestyle.NoteStyle; import funkin.play.notes.Strumline; @@ -58,15 +52,9 @@ import funkin.ui.debug.charting.ChartEditorState; import funkin.ui.debug.stage.StageOffsetSubState; import funkin.ui.mainmenu.MainMenuState; import funkin.ui.MusicBeatSubState; -import funkin.ui.options.PreferencesMenu; -import funkin.ui.story.StoryMenuState; import funkin.ui.transition.LoadingState; import funkin.util.SerializerUtil; import haxe.Int64; -import lime.ui.Haptic; -import openfl.display.BitmapData; -import openfl.geom.Rectangle; -import openfl.Lib; #if FEATURE_DISCORD_RPC import funkin.api.discord.DiscordClient; #end From 144ba003773cd15f07680898fc380c70293ac1ef Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Sat, 26 Oct 2024 15:53:22 -0400 Subject: [PATCH 36/44] refactor: remove commented out code from draw() in PlayState.hx --- source/funkin/play/PlayState.hx | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 9f32e7245..c662f9918 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -748,24 +748,6 @@ class PlayState extends MusicBeatSubState refresh(); } - public override function draw():Void - { - // if (FlxG.renderBlit) - // { - // camGame.fill(BACKGROUND_COLOR); - // } - // else if (FlxG.renderTile) - // { - // FlxG.log.warn("PlayState background not displayed properly on tile renderer!"); - // } - // else - // { - // FlxG.log.warn("PlayState background not displayed properly, unknown renderer!"); - // } - - super.draw(); - } - function assertChartExists():Bool { // Returns null if the song failed to load or doesn't have the selected difficulty. From 20d90169845f1e50f849e39f4c5f818359756c78 Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Wed, 23 Oct 2024 19:19:06 -0400 Subject: [PATCH 37/44] feat: Added smoother scrolling when using the Chart Editor smoother drag / movement in chart editor playbarHead movement fixie --- .../ui/debug/charting/ChartEditorState.hx | 109 +++++------------- 1 file changed, 31 insertions(+), 78 deletions(-) diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index c11f13342..035884693 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -94,6 +94,7 @@ import funkin.ui.mainmenu.MainMenuState; import funkin.ui.transition.LoadingState; import funkin.util.Constants; import funkin.util.FileUtil; +import funkin.util.MathUtil; import funkin.util.logging.CrashHandler; import funkin.util.SortUtil; import funkin.util.WindowUtil; @@ -244,7 +245,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState /** * Duration, in seconds, for the scroll easing animation. */ - public static final SCROLL_EASE_DURATION:Float = 0.2; + public static final SCROLL_EASE_DURATION:Float = 0.4; // Other @@ -773,9 +774,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState /** * The current process that is lerping the scroll position. - * Used to cancel the previous lerp if the user scrolls again. */ - var currentScrollEase:Null; + var currentScrollEase:Null; /** * The position where the user middle clicked to place a scroll anchor. @@ -2707,6 +2707,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState playbarHeadLayout.playbarHead.width = FlxG.width; playbarHeadLayout.playbarHead.height = 10; playbarHeadLayout.playbarHead.styleString = 'padding-left: 0px; padding-right: 0px; border-left: 0px; border-right: 0px;'; + playbarHeadLayout.playbarHead.min = 0; playbarHeadLayout.playbarHead.onDragStart = function(_:DragEvent) { playbarHeadDragging = true; @@ -2723,13 +2724,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState } } - playbarHeadLayout.playbarHead.onDrag = function(_:DragEvent) { + playbarHeadLayout.playbarHead.onDrag = function(d:DragEvent) { if (playbarHeadDragging) { - // Set the song position to where the playhead was moved to. - scrollPositionInPixels = (songLengthInPixels) * playbarHeadLayout.playbarHead.value / 100; // Update the conductor and audio tracks to match. - moveSongToScrollPosition(); + currentScrollEase = d.value; } } @@ -2740,6 +2739,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState if (playbarHeadDraggingWasPlaying) { playbarHeadDraggingWasPlaying = false; + // Disabled code to resume song playback on drag. // startAudioPlayback(); } @@ -3417,10 +3417,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState audioInstTrack.time = -Conductor.instance.instrumentalOffset; } } + + if (!audioInstTrack.isPlaying && currentScrollEase != scrollPositionInPixels) easeSongToScrollPosition(currentScrollEase); } if (audioInstTrack != null && audioInstTrack.isPlaying) { + currentScrollEase = scrollPositionInPixels; + if (FlxG.keys.pressed.ALT) { // If middle mouse panning during song playback, we move ONLY the playhead, without scrolling. Neat! @@ -4057,27 +4061,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState shouldPause = true; } - if (Math.abs(scrollAmount) > GRID_SIZE * 8) - { - shouldEase = true; - } + shouldEase = true; + if (shouldPause) stopAudioPlayback(); // Resync the conductor and audio tracks. - if (scrollAmount != 0 || playheadAmount != 0) - { - this.playheadPositionInPixels += playheadAmount; - if (shouldEase) - { - easeSongToScrollPosition(this.scrollPositionInPixels + scrollAmount); - } - else - { - // Apply the scroll amount. - this.scrollPositionInPixels += scrollAmount; - moveSongToScrollPosition(); - } - } - if (shouldPause) stopAudioPlayback(); + if (playheadAmount != 0) this.playheadPositionInPixels += playheadAmount; + + if (scrollAmount != 0) currentScrollEase += scrollAmount; } /** @@ -4333,15 +4323,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { // Scroll up. var diff:Float = MENU_BAR_HEIGHT - FlxG.mouse.viewY; - scrollPositionInPixels -= diff * 0.5; // Too fast! - moveSongToScrollPosition(); + currentScrollEase -= diff * 0.5; // Too fast! } else if (FlxG.mouse.viewY > (playbarHeadLayout?.y ?? 0.0)) { // Scroll down. var diff:Float = FlxG.mouse.viewY - (playbarHeadLayout?.y ?? 0.0); - scrollPositionInPixels += diff * 0.5; // Too fast! - moveSongToScrollPosition(); + currentScrollEase += (diff * 0.5); // Too fast! } // Render the selection box. @@ -4480,8 +4468,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState var clickedPosInPixels:Float = FlxMath.remapToRange(FlxG.mouse.viewY, (notePreview?.y ?? 0.0), (notePreview?.y ?? 0.0) + (notePreview?.height ?? 0.0), 0, songLengthInPixels); - scrollPositionInPixels = clickedPosInPixels; - moveSongToScrollPosition(); + currentScrollEase = clickedPosInPixels; } else if (scrollAnchorScreenPos != null) { @@ -4540,15 +4527,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { // Scroll up. var diff:Float = MENU_BAR_HEIGHT - FlxG.mouse.viewY; - scrollPositionInPixels -= diff * 0.5; // Too fast! - moveSongToScrollPosition(); + currentScrollEase -= (diff * 0.5); } else if (FlxG.mouse.viewY > (playbarHeadLayout?.y ?? 0.0)) { // Scroll down. var diff:Float = FlxG.mouse.viewY - (playbarHeadLayout?.y ?? 0.0); - scrollPositionInPixels += diff * 0.5; // Too fast! - moveSongToScrollPosition(); + currentScrollEase += (diff * 0.5); } // Calculate distance between the position dragged to and the original position. @@ -5142,18 +5127,15 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { if (playbarHeadLayout == null) throw "ERROR: Tried to handle playbar, but playbarHeadLayout is null!"; + // Move the playhead to match the song position, if we aren't dragging it. + playbarHeadLayout.playbarHead.pos = currentScrollEase; + + playbarHeadLayout.playbarHead.max = songLengthInPixels; + // Make sure the playbar is never nudged out of the correct spot. playbarHeadLayout.x = 4; playbarHeadLayout.y = FlxG.height - 48 - 8; - // Move the playhead to match the song position, if we aren't dragging it. - if (!playbarHeadDragging) - { - var songPosPercent = scrollPositionInPixels / (songLengthInPixels) * 100; - - if (playbarHeadLayout.playbarHead.value != songPosPercent) playbarHeadLayout.playbarHead.value = songPosPercent; - } - 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 songPosSeconds:String = Std.string(Math.floor((Math.abs(songPos) / 1000) % 60)).lpad('0', 2); @@ -6134,43 +6116,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ function easeSongToScrollPosition(targetScrollPosition:Float):Void { - if (currentScrollEase != null) cancelScrollEase(currentScrollEase); - - currentScrollEase = FlxTween.tween(this, {scrollPositionInPixels: targetScrollPosition}, SCROLL_EASE_DURATION, - { - ease: FlxEase.quintInOut, - onUpdate: this.onScrollEaseUpdate, - onComplete: this.cancelScrollEase, - type: ONESHOT - }); - } - - /** - * Callback function executed every frame that the scroll position is being eased. - * @param _ - */ - function onScrollEaseUpdate(_:FlxTween):Void - { + currentScrollEase = Math.max(0, targetScrollPosition); + currentScrollEase = Math.min(currentScrollEase, songLengthInPixels); + scrollPositionInPixels = MathUtil.smoothLerp(scrollPositionInPixels, currentScrollEase, FlxG.elapsed, SCROLL_EASE_DURATION, 1 / 1000); moveSongToScrollPosition(); } - /** - * Callback function executed when cancelling an existing scroll position ease. - * Ensures that the ease is immediately cancelled and the scroll position is set to the target value. - */ - function cancelScrollEase(_:FlxTween):Void - { - if (currentScrollEase != null) - { - @:privateAccess - var targetScrollPosition:Float = currentScrollEase._properties.scrollPositionInPixels; - - currentScrollEase.cancel(); - currentScrollEase = null; - this.scrollPositionInPixels = targetScrollPosition; - } - } - /** * Fix the current scroll position after exiting the PlayState used when testing. */ @@ -6351,6 +6302,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { if (audioInstTrack == null) return; + currentScrollEase = this.scrollPositionInPixels; + if (audioInstTrack.isPlaying) { // Pause From 76c8c8b520c137cfc658ea629cd269026a118552 Mon Sep 17 00:00:00 2001 From: Abnormal <86753001+AbnormalPoof@users.noreply.github.com> Date: Sat, 26 Oct 2024 19:58:12 +0000 Subject: [PATCH 38/44] docs: Fix a 'ludem' -> 'ludum' typo in `CHANGELOG.md` --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fb4f02d4..faf1e7560 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -382,7 +382,7 @@ which would remove their rank if they had a lower one. - Improvements to video cutscenes and dialogue, allowing them to be easily skipped or restarted. - Updated Polymod by several major versions, allowing for fully dynamic asset replacement and support for scripted classes. - Completely refactored almost every part of the game's code for performance, stability, and extensibility. - - This is not the Ludem Dare game held together with sticks and glue you played three years ago. + - This is not the Ludum Dare game held together with sticks and glue you played three years ago. - Characters, stages, songs, story levels, and dialogue are now built from JSON data registries rather than being hardcoded. - All of these also support attaching scripts for custom behavior, more documentation on this soon. - You can forcibly reload the game's JSON data and scripts by pressing F5. From 090ddd1f1c2aa48fdb83127b2235041643c99af5 Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Fri, 25 Oct 2024 13:54:27 -0400 Subject: [PATCH 39/44] fix: fixes the initial camera position on the debug menu --- source/funkin/ui/debug/DebugMenuSubState.hx | 1 + 1 file changed, 1 insertion(+) diff --git a/source/funkin/ui/debug/DebugMenuSubState.hx b/source/funkin/ui/debug/DebugMenuSubState.hx index 45652095b..73c24912e 100644 --- a/source/funkin/ui/debug/DebugMenuSubState.hx +++ b/source/funkin/ui/debug/DebugMenuSubState.hx @@ -67,6 +67,7 @@ class DebugMenuSubState extends MusicBeatSubState #if sys createItem("OPEN CRASH LOG FOLDER", openLogFolder); #end + onMenuChange(items.members[0]); FlxG.camera.focusOn(new FlxPoint(camFocusPoint.x, camFocusPoint.y + 500)); } From e6b6b41766deb2e8655a8c2ddac3265f10dd82ab Mon Sep 17 00:00:00 2001 From: Hyper_ <40342021+NotHyper-474@users.noreply.github.com> Date: Sat, 26 Oct 2024 17:03:42 -0300 Subject: [PATCH 40/44] revert: "[BUGFIX?] Reset CWD before Preloader" (#3538) --- source/funkin/ui/transition/preload/FunkinPreloader.hx | 2 -- 1 file changed, 2 deletions(-) diff --git a/source/funkin/ui/transition/preload/FunkinPreloader.hx b/source/funkin/ui/transition/preload/FunkinPreloader.hx index 1b39a3482..e81bfbc4b 100644 --- a/source/funkin/ui/transition/preload/FunkinPreloader.hx +++ b/source/funkin/ui/transition/preload/FunkinPreloader.hx @@ -136,8 +136,6 @@ class FunkinPreloader extends FlxBasePreloader // We can't even call trace() yet, until Flixel loads. trace('Initializing custom preloader...'); - funkin.util.CLIUtil.resetWorkingDir(); - this.siteLockTitleText = Constants.SITE_LOCK_TITLE; this.siteLockBodyText = Constants.SITE_LOCK_DESC; } From e570dfb8e754f9cb29ac2d8fff6e8513bc68b630 Mon Sep 17 00:00:00 2001 From: Keoiki <55053690+Keoiki@users.noreply.github.com> Date: Sat, 26 Oct 2024 23:11:49 +0300 Subject: [PATCH 41/44] fix: Fix beat/step ticks sometimes not appearing on non-4/4 time signatures in chart editor (#2860) Beat and step ticks now appear fully on 5/4, 6/4 and 9/8 time signatures. --- .../handlers/ChartEditorThemeHandler.hx | 80 +++++++------------ 1 file changed, 27 insertions(+), 53 deletions(-) diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx index e42102a52..ac8985dc1 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx @@ -253,42 +253,23 @@ class ChartEditorThemeHandler var bottomTickY:Float = state.measureTickBitmap.height - (measureTickWidth / 2); state.measureTickBitmap.fillRect(new Rectangle(0, bottomTickY, state.measureTickBitmap.width, measureTickWidth / 2), GRID_MEASURE_DIVIDER_COLOR_LIGHT); - // Draw the beat ticks. - var beatTick2Y:Float = state.measureTickBitmap.height * 1 / Conductor.instance.beatsPerMeasure - (beatTickWidth / 2); - var beatTick3Y:Float = state.measureTickBitmap.height * 2 / Conductor.instance.beatsPerMeasure - (beatTickWidth / 2); - var beatTick4Y:Float = state.measureTickBitmap.height * 3 / Conductor.instance.beatsPerMeasure - (beatTickWidth / 2); - var beatTickLength:Float = state.measureTickBitmap.width * 2 / 3; - state.measureTickBitmap.fillRect(new Rectangle(0, beatTick2Y, beatTickLength, beatTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT); - state.measureTickBitmap.fillRect(new Rectangle(0, beatTick3Y, beatTickLength, beatTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT); - state.measureTickBitmap.fillRect(new Rectangle(0, beatTick4Y, beatTickLength, beatTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT); - - // Draw the step ticks. - // TODO: Make this a loop or something. - var stepTick2Y:Float = state.measureTickBitmap.height * 1 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2); - var stepTick3Y:Float = state.measureTickBitmap.height * 2 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2); - var stepTick4Y:Float = state.measureTickBitmap.height * 3 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2); - var stepTick6Y:Float = state.measureTickBitmap.height * 5 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2); - var stepTick7Y:Float = state.measureTickBitmap.height * 6 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2); - var stepTick8Y:Float = state.measureTickBitmap.height * 7 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2); - var stepTick10Y:Float = state.measureTickBitmap.height * 9 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2); - var stepTick11Y:Float = state.measureTickBitmap.height * 10 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2); - var stepTick12Y:Float = state.measureTickBitmap.height * 11 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2); - var stepTick14Y:Float = state.measureTickBitmap.height * 13 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2); - var stepTick15Y:Float = state.measureTickBitmap.height * 14 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2); - var stepTick16Y:Float = state.measureTickBitmap.height * 15 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2); - var stepTickLength:Float = state.measureTickBitmap.width * 1 / 3; - state.measureTickBitmap.fillRect(new Rectangle(0, stepTick2Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT); - state.measureTickBitmap.fillRect(new Rectangle(0, stepTick3Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT); - state.measureTickBitmap.fillRect(new Rectangle(0, stepTick4Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT); - state.measureTickBitmap.fillRect(new Rectangle(0, stepTick6Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT); - state.measureTickBitmap.fillRect(new Rectangle(0, stepTick7Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT); - state.measureTickBitmap.fillRect(new Rectangle(0, stepTick8Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT); - state.measureTickBitmap.fillRect(new Rectangle(0, stepTick10Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT); - state.measureTickBitmap.fillRect(new Rectangle(0, stepTick11Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT); - state.measureTickBitmap.fillRect(new Rectangle(0, stepTick12Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT); - state.measureTickBitmap.fillRect(new Rectangle(0, stepTick14Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT); - state.measureTickBitmap.fillRect(new Rectangle(0, stepTick15Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT); - state.measureTickBitmap.fillRect(new Rectangle(0, stepTick16Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + // Draw the beat and step ticks. No need for two seperate loops thankfully. + // This'll be fun to update when beat tuplets become functional. + for (i in 1...(Conductor.instance.stepsPerMeasure)) + { + if ((i % Constants.STEPS_PER_BEAT) == 0) // If we're on a beat, draw a beat tick. + { + var beatTickY:Float = state.measureTickBitmap.height * i / Conductor.instance.stepsPerMeasure - (beatTickWidth / 2); + var beatTickLength:Float = state.measureTickBitmap.width * 2 / 3; + state.measureTickBitmap.fillRect(new Rectangle(0, beatTickY, beatTickLength, beatTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + } + else // Else, draw a step tick. + { + var stepTickY:Float = state.measureTickBitmap.height * i / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2); + var stepTickLength:Float = state.measureTickBitmap.width * 1 / 3; + state.measureTickBitmap.fillRect(new Rectangle(0, stepTickY, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + } + } } /** @@ -314,23 +295,16 @@ class ChartEditorThemeHandler state.offsetTickBitmap.fillRect(new Rectangle(rightTickX, 0, majorTickWidth / 2, majorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT); // Draw the minor ticks. - var minorTick2X:Float = state.offsetTickBitmap.width * 1 / 10 - (minorTickWidth / 2); - var minorTick3X:Float = state.offsetTickBitmap.width * 2 / 10 - (minorTickWidth / 2); - var minorTick4X:Float = state.offsetTickBitmap.width * 3 / 10 - (minorTickWidth / 2); - var minorTick5X:Float = state.offsetTickBitmap.width * 4 / 10 - (minorTickWidth / 2); - var minorTick7X:Float = state.offsetTickBitmap.width * 6 / 10 - (minorTickWidth / 2); - var minorTick8X:Float = state.offsetTickBitmap.width * 7 / 10 - (minorTickWidth / 2); - var minorTick9X:Float = state.offsetTickBitmap.width * 8 / 10 - (minorTickWidth / 2); - var minorTick10X:Float = state.offsetTickBitmap.width * 9 / 10 - (minorTickWidth / 2); - var minorTickLength:Float = state.offsetTickBitmap.height * 1 / 3; - state.offsetTickBitmap.fillRect(new Rectangle(minorTick2X, 0, minorTickWidth, minorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT); - state.offsetTickBitmap.fillRect(new Rectangle(minorTick3X, 0, minorTickWidth, minorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT); - state.offsetTickBitmap.fillRect(new Rectangle(minorTick4X, 0, minorTickWidth, minorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT); - state.offsetTickBitmap.fillRect(new Rectangle(minorTick5X, 0, minorTickWidth, minorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT); - state.offsetTickBitmap.fillRect(new Rectangle(minorTick7X, 0, minorTickWidth, minorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT); - state.offsetTickBitmap.fillRect(new Rectangle(minorTick8X, 0, minorTickWidth, minorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT); - state.offsetTickBitmap.fillRect(new Rectangle(minorTick9X, 0, minorTickWidth, minorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT); - state.offsetTickBitmap.fillRect(new Rectangle(minorTick10X, 0, minorTickWidth, minorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + for (i in 1...11) + { + if (i % 5 == 0) + { + continue; + } + var minorTickX:Float = state.offsetTickBitmap.width * i / 10 - (minorTickWidth / 2); + var minorTickLength:Float = state.offsetTickBitmap.height * 1 / 3; + state.offsetTickBitmap.fillRect(new Rectangle(minorTickX, 0, minorTickWidth, minorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + } // Draw the offset ticks. // var ticksWidth:Int = Std.int(ChartEditorState.GRID_SIZE * TOTAL_COLUMN_COUNT); // 1 grid squares wide. From 101b9f59b3df4bee7eb61624c1f7baef8785bf89 Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Mon, 28 Oct 2024 14:24:30 -0400 Subject: [PATCH 42/44] assets submod --- art | 2 +- assets | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/art b/art index fbd3e3df7..66572f85d 160000 --- a/art +++ b/art @@ -1 +1 @@ -Subproject commit fbd3e3df77734606d88516770b71b56e6fa04bce +Subproject commit 66572f85d826ce2ec1d45468c12733b161237ffa diff --git a/assets b/assets index 4abd6cc06..021644d71 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 4abd6cc06e56c6d56440fa858262932db118250c +Subproject commit 021644d710ad5aceb185f0fd72bb78618d02df19 From 6321983eba40cb517a3b6ba422a21b3e6bfb9b13 Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Mon, 28 Oct 2024 14:27:00 -0400 Subject: [PATCH 43/44] assets submod --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index 021644d71..c1899ffbe 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 021644d710ad5aceb185f0fd72bb78618d02df19 +Subproject commit c1899ffbefb9a7c98b030c75a33623431d7ea6ba From 0d8e4a53305d6d069454812766300122f3581e31 Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Wed, 30 Oct 2024 12:41:28 -0400 Subject: [PATCH 44/44] fix: re-enable precise chart editor scrolling, and also fix smooth scroll playhead/playbar playback (#3806) --- source/funkin/ui/debug/charting/ChartEditorState.hx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 035884693..44c14be06 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -2729,6 +2729,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { // Update the conductor and audio tracks to match. currentScrollEase = d.value; + easeSongToScrollPosition(currentScrollEase); } } @@ -2741,7 +2742,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState playbarHeadDraggingWasPlaying = false; // Disabled code to resume song playback on drag. - // startAudioPlayback(); + startAudioPlayback(); } } @@ -3873,7 +3874,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState } // Mouse Wheel = Scroll - if (FlxG.mouse.wheel != 0 && !FlxG.keys.pressed.CONTROL) + if (FlxG.mouse.wheel != 0) { scrollAmount = -50 * FlxG.mouse.wheel; shouldPause = true; @@ -4469,6 +4470,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState 0, songLengthInPixels); currentScrollEase = clickedPosInPixels; + easeSongToScrollPosition(currentScrollEase); } else if (scrollAnchorScreenPos != null) {