From 336810b628631b0e1dcb67b25774bac1ce815395 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 4 Jan 2024 10:00:39 -0500 Subject: [PATCH 01/18] Tooltips when hovering over chart events --- hmm.json | 4 +- source/funkin/data/event/SongEventData.hx | 109 ++++++++++++++---- source/funkin/data/song/SongData.hx | 61 ++++++++++ .../funkin/play/event/FocusCameraSongEvent.hx | 4 +- .../play/event/PlayAnimationSongEvent.hx | 4 +- .../play/event/SetCameraBopSongEvent.hx | 4 +- .../funkin/play/event/ZoomCameraSongEvent.hx | 4 +- .../ui/debug/charting/ChartEditorState.hx | 3 +- .../components/ChartEditorEventSprite.hx | 42 ++++++- source/funkin/util/HaxeUIUtil.hx | 17 +++ 10 files changed, 217 insertions(+), 35 deletions(-) create mode 100644 source/funkin/util/HaxeUIUtil.hx diff --git a/hmm.json b/hmm.json index 57fbbb555..be9e2dd26 100644 --- a/hmm.json +++ b/hmm.json @@ -54,14 +54,14 @@ "name": "haxeui-core", "type": "git", "dir": null, - "ref": "e765a3e0b7a653823e8dec765e04623f27f573f8", + "ref": "67c5700e253ff8892589a95945a7799f34ae4df0", "url": "https://github.com/haxeui/haxeui-core" }, { "name": "haxeui-flixel", "type": "git", "dir": null, - "ref": "7a517d561eff49d8123c128bf9f5c1123b84d014", + "ref": "2b9cff727999b53ed292b1675ac1c9089ac77600", "url": "https://github.com/haxeui/haxeui-flixel" }, { diff --git a/source/funkin/data/event/SongEventData.hx b/source/funkin/data/event/SongEventData.hx index 7a167b031..a4a41e3a0 100644 --- a/source/funkin/data/event/SongEventData.hx +++ b/source/funkin/data/event/SongEventData.hx @@ -161,35 +161,71 @@ class SongEventParser } } -enum abstract SongEventFieldType(String) from String to String +@:forward(name, title, type, keys, min, max, step, defaultValue, iterator) +abstract SongEventSchema(SongEventSchemaRaw) { - /** - * The STRING type will display as a text field. - */ - var STRING = "string"; + public function new(?fields:Array) + { + this = fields; + } - /** - * The INTEGER type will display as a text field that only accepts numbers. - */ - var INTEGER = "integer"; + @:arrayAccess + public function getByName(name:String):SongEventSchemaField + { + for (field in this) + { + if (field.name == name) return field; + } - /** - * The FLOAT type will display as a text field that only accepts numbers. - */ - var FLOAT = "float"; + return null; + } - /** - * The BOOL type will display as a checkbox. - */ - var BOOL = "bool"; + public function getFirstField():SongEventSchemaField + { + return this[0]; + } - /** - * The ENUM type will display as a dropdown. - * Make sure to specify the `keys` field in the schema. - */ - var ENUM = "enum"; + public function stringifyFieldValue(name:String, value:Dynamic):String + { + var field:SongEventSchemaField = getByName(name); + if (field == null) return 'Unknown'; + + switch (field.type) + { + case SongEventFieldType.STRING: + return Std.string(value); + case SongEventFieldType.INTEGER: + return Std.string(value); + case SongEventFieldType.FLOAT: + return Std.string(value); + case SongEventFieldType.BOOL: + return Std.string(value); + case SongEventFieldType.ENUM: + for (key in field.keys.keys()) + { + if (field.keys.get(key) == value) return key; + } + return Std.string(value); + default: + return 'Unknown'; + } + } + + @:arrayAccess + public inline function get(key:Int) + { + return this[key]; + } + + @:arrayAccess + public inline function arrayWrite(k:Int, v:SongEventSchemaField):SongEventSchemaField + { + return this[k] = v; + } } +typedef SongEventSchemaRaw = Array; + typedef SongEventSchemaField = { /** @@ -240,4 +276,31 @@ typedef SongEventSchemaField = ?defaultValue:Dynamic, } -typedef SongEventSchema = Array; +enum abstract SongEventFieldType(String) from String to String +{ + /** + * The STRING type will display as a text field. + */ + var STRING = "string"; + + /** + * The INTEGER type will display as a text field that only accepts numbers. + */ + var INTEGER = "integer"; + + /** + * The FLOAT type will display as a text field that only accepts numbers. + */ + var FLOAT = "float"; + + /** + * The BOOL type will display as a checkbox. + */ + var BOOL = "bool"; + + /** + * The ENUM type will display as a dropdown. + * Make sure to specify the `keys` field in the schema. + */ + var ENUM = "enum"; +} diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx index 600871e2f..de73cd957 100644 --- a/source/funkin/data/song/SongData.hx +++ b/source/funkin/data/song/SongData.hx @@ -1,5 +1,8 @@ package funkin.data.song; +import funkin.play.event.SongEvent; +import funkin.data.event.SongEventData.SongEventParser; +import funkin.data.event.SongEventData.SongEventSchema; import funkin.data.song.SongRegistry; import thx.semver.Version; @@ -617,6 +620,38 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR this = new SongEventDataRaw(time, event, value); } + public inline function valueAsStruct(?defaultKey:String = "key"):Dynamic + { + if (this.value == null) return {}; + if (Std.isOfType(this.value, Array)) + { + var result:haxe.DynamicAccess = {}; + result.set(defaultKey, this.value); + return cast result; + } + else if (Reflect.isObject(this.value)) + { + // We enter this case if the value is a struct. + return cast this.value; + } + else + { + var result:haxe.DynamicAccess = {}; + result.set(defaultKey, this.value); + return cast result; + } + } + + public inline function getHandler():Null + { + return SongEventParser.getEvent(this.event); + } + + public inline function getSchema():Null + { + return SongEventParser.getEventSchema(this.event); + } + public inline function getDynamic(key:String):Null { return this.value == null ? null : Reflect.field(this.value, key); @@ -662,6 +697,32 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR return this.value == null ? null : cast Reflect.field(this.value, key); } + public function buildTooltip():String + { + var eventHandler = getHandler(); + var eventSchema = getSchema(); + + if (eventSchema == null) return 'Unknown Event: ${this.event}'; + + var result = '${eventHandler.getTitle()}'; + + var defaultKey = eventSchema.getFirstField()?.name; + var valueStruct:haxe.DynamicAccess = valueAsStruct(defaultKey); + + for (pair in valueStruct.keyValueIterator()) + { + var key = pair.key; + var value = pair.value; + + var title = eventSchema.getByName(key)?.title ?? 'UnknownField'; + var valueStr = eventSchema.stringifyFieldValue(key, value); + + result += '\n- ${title}: ${valueStr}'; + } + + return result; + } + public function clone():SongEventData { return new SongEventData(this.time, this.event, this.value); diff --git a/source/funkin/play/event/FocusCameraSongEvent.hx b/source/funkin/play/event/FocusCameraSongEvent.hx index 5f63254b0..c91769eb5 100644 --- a/source/funkin/play/event/FocusCameraSongEvent.hx +++ b/source/funkin/play/event/FocusCameraSongEvent.hx @@ -132,7 +132,7 @@ class FocusCameraSongEvent extends SongEvent */ public override function getEventSchema():SongEventSchema { - return [ + return new SongEventSchema([ { name: "char", title: "Character", @@ -154,6 +154,6 @@ class FocusCameraSongEvent extends SongEvent step: 10.0, type: SongEventFieldType.FLOAT, } - ]; + ]); } } diff --git a/source/funkin/play/event/PlayAnimationSongEvent.hx b/source/funkin/play/event/PlayAnimationSongEvent.hx index 6bc625517..0f611874b 100644 --- a/source/funkin/play/event/PlayAnimationSongEvent.hx +++ b/source/funkin/play/event/PlayAnimationSongEvent.hx @@ -89,7 +89,7 @@ class PlayAnimationSongEvent extends SongEvent */ public override function getEventSchema():SongEventSchema { - return [ + return new SongEventSchema([ { name: 'target', title: 'Target', @@ -108,6 +108,6 @@ class PlayAnimationSongEvent extends SongEvent type: SongEventFieldType.BOOL, defaultValue: false } - ]; + ]); } } diff --git a/source/funkin/play/event/SetCameraBopSongEvent.hx b/source/funkin/play/event/SetCameraBopSongEvent.hx index 3cdeb9a67..7d5fd4699 100644 --- a/source/funkin/play/event/SetCameraBopSongEvent.hx +++ b/source/funkin/play/event/SetCameraBopSongEvent.hx @@ -72,7 +72,7 @@ class SetCameraBopSongEvent extends SongEvent */ public override function getEventSchema():SongEventSchema { - return [ + return new SongEventSchema([ { name: 'intensity', title: 'Intensity', @@ -87,6 +87,6 @@ class SetCameraBopSongEvent extends SongEvent step: 1, type: SongEventFieldType.INTEGER, } - ]; + ]); } } diff --git a/source/funkin/play/event/ZoomCameraSongEvent.hx b/source/funkin/play/event/ZoomCameraSongEvent.hx index 1ae76039e..9a361f71b 100644 --- a/source/funkin/play/event/ZoomCameraSongEvent.hx +++ b/source/funkin/play/event/ZoomCameraSongEvent.hx @@ -99,7 +99,7 @@ class ZoomCameraSongEvent extends SongEvent */ public override function getEventSchema():SongEventSchema { - return [ + return new SongEventSchema([ { name: 'zoom', title: 'Zoom Level', @@ -145,6 +145,6 @@ class ZoomCameraSongEvent extends SongEvent 'Elastic In/Out' => 'elasticInOut', ] } - ]; + ]); } } diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 4f96fad69..5c12e3408 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -2113,7 +2113,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState add(gridGhostHoldNote); gridGhostHoldNote.zIndex = 11; - gridGhostEvent = new ChartEditorEventSprite(this); + gridGhostEvent = new ChartEditorEventSprite(this, true); gridGhostEvent.alpha = 0.6; gridGhostEvent.eventData = new SongEventData(-1, '', {}); gridGhostEvent.visible = false; @@ -3127,6 +3127,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Setting event data resets position relative to the grid so we fix that. eventSprite.x += renderedEvents.x; eventSprite.y += renderedEvents.y; + eventSprite.updateTooltipPosition(); } // Add hold notes that have been made visible (but not their parents) diff --git a/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx index 4c9d91407..cc9acf344 100644 --- a/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx +++ b/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx @@ -11,6 +11,9 @@ import flixel.graphics.frames.FlxFramesCollection; import flixel.graphics.frames.FlxTileFrames; import flixel.math.FlxPoint; import funkin.data.song.SongData.SongEventData; +import haxe.ui.tooltips.ToolTipRegionOptions; +import funkin.util.HaxeUIUtil; +import haxe.ui.tooltips.ToolTipManager; /** * A sprite that can be used to display a song event in a chart. @@ -36,6 +39,13 @@ class ChartEditorEventSprite extends FlxSprite public var overrideStepTime(default, set):Null = null; + public var tooltip:ToolTipRegionOptions; + + /** + * Whether this sprite is a "ghost" sprite used when hovering to place a new event. + */ + public var isGhost:Bool = false; + function set_overrideStepTime(value:Null):Null { if (overrideStepTime == value) return overrideStepTime; @@ -45,12 +55,14 @@ class ChartEditorEventSprite extends FlxSprite return overrideStepTime; } - public function new(parent:ChartEditorState) + public function new(parent:ChartEditorState, isGhost:Bool = false) { super(); this.parentState = parent; + this.isGhost = isGhost; + this.tooltip = HaxeUIUtil.buildTooltip('N/A'); this.frames = buildFrames(); buildAnimations(); @@ -140,6 +152,7 @@ class ChartEditorEventSprite extends FlxSprite // Disown parent. MAKE SURE TO REVIVE BEFORE REUSING this.kill(); this.visible = false; + updateTooltipPosition(); return null; } else @@ -151,6 +164,8 @@ class ChartEditorEventSprite extends FlxSprite this.eventData = value; // Update the position to match the note data. updateEventPosition(); + // Update the tooltip text. + this.tooltip.tipData = {text: this.eventData.buildTooltip()}; return this.eventData; } } @@ -169,6 +184,31 @@ class ChartEditorEventSprite extends FlxSprite this.x += origin.x; this.y += origin.y; } + + this.updateTooltipPosition(); + } + + public function updateTooltipPosition():Void + { + // No tooltip for ghost sprites. + if (this.isGhost) return; + + if (this.eventData == null) + { + // Disable the tooltip. + ToolTipManager.instance.unregisterTooltipRegion(this.tooltip); + } + else + { + // Update the position. + this.tooltip.left = this.x; + this.tooltip.top = this.y; + this.tooltip.width = this.width; + this.tooltip.height = this.height; + + // Enable the tooltip. + ToolTipManager.instance.registerTooltipRegion(this.tooltip); + } } /** diff --git a/source/funkin/util/HaxeUIUtil.hx b/source/funkin/util/HaxeUIUtil.hx new file mode 100644 index 000000000..1ffd9cd40 --- /dev/null +++ b/source/funkin/util/HaxeUIUtil.hx @@ -0,0 +1,17 @@ +package funkin.util; + +import haxe.ui.tooltips.ToolTipRegionOptions; + +class HaxeUIUtil +{ + public static function buildTooltip(text:String, ?left:Float, ?top:Float, ?width:Float, ?height:Float):ToolTipRegionOptions + { + return { + tipData: {text: text}, + left: left ?? 0.0, + top: top ?? 0.0, + width: width ?? 0.0, + height: height ?? 0.0 + } + } +} From 80c7bcdfdfaeec6a37d426c2b8b6ddb2b52b8d04 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Tue, 16 Jan 2024 16:49:15 -0500 Subject: [PATCH 02/18] Rewrite Stage data handling to use the Registry pattern, and add support for solid colors. --- source/funkin/InitState.hx | 5 +- source/funkin/data/character/TODO.md | 0 source/funkin/data/conversation/TODO.md | 0 source/funkin/data/dialogue/TODO.md | 0 source/funkin/data/level/LevelRegistry.hx | 4 +- source/funkin/data/song/SongRegistry.hx | 16 +- source/funkin/data/speaker/TODO.md | 0 source/funkin/data/stage/StageData.hx | 199 +++++++ source/funkin/data/stage/StageRegistry.hx | 103 ++++ source/funkin/graphics/FunkinSprite.hx | 53 ++ source/funkin/modding/PolymodHandler.hx | 5 +- source/funkin/play/GameOverSubState.hx | 3 +- source/funkin/play/PlayState.hx | 4 +- .../play/character/AnimateAtlasCharacter.hx | 3 +- source/funkin/play/stage/Stage.hx | 93 +-- source/funkin/play/stage/StageData.hx | 548 ------------------ source/funkin/play/stage/StageProp.hx | 4 +- .../ui/debug/charting/ChartEditorState.hx | 11 +- .../handlers/ChartEditorDialogHandler.hx | 2 +- .../handlers/ChartEditorToolboxHandler.hx | 6 +- .../toolboxes/ChartEditorEventDataToolbox.hx | 2 +- .../toolboxes/ChartEditorMetadataToolbox.hx | 10 +- .../charting/util/ChartEditorDropdowns.hx | 11 +- .../ui/debug/stage/StageOffsetSubState.hx | 14 +- source/funkin/ui/options/ControlsMenu.hx | 5 +- source/funkin/ui/story/StoryMenuState.hx | 3 +- source/funkin/ui/title/OutdatedSubState.hx | 2 +- source/funkin/ui/title/TitleState.hx | 4 +- source/funkin/ui/transition/LoadingState.hx | 5 +- 29 files changed, 484 insertions(+), 631 deletions(-) create mode 100644 source/funkin/data/character/TODO.md create mode 100644 source/funkin/data/conversation/TODO.md create mode 100644 source/funkin/data/dialogue/TODO.md create mode 100644 source/funkin/data/speaker/TODO.md create mode 100644 source/funkin/data/stage/StageData.hx create mode 100644 source/funkin/data/stage/StageRegistry.hx create mode 100644 source/funkin/graphics/FunkinSprite.hx delete mode 100644 source/funkin/play/stage/StageData.hx diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index 02b46c88c..c9198c3d4 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -20,11 +20,11 @@ import openfl.display.BitmapData; import funkin.data.level.LevelRegistry; import funkin.data.notestyle.NoteStyleRegistry; import funkin.data.event.SongEventRegistry; +import funkin.data.stage.StageRegistry; import funkin.play.cutscene.dialogue.ConversationDataParser; import funkin.play.cutscene.dialogue.DialogueBoxDataParser; import funkin.play.cutscene.dialogue.SpeakerDataParser; import funkin.data.song.SongRegistry; -import funkin.play.stage.StageData.StageDataParser; import funkin.play.character.CharacterData.CharacterDataParser; import funkin.modding.module.ModuleHandler; import funkin.ui.title.TitleState; @@ -217,8 +217,9 @@ class InitState extends FlxState ConversationDataParser.loadConversationCache(); DialogueBoxDataParser.loadDialogueBoxCache(); SpeakerDataParser.loadSpeakerCache(); - StageDataParser.loadStageCache(); + StageRegistry.instance.loadEntries(); CharacterDataParser.loadCharacterCache(); + ModuleHandler.buildModuleCallbacks(); ModuleHandler.loadModuleCache(); diff --git a/source/funkin/data/character/TODO.md b/source/funkin/data/character/TODO.md new file mode 100644 index 000000000..e69de29bb diff --git a/source/funkin/data/conversation/TODO.md b/source/funkin/data/conversation/TODO.md new file mode 100644 index 000000000..e69de29bb diff --git a/source/funkin/data/dialogue/TODO.md b/source/funkin/data/dialogue/TODO.md new file mode 100644 index 000000000..e69de29bb diff --git a/source/funkin/data/level/LevelRegistry.hx b/source/funkin/data/level/LevelRegistry.hx index b5c15de0f..96712cba5 100644 --- a/source/funkin/data/level/LevelRegistry.hx +++ b/source/funkin/data/level/LevelRegistry.hx @@ -7,9 +7,9 @@ import funkin.ui.story.ScriptedLevel; class LevelRegistry extends BaseRegistry { /** - * The current version string for the stage data format. + * The current version string for the level data format. * Handle breaking changes by incrementing this value - * and adding migration to the `migrateStageData()` function. + * and adding migration to the `migrateLevelData()` function. */ public static final LEVEL_DATA_VERSION:thx.semver.Version = "1.0.0"; diff --git a/source/funkin/data/song/SongRegistry.hx b/source/funkin/data/song/SongRegistry.hx index 98b46c782..b772349bc 100644 --- a/source/funkin/data/song/SongRegistry.hx +++ b/source/funkin/data/song/SongRegistry.hx @@ -127,7 +127,7 @@ class SongRegistry extends BaseRegistry variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; + parser.ignoreUnknownVariables = true; switch (loadEntryMetadataFile(id, variation)) { @@ -150,7 +150,7 @@ class SongRegistry extends BaseRegistry variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; + parser.ignoreUnknownVariables = true; parser.fromJson(contents, fileName); if (parser.errors.length > 0) @@ -210,7 +210,7 @@ class SongRegistry extends BaseRegistry variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; + parser.ignoreUnknownVariables = true; switch (loadEntryMetadataFile(id, variation)) { @@ -232,7 +232,7 @@ class SongRegistry extends BaseRegistry variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; + parser.ignoreUnknownVariables = true; switch (loadEntryMetadataFile(id, variation)) { @@ -252,7 +252,7 @@ class SongRegistry extends BaseRegistry function parseEntryMetadataRaw_v2_1_0(contents:String, ?fileName:String = 'raw'):Null { var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; + parser.ignoreUnknownVariables = true; parser.fromJson(contents, fileName); if (parser.errors.length > 0) @@ -266,7 +266,7 @@ class SongRegistry extends BaseRegistry function parseEntryMetadataRaw_v2_0_0(contents:String, ?fileName:String = 'raw'):Null { var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; + parser.ignoreUnknownVariables = true; parser.fromJson(contents, fileName); if (parser.errors.length > 0) @@ -347,7 +347,7 @@ class SongRegistry extends BaseRegistry variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; + parser.ignoreUnknownVariables = true; switch (loadEntryChartFile(id, variation)) { @@ -370,7 +370,7 @@ class SongRegistry extends BaseRegistry variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; + parser.ignoreUnknownVariables = true; parser.fromJson(contents, fileName); if (parser.errors.length > 0) diff --git a/source/funkin/data/speaker/TODO.md b/source/funkin/data/speaker/TODO.md new file mode 100644 index 000000000..e69de29bb diff --git a/source/funkin/data/stage/StageData.hx b/source/funkin/data/stage/StageData.hx new file mode 100644 index 000000000..cb914007f --- /dev/null +++ b/source/funkin/data/stage/StageData.hx @@ -0,0 +1,199 @@ +package funkin.data.stage; + +import funkin.data.animation.AnimationData; + +@:nullSafety +class StageData +{ + /** + * The sematic version number of the stage data JSON format. + * Supports fancy comparisons like NPM does it's neat. + */ + @:default(funkin.data.stage.StageRegistry.STAGE_DATA_VERSION) + public var version:String; + + public var name:String = 'Unknown'; + public var props:Array = []; + public var characters:StageDataCharacters; + + @:default(1.0) + @:optional + public var cameraZoom:Null; + + public function new() + { + this.version = StageRegistry.STAGE_DATA_VERSION; + this.characters = makeDefaultCharacters(); + } + + function makeDefaultCharacters():StageDataCharacters + { + return { + bf: + { + zIndex: 0, + position: [0, 0], + cameraOffsets: [-100, -100] + }, + dad: + { + zIndex: 0, + position: [0, 0], + cameraOffsets: [100, -100] + }, + gf: + { + zIndex: 0, + position: [0, 0], + cameraOffsets: [0, 0] + } + }; + } + + /** + * Convert this StageData into a JSON string. + */ + public function serialize(pretty:Bool = true):String + { + var writer = new json2object.JsonWriter(); + return writer.write(this, pretty ? ' ' : null); + } +} + +typedef StageDataCharacters = +{ + var bf:StageDataCharacter; + var dad:StageDataCharacter; + var gf:StageDataCharacter; +}; + +typedef StageDataProp = +{ + /** + * The name of the prop for later lookup by scripts. + * Optional; if unspecified, the prop can't be referenced by scripts. + */ + @:optional + var name:String; + + /** + * The asset used to display the prop. + * NOTE: As of Stage data v1.0.1, you can also use a color here to create a rectangle, like "#ff0000". + * In this case, the `scale` property will be used to determine the size of the prop. + */ + var assetPath:String; + + /** + * The position of the prop as an [x, y] array of two floats. + */ + var position:Array; + + /** + * A number determining the stack order of the prop, relative to other props and the characters in the stage. + * Props with lower numbers render below those with higher numbers. + * This is just like CSS, it isn't hard. + * @default 0 + */ + @:optional + @:default(0) + var zIndex:Int; + + /** + * If set to true, anti-aliasing will be forcibly disabled on the sprite. + * This prevents blurry images on pixel-art levels. + * @default false + */ + @:optional + @:default(false) + var isPixel:Bool; + + /** + * Either the scale of the prop as a float, or the [w, h] scale as an array of two floats. + * Pro tip: On pixel-art levels, save the sprite small and set this value to 6 or so to save memory. + */ + @:jcustomparse(funkin.data.DataParse.eitherFloatOrFloats) + @:jcustomwrite(funkin.data.DataWrite.eitherFloatOrFloats) + @:optional + var scale:haxe.ds.Either>; + + /** + * The alpha of the prop, as a float. + * @default 1.0 + */ + @:optional + @:default(1.0) + var alpha:Float; + + /** + * If not zero, this prop will play an animation every X beats of the song. + * This requires animations to be defined. If `danceLeft` and `danceRight` are defined, + * they will alternated between, otherwise the `idle` animation will be used. + * + * @default 0 + */ + @:default(0) + @:optional + var danceEvery:Int; + + /** + * How much the prop scrolls relative to the camera. Used to create a parallax effect. + * Represented as an [x, y] array of two floats. + * [1, 1] means the prop moves 1:1 with the camera. + * [0.5, 0.5] means the prop half as much as the camera. + * [0, 0] means the prop is not moved. + * @default [0, 0] + */ + @:optional + @:default([0, 0]) + var scroll:Array; + + /** + * An optional array of animations which the prop can play. + * @default Prop has no animations. + */ + @:optional + @:default([]) + var animations:Array; + + /** + * If animations are used, this is the name of the animation to play first. + * @default Don't play an animation. + */ + @:optional + var startingAnimation:Null; + + /** + * The animation type to use. + * Options: "sparrow", "packer" + * @default "sparrow" + */ + @:default("sparrow") + @:optional + var animType:String; +}; + +typedef StageDataCharacter = +{ + /** + * A number determining the stack order of the character, relative to props and other characters in the stage. + * Again, just like CSS. + * @default 0 + */ + @:optional + @:default(0) + var zIndex:Int; + + /** + * The position to render the character at. + */ + @:optional + @:default([0, 0]) + var position:Array; + + /** + * The camera offsets to apply when focusing on the character on this stage. + * @default [-100, -100] for BF, [100, -100] for DAD/OPPONENT, [0, 0] for GF + */ + @:optional + var cameraOffsets:Array; +}; diff --git a/source/funkin/data/stage/StageRegistry.hx b/source/funkin/data/stage/StageRegistry.hx new file mode 100644 index 000000000..b78292e5b --- /dev/null +++ b/source/funkin/data/stage/StageRegistry.hx @@ -0,0 +1,103 @@ +package funkin.data.stage; + +import funkin.data.stage.StageData; +import funkin.play.stage.Stage; +import funkin.play.stage.ScriptedStage; + +class StageRegistry extends BaseRegistry +{ + /** + * The current version string for the stage data format. + * 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.1"; + + public static final STAGE_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x"; + + public static final instance:StageRegistry = new StageRegistry(); + + public function new() + { + super('STAGE', 'stages', STAGE_DATA_VERSION_RULE); + } + + /** + * Read, parse, and validate the JSON data and produce the corresponding data object. + */ + public function parseEntryData(id:String):Null + { + // JsonParser does not take type parameters, + // otherwise this function would be in BaseRegistry. + var parser = new json2object.JsonParser(); + parser.ignoreUnknownVariables = false; + + switch (loadEntryFile(id)) + { + case {fileName: fileName, contents: contents}: + parser.fromJson(contents, fileName); + default: + return null; + } + + if (parser.errors.length > 0) + { + printErrors(parser.errors, id); + return null; + } + return parser.value; + } + + /** + * Parse and validate the JSON data and produce the corresponding data object. + * + * NOTE: Must be implemented on the implementation class. + * @param contents The JSON as a string. + * @param fileName An optional file name for error reporting. + */ + public function parseEntryDataRaw(contents:String, ?fileName:String):Null + { + var parser = new json2object.JsonParser(); + parser.ignoreUnknownVariables = false; + parser.fromJson(contents, fileName); + + if (parser.errors.length > 0) + { + printErrors(parser.errors, fileName); + return null; + } + return parser.value; + } + + function createScriptedEntry(clsName:String):Stage + { + return ScriptedStage.init(clsName, "unknown"); + } + + function getScriptedClassNames():Array + { + return ScriptedStage.listScriptClasses(); + } + + /** + * A list of all the stages from the base game, in order. + * TODO: Should this be hardcoded? + */ + public function listBaseGameStageIds():Array + { + return [ + "mainStage", "spookyMansion", "phillyTrain", "limoRide", "mallXmas", "mallEvil", "school", "schoolEvil", "tankmanBattlefield", "phillyStreets", + "phillyBlazin", + ]; + } + + /** + * A list of all installed story weeks that are not from the base game. + */ + public function listModdedStageIds():Array + { + return listEntryIds().filter(function(id:String):Bool { + return listBaseGameStageIds().indexOf(id) == -1; + }); + } +} diff --git a/source/funkin/graphics/FunkinSprite.hx b/source/funkin/graphics/FunkinSprite.hx new file mode 100644 index 000000000..487aaac34 --- /dev/null +++ b/source/funkin/graphics/FunkinSprite.hx @@ -0,0 +1,53 @@ +package funkin.graphics; + +import flixel.FlxSprite; +import flixel.util.FlxColor; +import flixel.graphics.FlxGraphic; + +/** + * An FlxSprite with additional functionality. + */ +class FunkinSprite extends FlxSprite +{ + /** + * @param x Starting X position + * @param y Starting Y position + */ + public function new(?x:Float = 0, ?y:Float = 0) + { + super(x, y); + } + + /** + * Acts similarly to `makeGraphic`, but with improved memory usage, + * at the expense of not being able to paint onto the sprite. + * + * @param width The target width of the sprite. + * @param height The target height of the sprite. + * @param color The color to fill the sprite with. + */ + public function makeSolidColor(width:Int, height:Int, color:FlxColor = FlxColor.WHITE):FunkinSprite + { + var graphic:FlxGraphic = FlxG.bitmap.create(2, 2, color, false, 'solid#${color.toHexString(true, false)}'); + frames = graphic.imageFrame; + scale.set(width / 2, height / 2); + updateHitbox(); + + return this; + } + + /** + * Ensure scale is applied when cloning a sprite. + * The default `clone()` method acts kinda weird TBH. + * @return A clone of this sprite. + */ + public override function clone():FunkinSprite + { + var result = new FunkinSprite(this.x, this.y); + result.frames = this.frames; + result.scale.set(this.scale.x, this.scale.y); + result.updateHitbox(); + + return result; + } +} diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx index b7ef07be5..151e658b4 100644 --- a/source/funkin/modding/PolymodHandler.hx +++ b/source/funkin/modding/PolymodHandler.hx @@ -4,11 +4,12 @@ import funkin.util.macro.ClassMacro; import funkin.modding.module.ModuleHandler; import funkin.play.character.CharacterData.CharacterDataParser; import funkin.data.song.SongData; -import funkin.play.stage.StageData; +import funkin.data.stage.StageData; import polymod.Polymod; import polymod.backends.PolymodAssets.PolymodAssetType; import polymod.format.ParseRules.TextFileFormat; import funkin.data.event.SongEventRegistry; +import funkin.data.stage.StageRegistry; import funkin.util.FileUtil; import funkin.data.level.LevelRegistry; import funkin.data.notestyle.NoteStyleRegistry; @@ -275,7 +276,7 @@ class PolymodHandler ConversationDataParser.loadConversationCache(); DialogueBoxDataParser.loadDialogueBoxCache(); SpeakerDataParser.loadSpeakerCache(); - StageDataParser.loadStageCache(); + StageRegistry.instance.loadEntries(); CharacterDataParser.loadCharacterCache(); ModuleHandler.loadModuleCache(); } diff --git a/source/funkin/play/GameOverSubState.hx b/source/funkin/play/GameOverSubState.hx index 137bf3905..cb190d23a 100644 --- a/source/funkin/play/GameOverSubState.hx +++ b/source/funkin/play/GameOverSubState.hx @@ -7,6 +7,7 @@ import flixel.sound.FlxSound; import funkin.ui.story.StoryMenuState; import flixel.util.FlxColor; import flixel.util.FlxTimer; +import funkin.graphics.FunkinSprite; import funkin.ui.MusicBeatSubState; import funkin.modding.events.ScriptEvent; import funkin.modding.events.ScriptEventDispatcher; @@ -94,7 +95,7 @@ class GameOverSubState extends MusicBeatSubState // // Add a black background to the screen. - var bg = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.BLACK); + var bg = new FunkinSprite().makeSolidColor(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK); // We make this transparent so that we can see the stage underneath during debugging, // but it's normally opaque. bg.alpha = transparent ? 0.25 : 1.0; diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 9f98d3b04..da525d4e0 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -50,11 +50,11 @@ import funkin.play.notes.SustainTrail; import funkin.play.scoring.Scoring; import funkin.play.song.Song; import funkin.data.song.SongRegistry; +import funkin.data.stage.StageRegistry; import funkin.data.song.SongData.SongEventData; import funkin.data.song.SongData.SongNoteData; import funkin.data.song.SongData.SongCharacterData; import funkin.play.stage.Stage; -import funkin.play.stage.StageData.StageDataParser; import funkin.ui.transition.LoadingState; import funkin.play.components.PopUpStuff; import funkin.ui.options.PreferencesMenu; @@ -1353,7 +1353,7 @@ class PlayState extends MusicBeatSubState */ function loadStage(id:String):Void { - currentStage = StageDataParser.fetchStage(id); + currentStage = StageRegistry.instance.fetchEntry(id); if (currentStage != null) { diff --git a/source/funkin/play/character/AnimateAtlasCharacter.hx b/source/funkin/play/character/AnimateAtlasCharacter.hx index 3523ec994..f9dc18119 100644 --- a/source/funkin/play/character/AnimateAtlasCharacter.hx +++ b/source/funkin/play/character/AnimateAtlasCharacter.hx @@ -9,6 +9,7 @@ import flixel.math.FlxMath; import flixel.math.FlxPoint.FlxCallbackPoint; import flixel.math.FlxPoint; import flixel.math.FlxRect; +import funkin.graphics.FunkinSprite; import flixel.system.FlxAssets.FlxGraphicAsset; import flixel.util.FlxColor; import flixel.util.FlxDestroyUtil; @@ -621,7 +622,7 @@ class AnimateAtlasCharacter extends BaseCharacter * This functionality isn't supported in SpriteGroup * @return this sprite group */ - public override function loadGraphicFromSprite(Sprite:FlxSprite):FlxSprite + public override function loadGraphicFromSprite(Sprite:FlxSprite):FunkinSprite { #if FLX_DEBUG throw "This function is not supported in FlxSpriteGroup"; diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx index c8cb8ce66..ac6c3705e 100644 --- a/source/funkin/play/stage/Stage.hx +++ b/source/funkin/play/stage/Stage.hx @@ -5,13 +5,16 @@ import flixel.group.FlxSpriteGroup; import flixel.math.FlxPoint; import flixel.system.FlxAssets.FlxShader; import flixel.util.FlxSort; +import flixel.util.FlxColor; import funkin.modding.IScriptedClass; import funkin.modding.events.ScriptEvent; import funkin.modding.events.ScriptEventType; import funkin.modding.events.ScriptEventDispatcher; import funkin.play.character.BaseCharacter; -import funkin.play.stage.StageData.StageDataCharacter; -import funkin.play.stage.StageData.StageDataParser; +import funkin.data.IRegistryEntry; +import funkin.data.stage.StageData; +import funkin.data.stage.StageData.StageDataCharacter; +import funkin.data.stage.StageRegistry; import funkin.play.stage.StageProp; import funkin.util.SortUtil; import funkin.util.assets.FlxAnimationUtil; @@ -23,14 +26,25 @@ typedef StagePropGroup = FlxTypedSpriteGroup; * * A Stage is comprised of one or more props, each of which is a FlxSprite. */ -class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass +class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements IRegistryEntry { - public final stageId:String; - public final stageName:String; + public final id:String; - final _data:StageData; + public final _data:StageData; - public var camZoom:Float = 1.0; + public var stageName(get, never):String; + + function get_stageName():String + { + return _data?.name ?? 'Unknown'; + } + + public var camZoom(get, never):Float; + + function get_camZoom():Float + { + return _data?.cameraZoom ?? 1.0; + } var namedProps:Map = new Map(); var characters:Map = new Map(); @@ -41,21 +55,18 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass * They're used to cache the data needed to build the stage, * then accessed and fleshed out when the stage needs to be built. * - * @param stageId + * @param id */ - public function new(stageId:String) + public function new(id:String) { super(); - this.stageId = stageId; - _data = StageDataParser.parseStageData(this.stageId); + this.id = id; + _data = _fetchData(id); + if (_data == null) { - throw 'Could not find stage data for stageId: $stageId'; - } - else - { - this.stageName = _data.name; + throw 'Could not find stage data for stage id: $id'; } } @@ -129,9 +140,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass */ function buildStage():Void { - trace('Building stage for display: ${this.stageId}'); - - this.camZoom = _data.cameraZoom; + trace('Building stage for display: ${this.id}'); this.debugIconGroup = new FlxSpriteGroup(); @@ -139,6 +148,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass { trace(' Placing prop: ${dataProp.name} (${dataProp.assetPath})'); + var isSolidColor = dataProp.assetPath.startsWith('#'); var isAnimated = dataProp.animations.length > 0; var propSprite:StageProp; @@ -162,6 +172,22 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass propSprite.frames = Paths.getSparrowAtlas(dataProp.assetPath); } } + else if (isSolidColor) + { + var width:Int = 1; + var height:Int = 1; + switch (dataProp.scale) + { + case Left(value): + width = Std.int(value); + height = Std.int(value); + + case Right(values): + width = Std.int(values[0]); + height = Std.int(values[1]); + } + propSprite.makeSolidColor(width, height, FlxColor.fromString(dataProp.assetPath)); + } else { // Initalize static sprite. @@ -177,13 +203,16 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass continue; } - switch (dataProp.scale) + if (!isSolidColor) { - case Left(value): - propSprite.scale.set(value); + switch (dataProp.scale) + { + case Left(value): + propSprite.scale.set(value); - case Right(values): - propSprite.scale.set(values[0], values[1]); + case Right(values): + propSprite.scale.set(values[0], values[1]); + } } propSprite.updateHitbox(); @@ -195,15 +224,8 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass // If pixel, disable antialiasing. propSprite.antialiasing = !dataProp.isPixel; - switch (dataProp.scroll) - { - case Left(value): - propSprite.scrollFactor.x = value; - propSprite.scrollFactor.y = value; - case Right(values): - propSprite.scrollFactor.x = values[0]; - propSprite.scrollFactor.y = values[1]; - } + propSprite.scrollFactor.x = dataProp.scroll[0]; + propSprite.scrollFactor.y = dataProp.scroll[1]; propSprite.zIndex = dataProp.zIndex; @@ -731,6 +753,11 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass return Sprite; } + static function _fetchData(id:String):Null + { + return StageRegistry.instance.parseEntryDataWithMigration(id, StageRegistry.instance.fetchEntryVersion(id)); + } + public function onScriptEvent(event:ScriptEvent) {} public function onPause(event:PauseScriptEvent) {} diff --git a/source/funkin/play/stage/StageData.hx b/source/funkin/play/stage/StageData.hx deleted file mode 100644 index 2d87dec31..000000000 --- a/source/funkin/play/stage/StageData.hx +++ /dev/null @@ -1,548 +0,0 @@ -package funkin.play.stage; - -import funkin.data.animation.AnimationData; -import funkin.play.stage.ScriptedStage; -import funkin.play.stage.Stage; -import funkin.util.VersionUtil; -import funkin.util.assets.DataAssets; -import haxe.Json; -import openfl.Assets; - -/** - * Contains utilities for loading and parsing stage data. - */ -class StageDataParser -{ - /** - * The current version string for the stage data format. - * Handle breaking changes by incrementing this value - * and adding migration to the `migrateStageData()` function. - */ - public static final STAGE_DATA_VERSION:String = "1.0.0"; - - /** - * The current version rule check for the stage data format. - */ - public static final STAGE_DATA_VERSION_RULE:String = "1.0.x"; - - static final stageCache:Map = new Map(); - - static final DEFAULT_STAGE_ID = 'UNKNOWN'; - - /** - * Parses and preloads the game's stage data and scripts when the game starts. - * - * If you want to force stages to be reloaded, you can just call this function again. - */ - public static function loadStageCache():Void - { - // Clear any stages that are cached if there were any. - clearStageCache(); - trace("Loading stage cache..."); - - // - // SCRIPTED STAGES - // - var scriptedStageClassNames:Array = ScriptedStage.listScriptClasses(); - trace(' Instantiating ${scriptedStageClassNames.length} scripted stages...'); - for (stageCls in scriptedStageClassNames) - { - var stage:Stage = ScriptedStage.init(stageCls, DEFAULT_STAGE_ID); - if (stage != null) - { - trace(' Loaded scripted stage: ${stage.stageName}'); - // Disable the rendering logic for stage until it's loaded. - // Note that kill() =/= destroy() - stage.kill(); - - // Then store it. - stageCache.set(stage.stageId, stage); - } - else - { - trace(' Failed to instantiate scripted stage class: ${stageCls}'); - } - } - - // - // UNSCRIPTED STAGES - // - var stageIdList:Array = DataAssets.listDataFilesInPath('stages/'); - var unscriptedStageIds:Array = stageIdList.filter(function(stageId:String):Bool { - return !stageCache.exists(stageId); - }); - trace(' Instantiating ${unscriptedStageIds.length} non-scripted stages...'); - for (stageId in unscriptedStageIds) - { - var stage:Stage; - try - { - stage = new Stage(stageId); - if (stage != null) - { - trace(' Loaded stage data: ${stage.stageName}'); - stageCache.set(stageId, stage); - } - } - catch (e) - { - trace(' An error occurred while loading stage data: ${stageId}'); - // Assume error was already logged. - continue; - } - } - - trace(' Successfully loaded ${Lambda.count(stageCache)} stages.'); - } - - public static function fetchStage(stageId:String):Null - { - if (stageCache.exists(stageId)) - { - trace('Successfully fetch stage: ${stageId}'); - var stage:Stage = stageCache.get(stageId); - stage.revive(); - return stage; - } - else - { - trace('Failed to fetch stage, not found in cache: ${stageId}'); - return null; - } - } - - static function clearStageCache():Void - { - if (stageCache != null) - { - for (stage in stageCache) - { - stage.destroy(); - } - stageCache.clear(); - } - } - - /** - * Load a stage's JSON file, parse its data, and return it. - * - * @param stageId The stage to load. - * @return The stage data, or null if validation failed. - */ - public static function parseStageData(stageId:String):Null - { - var rawJson:String = loadStageFile(stageId); - - var stageData:StageData = migrateStageData(rawJson, stageId); - - return validateStageData(stageId, stageData); - } - - public static function listStageIds():Array - { - return stageCache.keys().array(); - } - - static function loadStageFile(stagePath:String):String - { - var stageFilePath:String = Paths.json('stages/${stagePath}'); - var rawJson = Assets.getText(stageFilePath).trim(); - - while (!rawJson.endsWith("}")) - { - rawJson = rawJson.substr(0, rawJson.length - 1); - } - - return rawJson; - } - - static function migrateStageData(rawJson:String, stageId:String):Null - { - // If you update the stage data format in a breaking way, - // handle migration here by checking the `version` value. - - try - { - var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - parser.fromJson(rawJson, '$stageId.json'); - - if (parser.errors.length > 0) - { - trace('[STAGE] Failed to parse stage data'); - - for (error in parser.errors) - funkin.data.DataError.printError(error); - - return null; - } - return parser.value; - } - catch (e) - { - trace(' Error parsing data for stage: ${stageId}'); - trace(' ${e}'); - return null; - } - } - - static final DEFAULT_ANIMTYPE:String = "sparrow"; - static final DEFAULT_CAMERAZOOM:Float = 1.0; - static final DEFAULT_DANCEEVERY:Int = 0; - static final DEFAULT_ISPIXEL:Bool = false; - static final DEFAULT_NAME:String = "Untitled Stage"; - static final DEFAULT_OFFSETS:Array = [0, 0]; - static final DEFAULT_CAMERA_OFFSETS_BF:Array = [-100, -100]; - static final DEFAULT_CAMERA_OFFSETS_DAD:Array = [150, -100]; - static final DEFAULT_POSITION:Array = [0, 0]; - static final DEFAULT_SCALE:Float = 1.0; - static final DEFAULT_ALPHA:Float = 1.0; - static final DEFAULT_SCROLL:Array = [0, 0]; - static final DEFAULT_ZINDEX:Int = 0; - - static final DEFAULT_CHARACTER_DATA:StageDataCharacter = - { - zIndex: DEFAULT_ZINDEX, - position: DEFAULT_POSITION, - cameraOffsets: DEFAULT_OFFSETS, - } - - /** - * Set unspecified parameters to their defaults. - * If the parameter is mandatory, print an error message. - * @param id - * @param input - * @return The validated stage data - */ - static function validateStageData(id:String, input:StageData):Null - { - if (input == null) - { - trace('ERROR: Could not parse stage data for "${id}".'); - return null; - } - - if (input.version == null) - { - trace('ERROR: Could not load stage data for "$id": missing version'); - return null; - } - - if (!VersionUtil.validateVersionStr(input.version, STAGE_DATA_VERSION_RULE)) - { - trace('ERROR: Could not load stage data for "$id": bad version (got ${input.version}, expected ${STAGE_DATA_VERSION_RULE})'); - return null; - } - - if (input.name == null) - { - trace('WARN: Stage data for "$id" missing name'); - input.name = DEFAULT_NAME; - } - - if (input.cameraZoom == null) - { - input.cameraZoom = DEFAULT_CAMERAZOOM; - } - - if (input.props == null) - { - input.props = []; - } - - for (inputProp in input.props) - { - // It's fine for inputProp.name to be null - - if (inputProp.assetPath == null) - { - trace('ERROR: Could not load stage data for "$id": missing assetPath for prop "${inputProp.name}"'); - return null; - } - - if (inputProp.position == null) - { - inputProp.position = DEFAULT_POSITION; - } - - if (inputProp.zIndex == null) - { - inputProp.zIndex = DEFAULT_ZINDEX; - } - - if (inputProp.isPixel == null) - { - inputProp.isPixel = DEFAULT_ISPIXEL; - } - - if (inputProp.danceEvery == null) - { - inputProp.danceEvery = DEFAULT_DANCEEVERY; - } - - if (inputProp.animType == null) - { - inputProp.animType = DEFAULT_ANIMTYPE; - } - - switch (inputProp.scale) - { - case null: - inputProp.scale = Right([DEFAULT_SCALE, DEFAULT_SCALE]); - case Left(value): - inputProp.scale = Right([value, value]); - case Right(_): - // Do nothing - } - - switch (inputProp.scroll) - { - case null: - inputProp.scroll = Right(DEFAULT_SCROLL); - case Left(value): - inputProp.scroll = Right([value, value]); - case Right(_): - // Do nothing - } - - if (inputProp.alpha == null) - { - inputProp.alpha = DEFAULT_ALPHA; - } - - if (inputProp.animations == null) - { - inputProp.animations = []; - } - - if (inputProp.animations.length == 0 && inputProp.startingAnimation != null) - { - trace('ERROR: Could not load stage data for "$id": missing animations for prop "${inputProp.name}"'); - return null; - } - - for (inputAnimation in inputProp.animations) - { - if (inputAnimation.name == null) - { - trace('ERROR: Could not load stage data for "$id": missing animation name for prop "${inputProp.name}"'); - return null; - } - - if (inputAnimation.frameRate == null) - { - inputAnimation.frameRate = 24; - } - - if (inputAnimation.offsets == null) - { - inputAnimation.offsets = DEFAULT_OFFSETS; - } - - if (inputAnimation.looped == null) - { - inputAnimation.looped = true; - } - - if (inputAnimation.flipX == null) - { - inputAnimation.flipX = false; - } - - if (inputAnimation.flipY == null) - { - inputAnimation.flipY = false; - } - } - } - - if (input.characters == null) - { - trace('ERROR: Could not load stage data for "$id": missing characters'); - return null; - } - - if (input.characters.bf == null) - { - input.characters.bf = DEFAULT_CHARACTER_DATA; - } - if (input.characters.dad == null) - { - input.characters.dad = DEFAULT_CHARACTER_DATA; - } - if (input.characters.gf == null) - { - input.characters.gf = DEFAULT_CHARACTER_DATA; - } - - for (inputCharacter in [input.characters.bf, input.characters.dad, input.characters.gf]) - { - if (inputCharacter.position == null || inputCharacter.position.length != 2) - { - inputCharacter.position = [0, 0]; - } - } - - // All good! - return input; - } -} - -class StageData -{ - /** - * The sematic version number of the stage data JSON format. - * Supports fancy comparisons like NPM does it's neat. - */ - public var version:String; - - public var name:String; - public var cameraZoom:Null; - public var props:Array; - public var characters:StageDataCharacters; - - public function new() - { - this.version = StageDataParser.STAGE_DATA_VERSION; - } - - /** - * Convert this StageData into a JSON string. - */ - public function serialize(pretty:Bool = true):String - { - var writer = new json2object.JsonWriter(); - return writer.write(this, pretty ? ' ' : null); - } -} - -typedef StageDataCharacters = -{ - var bf:StageDataCharacter; - var dad:StageDataCharacter; - var gf:StageDataCharacter; -}; - -typedef StageDataProp = -{ - /** - * The name of the prop for later lookup by scripts. - * Optional; if unspecified, the prop can't be referenced by scripts. - */ - @:optional - var name:String; - - /** - * The asset used to display the prop. - */ - var assetPath:String; - - /** - * The position of the prop as an [x, y] array of two floats. - */ - var position:Array; - - /** - * A number determining the stack order of the prop, relative to other props and the characters in the stage. - * Props with lower numbers render below those with higher numbers. - * This is just like CSS, it isn't hard. - * @default 0 - */ - @:optional - @:default(0) - var zIndex:Int; - - /** - * If set to true, anti-aliasing will be forcibly disabled on the sprite. - * This prevents blurry images on pixel-art levels. - * @default false - */ - @:optional - @:default(false) - var isPixel:Bool; - - /** - * Either the scale of the prop as a float, or the [w, h] scale as an array of two floats. - * Pro tip: On pixel-art levels, save the sprite small and set this value to 6 or so to save memory. - */ - @:jcustomparse(funkin.data.DataParse.eitherFloatOrFloats) - @:jcustomwrite(funkin.data.DataWrite.eitherFloatOrFloats) - @:optional - var scale:haxe.ds.Either>; - - /** - * The alpha of the prop, as a float. - * @default 1.0 - */ - @:optional - @:default(1.0) - var alpha:Float; - - /** - * If not zero, this prop will play an animation every X beats of the song. - * This requires animations to be defined. If `danceLeft` and `danceRight` are defined, - * they will alternated between, otherwise the `idle` animation will be used. - * - * @default 0 - */ - @:default(0) - @:optional - var danceEvery:Int; - - /** - * How much the prop scrolls relative to the camera. Used to create a parallax effect. - * Represented as a float or as an [x, y] array of two floats. - * [1, 1] means the prop moves 1:1 with the camera. - * [0.5, 0.5] means the prop half as much as the camera. - * [0, 0] means the prop is not moved. - * @default [0, 0] - */ - @:jcustomparse(funkin.data.DataParse.eitherFloatOrFloats) - @:jcustomwrite(funkin.data.DataWrite.eitherFloatOrFloats) - @:optional - var scroll:haxe.ds.Either>; - - /** - * An optional array of animations which the prop can play. - * @default Prop has no animations. - */ - @:optional - var animations:Array; - - /** - * If animations are used, this is the name of the animation to play first. - * @default Don't play an animation. - */ - @:optional - var startingAnimation:Null; - - /** - * The animation type to use. - * Options: "sparrow", "packer" - * @default "sparrow" - */ - @:default("sparrow") - @:optional - var animType:String; -}; - -typedef StageDataCharacter = -{ - /** - * A number determining the stack order of the character, relative to props and other characters in the stage. - * Again, just like CSS. - * @default 0 - */ - var zIndex:Int; - - /** - * The position to render the character at. - */ - var position:Array; - - /** - * The camera offsets to apply when focusing on the character on this stage. - * @default [-100, -100] for BF, [100, -100] for DAD/OPPONENT, [0, 0] for GF - */ - var cameraOffsets:Array; -}; diff --git a/source/funkin/play/stage/StageProp.hx b/source/funkin/play/stage/StageProp.hx index 4f67c5e4b..4d846162b 100644 --- a/source/funkin/play/stage/StageProp.hx +++ b/source/funkin/play/stage/StageProp.hx @@ -1,10 +1,10 @@ package funkin.play.stage; import funkin.modding.events.ScriptEvent; -import flixel.FlxSprite; +import funkin.graphics.FunkinSprite; import funkin.modding.IScriptedClass.IStateStageProp; -class StageProp extends FlxSprite implements IStateStageProp +class StageProp extends FunkinSprite implements IStateStageProp { /** * An internal name for this prop. diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 0853bf390..3090abad2 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -12,6 +12,7 @@ import flixel.FlxCamera; import flixel.FlxSprite; import flixel.FlxSubState; import flixel.group.FlxSpriteGroup; +import funkin.graphics.FunkinSprite; import flixel.input.keyboard.FlxKey; import flixel.math.FlxMath; import flixel.math.FlxPoint; @@ -56,7 +57,7 @@ import funkin.data.song.SongData.SongNoteData; import funkin.data.song.SongData.SongCharacterData; import funkin.data.song.SongDataUtils; import funkin.ui.debug.charting.commands.ChartEditorCommand; -import funkin.play.stage.StageData; +import funkin.data.stage.StageData; import funkin.save.Save; import funkin.ui.debug.charting.commands.AddEventsCommand; import funkin.ui.debug.charting.commands.AddNotesCommand; @@ -2230,7 +2231,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState var playheadWidth:Int = GRID_SIZE * (STRUMLINE_SIZE * 2 + 1) + (PLAYHEAD_SCROLL_AREA_WIDTH * 2); var playheadBaseYPos:Float = GRID_INITIAL_Y_POS; gridPlayhead.setPosition(GRID_X_POS, playheadBaseYPos); - var playheadSprite:FlxSprite = new FlxSprite().makeGraphic(playheadWidth, PLAYHEAD_HEIGHT, PLAYHEAD_COLOR); + var playheadSprite:FunkinSprite = new FunkinSprite().makeSolidColor(playheadWidth, PLAYHEAD_HEIGHT, PLAYHEAD_COLOR); playheadSprite.x = -PLAYHEAD_SCROLL_AREA_WIDTH; playheadSprite.y = 0; gridPlayhead.add(playheadSprite); @@ -5951,9 +5952,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState ChartEditorNoteSprite.noteFrameCollection = null; // Stop the music. - welcomeMusic.destroy(); - audioInstTrack.destroy(); - audioVocalTrackGroup.destroy(); + if (welcomeMusic != null) welcomeMusic.destroy(); + if (audioInstTrack != null) audioInstTrack.destroy(); + if (audioVocalTrackGroup != null) audioVocalTrackGroup.destroy(); } function applyCanQuickSave():Void diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx index 1e1a02974..32e9fef53 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx @@ -13,7 +13,7 @@ import funkin.play.character.BaseCharacter; import funkin.play.character.CharacterData; import funkin.play.character.CharacterData.CharacterDataParser; import funkin.play.song.Song; -import funkin.play.stage.StageData; +import funkin.data.stage.StageData; import funkin.ui.debug.charting.dialogs.ChartEditorAboutDialog; import funkin.ui.debug.charting.dialogs.ChartEditorBaseDialog.DialogDropTarget; import funkin.ui.debug.charting.dialogs.ChartEditorCharacterIconSelectorMenu; diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx index ce1997968..1916f92c2 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx @@ -1,7 +1,6 @@ package funkin.ui.debug.charting.handlers; -import funkin.play.stage.StageData.StageDataParser; -import funkin.play.stage.StageData; +import funkin.data.stage.StageData; import funkin.play.character.CharacterData; import funkin.play.character.CharacterData.CharacterDataParser; import haxe.ui.components.HorizontalSlider; @@ -16,8 +15,7 @@ import funkin.play.character.CharacterData; import funkin.play.character.CharacterData.CharacterDataParser; import funkin.play.event.SongEvent; import funkin.play.song.SongSerializer; -import funkin.play.stage.StageData; -import funkin.play.stage.StageData.StageDataParser; +import funkin.data.stage.StageData; import haxe.ui.RuntimeComponentBuilder; import funkin.ui.debug.charting.util.ChartEditorDropdowns; import funkin.ui.haxeui.components.CharacterPlayer; diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx index 480873bc5..fbd1562b4 100644 --- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx +++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx @@ -2,7 +2,7 @@ package funkin.ui.debug.charting.toolboxes; import funkin.play.character.BaseCharacter.CharacterType; import funkin.play.character.CharacterData; -import funkin.play.stage.StageData; +import funkin.data.stage.StageData; import funkin.play.event.SongEvent; import funkin.data.event.SongEventSchema; import funkin.ui.debug.charting.commands.ChangeStartingBPMCommand; diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx index 98aa02151..06b20ed7c 100644 --- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx +++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx @@ -2,7 +2,8 @@ package funkin.ui.debug.charting.toolboxes; import funkin.play.character.BaseCharacter.CharacterType; import funkin.play.character.CharacterData; -import funkin.play.stage.StageData; +import funkin.data.stage.StageData; +import funkin.data.stage.StageRegistry; import funkin.ui.debug.charting.commands.ChangeStartingBPMCommand; import funkin.ui.debug.charting.util.ChartEditorDropdowns; import haxe.ui.components.Button; @@ -13,6 +14,7 @@ import haxe.ui.components.Label; import haxe.ui.components.NumberStepper; import haxe.ui.components.Slider; import haxe.ui.components.TextField; +import funkin.play.stage.Stage; import haxe.ui.containers.Box; import haxe.ui.containers.Frame; import haxe.ui.events.UIEvent; @@ -199,11 +201,11 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox inputTimeSignature.value = {id: currentTimeSignature, text: currentTimeSignature}; var stageId:String = chartEditorState.currentSongMetadata.playData.stage; - var stageData:Null = StageDataParser.parseStageData(stageId); + var stage:Null = StageRegistry.instance.fetchEntry(stageId); if (inputStage != null) { - inputStage.value = (stageData != null) ? - {id: stageId, text: stageData.name} : + inputStage.value = (stage != null) ? + {id: stage.id, text: stage.stageName} : {id: "mainStage", text: "Main Stage"}; } diff --git a/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx b/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx index dfa0408d3..14c07440b 100644 --- a/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx +++ b/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx @@ -2,10 +2,11 @@ package funkin.ui.debug.charting.util; import funkin.data.notestyle.NoteStyleRegistry; import funkin.play.notes.notestyle.NoteStyle; -import funkin.play.stage.StageData; -import funkin.play.stage.StageData.StageDataParser; +import funkin.data.stage.StageData; +import funkin.data.stage.StageRegistry; import funkin.play.character.CharacterData; import haxe.ui.components.DropDown; +import funkin.play.stage.Stage; import funkin.play.character.BaseCharacter.CharacterType; import funkin.play.character.CharacterData.CharacterDataParser; @@ -60,16 +61,16 @@ class ChartEditorDropdowns { dropDown.dataSource.clear(); - var stageIds:Array = StageDataParser.listStageIds(); + var stageIds:Array = StageRegistry.instance.listEntryIds(); var returnValue:DropDownEntry = {id: "mainStage", text: "Main Stage"}; for (stageId in stageIds) { - var stage:Null = StageDataParser.parseStageData(stageId); + var stage:Null = StageRegistry.instance.fetchEntry(stageId); if (stage == null) continue; - var value = {id: stageId, text: stage.name}; + var value = {id: stage.id, text: stage.stageName}; if (startingStageId == stageId) returnValue = value; dropDown.dataSource.add(value); diff --git a/source/funkin/ui/debug/stage/StageOffsetSubState.hx b/source/funkin/ui/debug/stage/StageOffsetSubState.hx index 68546f1c7..e8a5d0a23 100644 --- a/source/funkin/ui/debug/stage/StageOffsetSubState.hx +++ b/source/funkin/ui/debug/stage/StageOffsetSubState.hx @@ -5,15 +5,17 @@ import flixel.input.mouse.FlxMouseEvent; import flixel.math.FlxPoint; import funkin.play.character.BaseCharacter; import funkin.play.PlayState; -import funkin.play.stage.StageData; +import funkin.data.stage.StageData; import funkin.play.stage.StageProp; import funkin.graphics.shaders.StrokeShader; import funkin.ui.haxeui.HaxeUISubState; import funkin.ui.debug.stage.StageEditorCommand; import funkin.util.SerializerUtil; +import funkin.data.stage.StageRegistry; import funkin.util.MouseUtil; import haxe.ui.containers.ListView; import haxe.ui.core.Component; +import funkin.graphics.FunkinSprite; import haxe.ui.events.UIEvent; import haxe.ui.RuntimeComponentBuilder; import openfl.events.Event; @@ -354,7 +356,13 @@ class StageOffsetSubState extends HaxeUISubState function prepStageStuff():String { - var stageLol:StageData = StageDataParser.parseStageData(PlayState.instance.currentStageId); + var stageLol:StageData = StageRegistry.instance.fetchEntry(PlayState.instance.currentStageId)?._data; + + if (stageLol == null) + { + FlxG.log.error("Stage not found in registry!"); + return ""; + } for (prop in stageLol.props) { @@ -378,6 +386,6 @@ class StageOffsetSubState extends HaxeUISubState stageLol.characters.gf.position[0] = Std.int(GF_FEET_SNIIIIIIIIIIIIIFFFF.x); stageLol.characters.gf.position[1] = Std.int(GF_FEET_SNIIIIIIIIIIIIIFFFF.y); - return SerializerUtil.toJSON(stageLol); + return stageLol.serialize(); } } diff --git a/source/funkin/ui/options/ControlsMenu.hx b/source/funkin/ui/options/ControlsMenu.hx index b83b54152..ea04e1208 100644 --- a/source/funkin/ui/options/ControlsMenu.hx +++ b/source/funkin/ui/options/ControlsMenu.hx @@ -8,6 +8,7 @@ import flixel.group.FlxGroup; import flixel.input.actions.FlxActionInput; import flixel.input.gamepad.FlxGamepadInputID; import flixel.input.keyboard.FlxKey; +import funkin.graphics.FunkinSprite; import funkin.input.Controls; import funkin.ui.AtlasText; import funkin.ui.MenuList; @@ -61,8 +62,8 @@ class ControlsMenu extends funkin.ui.options.OptionsState.Page if (FlxG.gamepads.numActiveGamepads > 0) { - var devicesBg:FlxSprite = new FlxSprite(); - devicesBg.makeGraphic(FlxG.width, 100, 0xFFFAFD6D); + var devicesBg:FunkinSprite = new FunkinSprite(); + devicesBg.makeSolidColor(FlxG.width, 100, 0xFFFAFD6D); add(devicesBg); deviceList = new TextMenuList(Horizontal, None); add(deviceList); diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx index d6dd536f7..a616fd46b 100644 --- a/source/funkin/ui/story/StoryMenuState.hx +++ b/source/funkin/ui/story/StoryMenuState.hx @@ -10,6 +10,7 @@ import flixel.group.FlxGroup.FlxTypedGroup; import flixel.text.FlxText; import flixel.addons.transition.FlxTransitionableState; import flixel.tweens.FlxEase; +import funkin.graphics.FunkinSprite; import funkin.ui.MusicBeatState; import flixel.tweens.FlxTween; import flixel.util.FlxColor; @@ -153,7 +154,7 @@ class StoryMenuState extends MusicBeatState updateBackground(); - var black:FlxSprite = new FlxSprite(levelBackground.x, 0).makeGraphic(FlxG.width, Std.int(400 + levelBackground.y), FlxColor.BLACK); + var black:FunkinSprite = new FunkinSprite(levelBackground.x, 0).makeSolidColor(FlxG.width, Std.int(400 + levelBackground.y), FlxColor.BLACK); black.zIndex = levelBackground.zIndex - 1; add(black); diff --git a/source/funkin/ui/title/OutdatedSubState.hx b/source/funkin/ui/title/OutdatedSubState.hx index d262fc4e4..012823541 100644 --- a/source/funkin/ui/title/OutdatedSubState.hx +++ b/source/funkin/ui/title/OutdatedSubState.hx @@ -15,7 +15,7 @@ class OutdatedSubState extends MusicBeatState override function create() { super.create(); - var bg:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.BLACK); + var bg:FunkinSprite = new FunkinSprite().makeSolidColor(FlxG.width, FlxG.height, FlxColor.BLACK); add(bg); var ver = "v" + Application.current.meta.get('version'); var txt:FlxText = new FlxText(0, 0, FlxG.width, diff --git a/source/funkin/ui/title/TitleState.hx b/source/funkin/ui/title/TitleState.hx index bc44af073..a5dcd6def 100644 --- a/source/funkin/ui/title/TitleState.hx +++ b/source/funkin/ui/title/TitleState.hx @@ -13,6 +13,7 @@ import funkin.audio.visualize.SpectogramSprite; import funkin.graphics.shaders.ColorSwap; import funkin.graphics.shaders.LeftMaskShader; import funkin.data.song.SongRegistry; +import funkin.graphics.FunkinSprite; import funkin.ui.MusicBeatState; import funkin.data.song.SongData.SongMusicData; import funkin.graphics.shaders.TitleOutline; @@ -118,7 +119,8 @@ class TitleState extends MusicBeatState persistentUpdate = true; - var bg:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.BLACK); + var bg:FunkinSprite = new FunkinSprite().makeSolidColor(FlxG.width, FlxG.height, FlxColor.BLACK); + bg.screenCenter(); add(bg); logoBl = new FlxSprite(-150, -100); diff --git a/source/funkin/ui/transition/LoadingState.hx b/source/funkin/ui/transition/LoadingState.hx index a223a4123..da9aeb28b 100644 --- a/source/funkin/ui/transition/LoadingState.hx +++ b/source/funkin/ui/transition/LoadingState.hx @@ -13,6 +13,7 @@ import funkin.play.song.Song.SongDifficulty; import funkin.ui.mainmenu.MainMenuState; import funkin.ui.MusicBeatState; import haxe.io.Path; +import funkin.graphics.FunkinSprite; import lime.app.Future; import lime.app.Promise; import lime.utils.AssetLibrary; @@ -42,7 +43,7 @@ class LoadingState extends MusicBeatState override function create():Void { - var bg:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, 0xFFcaff4d); + var bg:FlxSprite = new FunkinSprite().makeSolidColor(FlxG.width, FlxG.height, 0xFFcaff4d); add(bg); funkay = new FlxSprite(); @@ -53,7 +54,7 @@ class LoadingState extends MusicBeatState funkay.scrollFactor.set(); funkay.screenCenter(); - loadBar = new FlxSprite(0, FlxG.height - 20).makeGraphic(FlxG.width, 10, 0xFFff16d2); + loadBar = new FunkinSprite(0, FlxG.height - 20).makeSolidColor(FlxG.width, 10, 0xFFff16d2); loadBar.screenCenter(X); add(loadBar); From cd9035da199f3b114b84745785574ffb902e25fd Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Tue, 16 Jan 2024 17:08:41 -0500 Subject: [PATCH 03/18] Update assets --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index 9b9baa6f2..9e385784b 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 9b9baa6f2b38dc9bd3354b350084f548e1e16c0f +Subproject commit 9e385784b1d2f4332de0d696b1df655cfa269da0 From 19cd7da8ee39ec3b80702d154fcfcc87fa178802 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Wed, 17 Jan 2024 01:01:13 -0500 Subject: [PATCH 04/18] Fix an FlxDrawItems crash tied to FlxAnimate --- hmm.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hmm.json b/hmm.json index d93ea0658..7e17f105c 100644 --- a/hmm.json +++ b/hmm.json @@ -11,7 +11,7 @@ "name": "flixel", "type": "git", "dir": null, - "ref": "a83738673e7edbf8acba3a1426af284dfe6719fe", + "ref": "07c6018008801972d12275690fc144fcc22e3de6", "url": "https://github.com/FunkinCrew/flixel" }, { @@ -37,7 +37,7 @@ "name": "flxanimate", "type": "git", "dir": null, - "ref": "d7c5621be742e2c98d523dfe5af7528835eaff1e", + "ref": "9bacdd6ea39f5e3a33b0f5dfb7bc583fe76060d4", "url": "https://github.com/FunkinCrew/flxanimate" }, { From e44b028946e5d98c5798ec5aee0502c54fbac968 Mon Sep 17 00:00:00 2001 From: Jenny Crowe Date: Wed, 17 Jan 2024 16:24:03 -0700 Subject: [PATCH 05/18] Added side sliders that alter the volume of vocals and hitsounds on player/opponent sides. --- assets | 2 +- .../ui/debug/charting/ChartEditorState.hx | 88 ++++++++++++++++++- 2 files changed, 85 insertions(+), 5 deletions(-) diff --git a/assets b/assets index d094640f7..0e9019f0f 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit d094640f727a670a348b3579d11af5ff6a2ada3a +Subproject commit 0e9019f0fcb53f3e554604ea9a4e62d381873d1f diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index e98809ce8..5fa5308e2 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -104,6 +104,7 @@ import haxe.ui.components.Label; import haxe.ui.components.Button; import haxe.ui.components.NumberStepper; import haxe.ui.components.Slider; +import haxe.ui.components.VerticalSlider; import haxe.ui.components.TextField; import haxe.ui.containers.dialogs.CollapsibleDialog; import haxe.ui.containers.Frame; @@ -720,6 +721,34 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState return hitsoundsEnabledPlayer || hitsoundsEnabledOpponent; } + /** + * Sound multiplier for vocals and hitsounds on the player's side. + */ + var soundMultiplierPlayer(default, set):Float = 1.0; + + function set_soundMultiplierPlayer(value:Float):Float + { + soundMultiplierPlayer = value; + var vocalTargetVolume:Float = (menubarItemVolumeVocals.value ?? 100.0) / 100.0; + if (audioVocalTrackGroup != null) audioVocalTrackGroup.playerVolume = vocalTargetVolume * soundMultiplierPlayer; + + return soundMultiplierPlayer; + } + + /** + * Sound multiplier for vocals and hitsounds on the opponent's side. + */ + var soundMultiplierOpponent(default, set):Float = 1.0; + + function set_soundMultiplierOpponent(value:Float):Float + { + soundMultiplierOpponent = value; + var vocalTargetVolume:Float = (menubarItemVolumeVocals.value ?? 100.0) / 100.0; + if (audioVocalTrackGroup != null) audioVocalTrackGroup.opponentVolume = vocalTargetVolume * soundMultiplierOpponent; + + return soundMultiplierOpponent; + } + // Auto-save /** @@ -1749,6 +1778,18 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ var buttonSelectEvent:Button; + /** + * The slider above the grid that sets the volume of the player's sounds. + * Constructed manually and added to the layout so we can control its position. + */ + var sliderVolumePlayer:Slider; + + /** + * The slider above the grid that sets the volume of the opponent's sounds. + * Constructed manually and added to the layout so we can control its position. + */ + var sliderVolumeOpponent:Slider; + /** * RENDER OBJECTS */ @@ -2557,6 +2598,37 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState performCommand(new SetItemSelectionCommand([], currentSongChartEventData)); } } + + function setupSideSlider(x, y):VerticalSlider + { + var slider = new VerticalSlider(); + slider.allowFocus = false; + slider.x = x; + slider.y = y; + slider.width = NOTE_SELECT_BUTTON_HEIGHT; + slider.height = GRID_SIZE * 4; + slider.pos = slider.max; + slider.tooltip = "Slide to set the volume of sounds on this side."; + slider.zIndex = 110; + slider.styleNames = "sideSlider"; + add(slider); + + return slider; + } + + var sliderY = GRID_INITIAL_Y_POS + 34; + sliderVolumeOpponent = setupSideSlider(GRID_X_POS - 64, sliderY); + sliderVolumePlayer = setupSideSlider(buttonSelectEvent.x + buttonSelectEvent.width, sliderY); + + sliderVolumePlayer.onChange = event -> { + var volume:Float = event.value.toFloat() / 100.0; + soundMultiplierPlayer = volume; + } + + sliderVolumeOpponent.onChange = event -> { + var volume:Float = event.value.toFloat() / 100.0; + soundMultiplierOpponent = volume; + } } /** @@ -2797,7 +2869,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState menubarItemVolumeVocals.onChange = event -> { var volume:Float = event.value.toFloat() / 100.0; - if (audioVocalTrackGroup != null) audioVocalTrackGroup.volume = volume; + if (audioVocalTrackGroup != null) + { + audioVocalTrackGroup.playerVolume = volume * soundMultiplierPlayer; + audioVocalTrackGroup.opponentVolume = volume * soundMultiplierOpponent; + } menubarLabelVolumeVocals.text = 'Voices - ${Std.int(event.value)}%'; } @@ -5662,7 +5738,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState audioInstTrack.volume = instTargetVolume; audioInstTrack.onComplete = null; } - if (audioVocalTrackGroup != null) audioVocalTrackGroup.volume = vocalTargetVolume; + if (audioVocalTrackGroup != null) + { + audioVocalTrackGroup.playerVolume = vocalTargetVolume * soundMultiplierPlayer; + audioVocalTrackGroup.opponentVolume = vocalTargetVolume * soundMultiplierOpponent; + } } function updateTimeSignature():Void @@ -5864,9 +5944,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState switch (noteData.getStrumlineIndex()) { case 0: // Player - if (hitsoundsEnabledPlayer) this.playSound(Paths.sound('chartingSounds/hitNotePlayer'), hitsoundVolume); + if (hitsoundsEnabledPlayer) this.playSound(Paths.sound('chartingSounds/hitNotePlayer'), hitsoundVolume * soundMultiplierPlayer); case 1: // Opponent - if (hitsoundsEnabledOpponent) this.playSound(Paths.sound('chartingSounds/hitNoteOpponent'), hitsoundVolume); + if (hitsoundsEnabledOpponent) this.playSound(Paths.sound('chartingSounds/hitNoteOpponent'), hitsoundVolume * soundMultiplierOpponent); } } } From 027c2843f4958158e5ae74deca7499c2160ac91f Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Wed, 17 Jan 2024 22:19:40 -0500 Subject: [PATCH 06/18] This bug took me like 4-5 hours of staring at code to fix i am going crazy graaaa --- .../ui/debug/charting/ChartEditorState.hx | 25 ++++++++++--------- .../handlers/ChartEditorThemeHandler.hx | 6 +++++ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index e98809ce8..5e8f112dd 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -1958,7 +1958,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState buildGrid(); buildMeasureTicks(); buildNotePreview(); - buildSelectionBox(); buildAdditionalUI(); populateOpenRecentMenu(); @@ -2287,17 +2286,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState setNotePreviewViewportBounds(calculateNotePreviewViewportBounds()); } - function buildSelectionBox():Void - { - if (selectionBoxSprite == null) throw 'ERROR: Tried to build selection box, but selectionBoxSprite is null! Check ChartEditorThemeHandler.updateTheme().'; - - selectionBoxSprite.scrollFactor.set(0, 0); - add(selectionBoxSprite); - selectionBoxSprite.zIndex = 30; - - setSelectionBoxBounds(); - } - function setSelectionBoxBounds(bounds:FlxRect = null):Void { if (selectionBoxSprite == null) @@ -2319,6 +2307,19 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState } } + /** + * Automatically goes through and calls render on everything you added. + */ + override public function draw():Void + { + if (selectionBoxStartPos != null) + { + trace('selectionBoxSprite: ${selectionBoxSprite.visible} ${selectionBoxSprite.exists} ${this.members.contains(selectionBoxSprite)}'); + } + + super.draw(); + } + function calculateNotePreviewViewportBounds():FlxRect { var bounds:FlxRect = new FlxRect(); diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx index 98bb5c2c8..89fd4d5d3 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx @@ -317,6 +317,12 @@ class ChartEditorThemeHandler ChartEditorState.GRID_SIZE - (2 * SELECTION_SQUARE_BORDER_WIDTH + 8)), 32, 32); + + state.selectionBoxSprite.scrollFactor.set(0, 0); + state.selectionBoxSprite.zIndex = 30; + state.add(state.selectionBoxSprite); + + state.setSelectionBoxBounds(); } static function updateNotePreview(state:ChartEditorState):Void From a0c4499b03bb7c14fc2bf9bb0d78313617a54c28 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Wed, 17 Jan 2024 23:12:59 -0500 Subject: [PATCH 07/18] Fix a bug where replaying a level makes a pink screen --- source/funkin/play/PlayState.hx | 1 + 1 file changed, 1 insertion(+) diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 20f19f714..cc9debf13 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -1354,6 +1354,7 @@ class PlayState extends MusicBeatSubState function loadStage(id:String):Void { currentStage = StageRegistry.instance.fetchEntry(id); + currentStage.revive(); // Stages are killed and props destroyed when the PlayState is destroyed to save memory. if (currentStage != null) { From 26b761066303aa0fb1ab2e71689235ce9456baaa Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Wed, 17 Jan 2024 23:14:28 -0500 Subject: [PATCH 08/18] Fix an error with playable Pico death --- assets | 2 +- source/funkin/play/GameOverSubState.hx | 24 +++++++++++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/assets b/assets index 9e385784b..d6be0e084 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 9e385784b1d2f4332de0d696b1df655cfa269da0 +Subproject commit d6be0e084e4fda0416eca9ec7fe406af9b626e5c diff --git a/source/funkin/play/GameOverSubState.hx b/source/funkin/play/GameOverSubState.hx index 88d9be7d4..36f72237e 100644 --- a/source/funkin/play/GameOverSubState.hx +++ b/source/funkin/play/GameOverSubState.hx @@ -23,6 +23,12 @@ import funkin.play.character.BaseCharacter; */ class GameOverSubState extends MusicBeatSubState { + /** + * The currently active GameOverSubState. + * There should be only one GameOverSubState in existance at a time, we can use a singleton. + */ + public static var instance:GameOverSubState = null; + /** * Which alternate animation on the character to use. * You can set this via script. @@ -88,6 +94,13 @@ class GameOverSubState extends MusicBeatSubState override public function create() { + if (instance != null) + { + // TODO: Do something in this case? IDK. + trace('WARNING: GameOverSubState instance already exists. This should not happen.'); + } + instance = this; + super.create(); // @@ -283,10 +296,10 @@ class GameOverSubState extends MusicBeatSubState */ function startDeathMusic(?startingVolume:Float = 1, force:Bool = false):Void { - var musicPath = Paths.music('gameOver' + musicSuffix); + var musicPath = Paths.music('gameplay/gameover/gameOver' + musicSuffix); if (isEnding) { - musicPath = Paths.music('gameOverEnd' + musicSuffix); + musicPath = Paths.music('gameplay/gameover/gameOverEnd' + musicSuffix); } if (!gameOverMusic.playing || force) { @@ -306,7 +319,7 @@ class GameOverSubState extends MusicBeatSubState public static function playBlueBalledSFX() { blueballed = true; - FlxG.sound.play(Paths.sound('fnf_loss_sfx' + blueBallSuffix)); + FlxG.sound.play(Paths.sound('gameplay/gameover/fnf_loss_sfx' + blueBallSuffix)); } var playingJeffQuote:Bool = false; @@ -329,6 +342,11 @@ class GameOverSubState extends MusicBeatSubState } }); } + + public override function toString():String + { + return "GameOverSubState"; + } } typedef GameOverParams = From 25fe2c0d39ea0c3bf371fee23c5f3b4bee5ab53b Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Wed, 17 Jan 2024 23:53:23 -0500 Subject: [PATCH 09/18] Rewrite Upload Vocals dialog. --- assets | 2 +- .../dialogs/ChartEditorUploadChartDialog.hx | 1 + .../dialogs/ChartEditorUploadVocalsDialog.hx | 311 ++++++++++++++++++ .../handlers/ChartEditorDialogHandler.hx | 223 +++---------- 4 files changed, 364 insertions(+), 173 deletions(-) create mode 100644 source/funkin/ui/debug/charting/dialogs/ChartEditorUploadVocalsDialog.hx diff --git a/assets b/assets index 9e385784b..d0e0c9f94 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 9e385784b1d2f4332de0d696b1df655cfa269da0 +Subproject commit d0e0c9f94961793a815f66c9a2875c99ef3b4c8c diff --git a/source/funkin/ui/debug/charting/dialogs/ChartEditorUploadChartDialog.hx b/source/funkin/ui/debug/charting/dialogs/ChartEditorUploadChartDialog.hx index 5b84148c6..17f047106 100644 --- a/source/funkin/ui/debug/charting/dialogs/ChartEditorUploadChartDialog.hx +++ b/source/funkin/ui/debug/charting/dialogs/ChartEditorUploadChartDialog.hx @@ -13,6 +13,7 @@ import haxe.ui.notifications.NotificationType; // @:nullSafety // TODO: Fix null safety when used with HaxeUI build macros. @:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/dialogs/upload-chart.xml")) +@:access(funkin.ui.debug.charting.ChartEditorState) class ChartEditorUploadChartDialog extends ChartEditorBaseDialog { var dropHandlers:Array = []; diff --git a/source/funkin/ui/debug/charting/dialogs/ChartEditorUploadVocalsDialog.hx b/source/funkin/ui/debug/charting/dialogs/ChartEditorUploadVocalsDialog.hx new file mode 100644 index 000000000..537c7c36e --- /dev/null +++ b/source/funkin/ui/debug/charting/dialogs/ChartEditorUploadVocalsDialog.hx @@ -0,0 +1,311 @@ +package funkin.ui.debug.charting.dialogs; + +import funkin.input.Cursor; +import funkin.ui.debug.charting.dialogs.ChartEditorBaseDialog.DialogDropTarget; +import funkin.ui.debug.charting.dialogs.ChartEditorBaseDialog.DialogParams; +import funkin.util.FileUtil; +import funkin.play.character.CharacterData; +import haxe.io.Path; +import haxe.ui.components.Button; +import haxe.ui.components.Label; +import haxe.ui.containers.dialogs.Dialog.DialogButton; +import haxe.ui.containers.dialogs.Dialog.DialogEvent; +import haxe.ui.containers.Box; +import haxe.ui.containers.dialogs.Dialogs; +import haxe.ui.core.Component; +import haxe.ui.notifications.NotificationManager; +import haxe.ui.notifications.NotificationType; + +// @:nullSafety // TODO: Fix null safety when used with HaxeUI build macros. + +@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/dialogs/upload-vocals.xml")) +@:access(funkin.ui.debug.charting.ChartEditorState) +class ChartEditorUploadVocalsDialog extends ChartEditorBaseDialog +{ + var dropHandlers:Array = []; + + var vocalContainer:Component; + var dialogCancel:Button; + var dialogNoVocals:Button; + var dialogContinue:Button; + + var charIds:Array; + var instId:String; + var hasClearedVocals:Bool = false; + + public function new(state2:ChartEditorState, charIds:Array, params2:DialogParams) + { + super(state2, params2); + + this.charIds = charIds; + this.instId = chartEditorState.currentInstrumentalId; + + dialogCancel.onClick = function(_) { + hideDialog(DialogButton.CANCEL); + } + + dialogNoVocals.onClick = function(_) { + // Dismiss + chartEditorState.wipeVocalData(); + hideDialog(DialogButton.APPLY); + }; + + dialogContinue.onClick = function(_) { + // Dismiss + hideDialog(DialogButton.APPLY); + }; + + buildDropHandlers(); + } + + function buildDropHandlers():Void + { + for (charKey in charIds) + { + trace('Adding vocal upload for character ${charKey}'); + + var charMetadata:Null = CharacterDataParser.fetchCharacterData(charKey); + var charName:String = charMetadata?.name ?? charKey; + + var vocalsEntry = new ChartEditorUploadVocalsEntry(charName); + + var dropHandler:DialogDropTarget = {component: vocalsEntry, handler: null}; + + var onDropFile:String->Void = function(pathStr:String) { + trace('Selected file: $pathStr'); + var path:Path = new Path(pathStr); + + if (chartEditorState.loadVocalsFromPath(path, charKey, this.instId, !this.hasClearedVocals)) + { + this.hasClearedVocals = true; + // Tell the user the load was successful. + chartEditorState.success('Loaded Vocals', 'Loaded vocals for $charName (${path.file}.${path.ext}), variation ${chartEditorState.selectedVariation}'); + #if FILE_DROP_SUPPORTED + vocalsEntry.vocalsEntryLabel.text = 'Voices for $charName (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}'; + #else + vocalsEntry.vocalsEntryLabel.text = 'Voices for $charName (click to browse)\n${path.file}.${path.ext}'; + #end + + dialogNoVocals.hidden = true; + chartEditorState.removeDropHandler(dropHandler); + } + else + { + trace('Failed to load vocal track (${path.file}.${path.ext})'); + + chartEditorState.error('Failed to Load Vocals', + 'Failed to load vocal track (${path.file}.${path.ext}) for variation (${chartEditorState.selectedVariation})'); + + #if FILE_DROP_SUPPORTED + vocalsEntry.vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.'; + #else + vocalsEntry.vocalsEntryLabel.text = 'Click to browse for vocals for $charName.'; + #end + } + }; + + vocalsEntry.onClick = function(_event) { + Dialogs.openBinaryFile('Open $charName Vocals', [ + {label: 'Audio File (.ogg)', extension: 'ogg'}], function(selectedFile) { + if (selectedFile != null && selectedFile.bytes != null) + { + trace('Selected file: ' + selectedFile.name); + + if (chartEditorState.loadVocalsFromBytes(selectedFile.bytes, charKey, this.instId, !this.hasClearedVocals)) + { + hasClearedVocals = true; + // Tell the user the load was successful. + chartEditorState.success('Loaded Vocals', + 'Loaded vocals for $charName (${selectedFile.name}), variation ${chartEditorState.selectedVariation}'); + + #if FILE_DROP_SUPPORTED + vocalsEntry.vocalsEntryLabel.text = 'Voices for $charName (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}'; + #else + vocalsEntry.vocalsEntryLabel.text = 'Voices for $charName (click to browse)\n${selectedFile.name}'; + #end + + dialogNoVocals.hidden = true; + } + else + { + trace('Failed to load vocal track (${selectedFile.fullPath})'); + + chartEditorState.error('Failed to Load Vocals', + 'Failed to load vocal track (${selectedFile.name}) for variation (${chartEditorState.selectedVariation})'); + + #if FILE_DROP_SUPPORTED + vocalsEntry.vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.'; + #else + vocalsEntry.vocalsEntryLabel.text = 'Click to browse for vocals for $charName.'; + #end + } + } + }); + } + + dropHandler.handler = onDropFile; + + // onDropFile + #if FILE_DROP_SUPPORTED + dropHandlers.push(dropHandler); + #end + + vocalContainer.addComponent(vocalsEntry); + } + } + + public static function build(state:ChartEditorState, charIds:Array, ?closable:Bool, ?modal:Bool):ChartEditorUploadVocalsDialog + { + var dialog = new ChartEditorUploadVocalsDialog(state, charIds, + { + closable: closable ?? false, + modal: modal ?? true + }); + + for (dropTarget in dialog.dropHandlers) + { + state.addDropHandler(dropTarget); + } + + dialog.showDialog(modal ?? true); + + return dialog; + } + + public override function onClose(event:DialogEvent):Void + { + super.onClose(event); + + if (event.button != DialogButton.APPLY && !this.closable) + { + // User cancelled the wizard! Back to the welcome dialog. + chartEditorState.openWelcomeDialog(this.closable); + } + + for (dropTarget in dropHandlers) + { + chartEditorState.removeDropHandler(dropTarget); + } + } + + public override function lock():Void + { + super.lock(); + this.dialogCancel.disabled = true; + } + + public override function unlock():Void + { + super.unlock(); + this.dialogCancel.disabled = false; + } + + /** + * Called when clicking the Upload Chart box. + */ + public function onClickChartBox():Void + { + if (this.locked) return; + + this.lock(); + // TODO / BUG: File filtering not working on mac finder dialog, so we don't use it for now + #if !mac + FileUtil.browseForBinaryFile('Open Chart', [FileUtil.FILE_EXTENSION_INFO_FNFC], onSelectFile, onCancelBrowse); + #else + FileUtil.browseForBinaryFile('Open Chart', null, onSelectFile, onCancelBrowse); + #end + } + + /** + * Called when a file is selected by dropping a file onto the Upload Chart box. + */ + function onDropFileChartBox(pathStr:String):Void + { + var path:Path = new Path(pathStr); + trace('Dropped file (${path})'); + + try + { + var result:Null> = ChartEditorImportExportHandler.loadFromFNFCPath(chartEditorState, path.toString()); + if (result != null) + { + chartEditorState.success('Loaded Chart', + result.length == 0 ? 'Loaded chart (${path.toString()})' : 'Loaded chart (${path.toString()})\n${result.join("\n")}'); + this.hideDialog(DialogButton.APPLY); + } + else + { + chartEditorState.failure('Failed to Load Chart', 'Failed to load chart (${path.toString()})'); + } + } + catch (err) + { + chartEditorState.failure('Failed to Load Chart', 'Failed to load chart (${path.toString()}): ${err}'); + } + } + + /** + * Called when a file is selected by the dialog displayed when clicking the Upload Chart box. + */ + function onSelectFile(selectedFile:SelectedFileInfo):Void + { + this.unlock(); + + if (selectedFile != null && selectedFile.bytes != null) + { + try + { + var result:Null> = ChartEditorImportExportHandler.loadFromFNFC(chartEditorState, selectedFile.bytes); + if (result != null) + { + chartEditorState.success('Loaded Chart', + result.length == 0 ? 'Loaded chart (${selectedFile.name})' : 'Loaded chart (${selectedFile.name})\n${result.join("\n")}'); + + if (selectedFile.fullPath != null) chartEditorState.currentWorkingFilePath = selectedFile.fullPath; + this.hideDialog(DialogButton.APPLY); + } + } + catch (err) + { + chartEditorState.failure('Failed to Load Chart', 'Failed to load chart (${selectedFile.name}): ${err}'); + } + } + } + + function onCancelBrowse():Void + { + this.unlock(); + } +} + +@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/dialogs/upload-vocals-entry.xml")) +class ChartEditorUploadVocalsEntry extends Box +{ + public var vocalsEntryLabel:Label; + + var charName:String; + + public function new(charName:String) + { + super(); + + this.charName = charName; + + #if FILE_DROP_SUPPORTED + vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.'; + #else + vocalsEntryLabel.text = 'Click to browse for vocals for $charName.'; + #end + + this.onMouseOver = function(_event) { + // if (this.locked) return; + this.swapClass('upload-bg', 'upload-bg-hover'); + Cursor.cursorMode = Pointer; + } + + this.onMouseOut = function(_event) { + this.swapClass('upload-bg-hover', 'upload-bg'); + Cursor.cursorMode = Default; + } + } +} diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx index 32e9fef53..970f021ac 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx @@ -19,6 +19,7 @@ import funkin.ui.debug.charting.dialogs.ChartEditorBaseDialog.DialogDropTarget; import funkin.ui.debug.charting.dialogs.ChartEditorCharacterIconSelectorMenu; import funkin.ui.debug.charting.dialogs.ChartEditorUploadChartDialog; import funkin.ui.debug.charting.dialogs.ChartEditorWelcomeDialog; +import funkin.ui.debug.charting.dialogs.ChartEditorUploadVocalsDialog; import funkin.ui.debug.charting.util.ChartEditorDropdowns; import funkin.util.Constants; import funkin.util.DateUtil; @@ -59,11 +60,8 @@ using Lambda; class ChartEditorDialogHandler { // Paths to HaxeUI layout files for each dialog. - static final CHART_EDITOR_DIALOG_UPLOAD_CHART_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-chart'); static final CHART_EDITOR_DIALOG_UPLOAD_INST_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-inst'); static final CHART_EDITOR_DIALOG_SONG_METADATA_LAYOUT:String = Paths.ui('chart-editor/dialogs/song-metadata'); - static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-vocals'); - static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_ENTRY_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-vocals-entry'); static final CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart-parts'); static final CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_ENTRY_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart-parts-entry'); static final CHART_EDITOR_DIALOG_IMPORT_CHART_LAYOUT:String = Paths.ui('chart-editor/dialogs/import-chart'); @@ -105,6 +103,56 @@ class ChartEditorDialogHandler return dialog; } + /** + * Builds and opens a dialog letting the user browse for a chart file to open. + * @param state The current chart editor state. + * @param closable Whether the dialog can be closed by the user. + * @return The dialog that was opened. + */ + public static function openBrowseFNFC(state:ChartEditorState, closable:Bool):Null + { + var dialog = ChartEditorUploadChartDialog.build(state, closable); + + dialog.zIndex = 1000; + state.isHaxeUIDialogOpen = true; + + return dialog; + } + + /** + * Builds and opens a dialog where the user uploads vocals for the current song. + * @param state The current chart editor state. + * @param closable Whether the dialog can be closed by the user. + * @return The dialog that was opened. + */ + public static function openUploadVocalsDialog(state:ChartEditorState, closable:Bool = true):Dialog + { + var charData:SongCharacterData = state.currentSongMetadata.playData.characters; + + var hasClearedVocals:Bool = false; + + var charIdsForVocals:Array = [charData.player, charData.opponent]; + + var dialog = ChartEditorUploadVocalsDialog.build(state, charIdsForVocals, closable); + + dialog.zIndex = 1000; + state.isHaxeUIDialogOpen = true; + + return dialog; + } + + /** + * Builds and opens the dialog for selecting a character. + */ + public static function openCharacterDropdown(state:ChartEditorState, charType:CharacterType, lockPosition:Bool = false):Null + { + var menu = ChartEditorCharacterIconSelectorMenu.build(state, charType, lockPosition); + + menu.zIndex = 1000; + + return menu; + } + /** * Builds and opens a dialog letting the user know a backup is available, and prompting them to load it. */ @@ -186,22 +234,6 @@ class ChartEditorDialogHandler return dialog; } - /** - * Builds and opens a dialog letting the user browse for a chart file to open. - * @param state The current chart editor state. - * @param closable Whether the dialog can be closed by the user. - * @return The dialog that was opened. - */ - public static function openBrowseFNFC(state:ChartEditorState, closable:Bool):Null - { - var dialog = ChartEditorUploadChartDialog.build(state, closable); - - dialog.zIndex = 1000; - state.isHaxeUIDialogOpen = true; - - return dialog; - } - /** * Open the wizard for opening an existing chart from individual files. * @param state @@ -288,15 +320,6 @@ class ChartEditorDialogHandler }; } - public static function openCharacterDropdown(state:ChartEditorState, charType:CharacterType, lockPosition:Bool = false):Null - { - var menu = ChartEditorCharacterIconSelectorMenu.build(state, charType, lockPosition); - - menu.zIndex = 1000; - - return menu; - } - public static function openCreateSongWizardBasicOnly(state:ChartEditorState, closable:Bool):Void { // Step 1. Song Metadata @@ -699,150 +722,6 @@ class ChartEditorDialogHandler return dialog; } - /** - * Builds and opens a dialog where the user uploads vocals for the current song. - * @param state The current chart editor state. - * @param closable Whether the dialog can be closed by the user. - * @return The dialog that was opened. - */ - public static function openUploadVocalsDialog(state:ChartEditorState, closable:Bool = true):Dialog - { - var instId:String = state.currentInstrumentalId; - var charIdsForVocals:Array = []; - - var charData:SongCharacterData = state.currentSongMetadata.playData.characters; - - var hasClearedVocals:Bool = false; - - charIdsForVocals.push(charData.player); - charIdsForVocals.push(charData.opponent); - - var dialog:Null = openDialog(state, CHART_EDITOR_DIALOG_UPLOAD_VOCALS_LAYOUT, true, closable); - if (dialog == null) throw 'Could not locate Upload Vocals dialog'; - - var dialogContainer:Null = dialog.findComponent('vocalContainer'); - if (dialogContainer == null) throw 'Could not locate vocalContainer in Upload Vocals dialog'; - - var buttonCancel:Null