From f5046cece6b5de399f280a80957783cf401ef215 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 8 Jun 2023 16:25:32 -0400 Subject: [PATCH 01/14] Modified UI layouts, and performed XML formatting cleanup. --- .vscode/settings.json | 192 +++++++++++++++++++++++++----------------- 1 file changed, 114 insertions(+), 78 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index d7433698d..dd4cd7aef 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,84 +1,120 @@ { - "[haxe]": { - // Automatically keep Haxe files formatted. - "editor.formatOnSave": true, - "editor.formatOnPaste": true, - "editor.codeActionsOnSave": { - // Compilation server issues can cause auto-cleanup to remove valid imports. - "source.organizeImports": false - }, - "editor.defaultFormatter": "nadako.vshaxe", - "editor.tabSize": 2 + "[haxe]": { + // Automatically keep Haxe files formatted. + "editor.formatOnSave": true, + "editor.formatOnPaste": true, + "editor.codeActionsOnSave": { + // Compilation server issues can cause auto-cleanup to remove valid imports. + "source.organizeImports": false }, - - "[json]": { - // Automatically keep JSON files formatted. - "editor.formatOnSave": true, - "editor.formatOnPaste": true, - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "nadako.vshaxe", + "editor.tabSize": 2 + }, + + "[json]": { + // Automatically keep JSON files formatted. + "editor.formatOnSave": true, + "editor.formatOnPaste": true, + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + + "[jsonc]": { + // Automatically keep JSONC files formatted. + "editor.formatOnSave": true, + "editor.formatOnPaste": true, + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "prettier.tabWidth": 2, + + // XML formatting style configuration + "xml.format.enabled": true, + "xml.format.legacy": false, + "xml.format.emptyElements": "collapse", + "xml.preferences.quoteStyle": "double", + "xml.format.enforceQuoteStyle": "preferred", + "xml.format.preserveAttributeLineBreaks": false, + "xml.format.preservedNewlines": 0, + "xml.format.splitAttributes": false, + "xml.format.joinCDATALines": true, + "xml.format.preserveEmptyContent": false, + "xml.format.joinCommentLines": false, + "xml.format.joinContentLines": false, + "xml.format.spaceBeforeEmptyCloseTag": true, + "xml.format.xsiSchemaLocationSplit": "onPair", + "xml.format.splitAttributesIndentSize": 2, + "xml.format.closingBracketNewLine": false, + "xml.format.preserveSpace": [ + "xsl:text", + "xsl:comment", + "xsl:processing-instruction", + "literallayout", + "programlisting", + "screen", + "synopsis", + "pre", + "xd:pre" + ], + "xml.format.maxLineWidth": 0, + "xml.format.grammarAwareFormatting": true, + + // Generic file formatting style configuration + "files.insertFinalNewline": true, + "files.trimFinalNewlines": false, + "files.trimTrailingWhitespace": true, + + // Automatically detect indentation. + "editor.detectIndentation": true, + "editor.insertSpaces": true, + "editor.tabSize": 2, + + // Automatically enforce Linux style line endings. + "files.eol": "\n", + + "haxe.displayPort": "auto", + "haxe.enableCompilationServer": true, + "haxe.displayServer": { + "arguments": ["-v"] + }, + // Fix file associations for HScript. + "files.associations": { + "*.hxp": "haxe", + "*.hscript": "haxe", + "*.haxe": "haxe", + "*.hxs": "haxe", + "*.hxc": "haxe" + }, + "projectManager.git.baseFolders": ["./"], + + "haxecheckstyle.sourceFolders": ["src", "Source"], + "haxecheckstyle.externalSourceRoots": [], + "haxecheckstyle.configurationFile": "checkstyle.json", + "haxecheckstyle.codeSimilarityBufferSize": 100, + + "lime.targetConfigurations": [ + { + "label": "Windows / Debug", + "target": "windows", + "args": ["-debug"] }, - - "[jsonc]": { - // Automatically keep JSONC files formatted. - "editor.formatOnSave": true, - "editor.formatOnPaste": true, - "editor.defaultFormatter": "esbenp.prettier-vscode" + { + "label": "Windows / Debug (DEBUG ASSETS)", + "target": "windows", + "args": ["-debug", "-DDEBUG_ASSETS"] }, - "prettier.tabWidth": 2, - - // Automatically detect indentation. - "editor.detectIndentation": true, - "editor.insertSpaces": true, - "editor.tabSize": 2, - - // Automatically enforce Linux style line endings. - "files.eol": "\n", - - "haxe.displayPort": "auto", - "haxe.enableCompilationServer": true, - "haxe.displayServer": { - "arguments": ["-v"] + { + "label": "Windows / Debug (ANIMATE)", + "target": "windows", + "args": ["-debug", "-DANIMATE"] }, - // Fix file associations for HScript. - "files.associations": { - "*.hxp": "haxe", - "*.hscript": "haxe", - "*.haxe": "haxe", - "*.hxs": "haxe", - "*.hxc": "haxe" + { + "label": "HTML5 / Debug", + "target": "html5", + "args": ["-debug"] }, - "projectManager.git.baseFolders": ["./"], - - "haxecheckstyle.sourceFolders": ["src", "Source"], - "haxecheckstyle.externalSourceRoots": [], - "haxecheckstyle.configurationFile": "checkstyle.json", - "haxecheckstyle.codeSimilarityBufferSize": 100, - - "lime.targetConfigurations": [ - { - "label": "Windows / Debug", - "target": "windows", - "args": ["-debug"] - }, - { - "label": "Windows / Debug (DEBUG ASSETS)", - "target": "windows", - "args": ["-debug", "-DDEBUG_ASSETS"] - }, - { - "label": "Windows / Debug (ANIMATE)", - "target": "windows", - "args": ["-debug", "-DANIMATE"] - }, - { - "label": "HTML5 / Debug", - "target": "html5", - "args": ["-debug"] - }, - { - "label": "HTML5 / Debug (Watch)", - "target": "html5", - "args": ["-debug", "-watch"] - } - ] - } \ No newline at end of file + { + "label": "HTML5 / Debug (Watch)", + "target": "html5", + "args": ["-debug", "-watch"] + } + ] +} From 657d898090ebac4a3c19a705caa6349fea276d97 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 8 Jun 2023 16:42:02 -0400 Subject: [PATCH 02/14] Additional file read/write functions --- source/funkin/util/FileUtil.hx | 176 ++++++++++++++++++++++++--------- 1 file changed, 128 insertions(+), 48 deletions(-) diff --git a/source/funkin/util/FileUtil.hx b/source/funkin/util/FileUtil.hx index c30eacba8..3494e620b 100644 --- a/source/funkin/util/FileUtil.hx +++ b/source/funkin/util/FileUtil.hx @@ -18,7 +18,7 @@ class FileUtil /** * Browses for a single file, then calls `onSelect(path)` when a path chosen. * Note that on HTML5 this will immediately fail, you should call `openFile(onOpen:Resource->Void)` instead. - * + * * @param typeFilter Filters what kinds of files can be selected. * @return Whether the file dialog was opened successfully. */ @@ -26,9 +26,9 @@ class FileUtil ?dialogTitle:String):Bool { #if desktop - var filter = convertTypeFilter(typeFilter); + var filter:String = convertTypeFilter(typeFilter); - var fileDialog = new FileDialog(); + var fileDialog:FileDialog = new FileDialog(); if (onSelect != null) fileDialog.onSelect.add(onSelect); if (onCancel != null) fileDialog.onCancel.add(onCancel); @@ -46,7 +46,7 @@ class FileUtil /** * Browses for a directory, then calls `onSelect(path)` when a path chosen. * Note that on HTML5 this will immediately fail. - * + * * @param typeFilter TODO What does this do? * @return Whether the file dialog was opened successfully. */ @@ -54,9 +54,9 @@ class FileUtil ?dialogTitle:String):Bool { #if desktop - var filter = convertTypeFilter(typeFilter); + var filter:String = convertTypeFilter(typeFilter); - var fileDialog = new FileDialog(); + var fileDialog:FileDialog = new FileDialog(); if (onSelect != null) fileDialog.onSelect.add(onSelect); if (onCancel != null) fileDialog.onCancel.add(onCancel); @@ -74,16 +74,16 @@ class FileUtil /** * Browses for multiple file, then calls `onSelect(paths)` when a path chosen. * Note that on HTML5 this will immediately fail. - * + * * @return Whether the file dialog was opened successfully. */ public static function browseForMultipleFiles(?typeFilter:Array, ?onSelect:Array->Void, ?onCancel:Void->Void, ?defaultPath:String, ?dialogTitle:String):Bool { #if desktop - var filter = convertTypeFilter(typeFilter); + var filter:String = convertTypeFilter(typeFilter); - var fileDialog = new FileDialog(); + var fileDialog:FileDialog = new FileDialog(); if (onSelect != null) fileDialog.onSelectMultiple.add(onSelect); if (onCancel != null) fileDialog.onCancel.add(onCancel); @@ -101,7 +101,7 @@ class FileUtil /** * Browses for a file location to save to, then calls `onSelect(path)` when a path chosen. * Note that on HTML5 you can't do much with this, you should call `saveFile(resource:haxe.io.Bytes)` instead. - * + * * @param typeFilter TODO What does this do? * @return Whether the file dialog was opened successfully. */ @@ -109,9 +109,9 @@ class FileUtil ?dialogTitle:String):Bool { #if desktop - var filter = convertTypeFilter(typeFilter); + var filter:String = convertTypeFilter(typeFilter); - var fileDialog = new FileDialog(); + var fileDialog:FileDialog = new FileDialog(); if (onSelect != null) fileDialog.onSelect.add(onSelect); if (onCancel != null) fileDialog.onCancel.add(onCancel); @@ -129,36 +129,36 @@ class FileUtil /** * Browses for a single file location, then reads it and passes it to `onOpen(resource:haxe.io.Bytes)`. * Works great on desktop and HTML5. - * + * * @param typeFilter TODO What does this do? * @return Whether the file dialog was opened successfully. */ public static function openFile(?typeFilter:Array, ?onOpen:Bytes->Void, ?onCancel:Void->Void, ?defaultPath:String, ?dialogTitle:String):Bool { #if desktop - var filter = convertTypeFilter(typeFilter); + var filter:String = convertTypeFilter(typeFilter); - var fileDialog = new FileDialog(); + var fileDialog:FileDialog = new FileDialog(); if (onOpen != null) fileDialog.onOpen.add(onOpen); if (onCancel != null) fileDialog.onCancel.add(onCancel); fileDialog.open(filter, defaultPath, dialogTitle); return true; #elseif html5 - var onFileLoaded = function(event) { + var onFileLoaded:Event->Void = function(event) { var loadedFileRef:FileReference = event.target; trace('Loaded file: ' + loadedFileRef.name); onOpen(loadedFileRef.data); } - var onFileSelected = function(event) { + var onFileSelected:Event->Void = function(event) { var selectedFileRef:FileReference = event.target; trace('Selected file: ' + selectedFileRef.name); selectedFileRef.addEventListener(Event.COMPLETE, onFileLoaded); selectedFileRef.load(); } - var fileRef = new FileReference(); + var fileRef:FileReference = new FileReference(); fileRef.addEventListener(Event.SELECT, onFileSelected); fileRef.browse(typeFilter); return true; @@ -171,24 +171,24 @@ class FileUtil /** * Browses for a single file location, then writes the provided `haxe.io.Bytes` data and calls `onSave(path)` when done. * Works great on desktop and HTML5. - * + * * @return Whether the file dialog was opened successfully. */ public static function saveFile(data:Bytes, ?onSave:String->Void, ?onCancel:Void->Void, ?defaultFileName:String, ?dialogTitle:String):Bool { #if desktop - var filter = defaultFileName != null ? Path.extension(defaultFileName) : null; + var filter:String = defaultFileName != null ? Path.extension(defaultFileName) : null; - var fileDialog = new FileDialog(); + var fileDialog:FileDialog = new FileDialog(); if (onSave != null) fileDialog.onSelect.add(onSave); if (onCancel != null) fileDialog.onCancel.add(onCancel); fileDialog.save(data, filter, defaultFileName, dialogTitle); return true; #elseif html5 - var filter = defaultFileName != null ? Path.extension(defaultFileName) : null; + var filter:String = defaultFileName != null ? Path.extension(defaultFileName) : null; - var fileDialog = new FileDialog(); + var fileDialog:FileDialog = new FileDialog(); if (onSave != null) fileDialog.onSave.add(onSave); if (onCancel != null) fileDialog.onCancel.add(onCancel); @@ -204,7 +204,7 @@ class FileUtil * Prompts the user to save multiple files. * On desktop, this will prompt the user for a directory, then write all of the files to there. * On HTML5, this will zip the files up and prompt the user to save that. - * + * * @param typeFilter TODO What does this do? * @return Whether the file dialog was opened successfully. */ @@ -213,7 +213,7 @@ class FileUtil { #if desktop // Prompt the user for a directory, then write all of the files to there. - var onSelectDir = function(targetPath:String) { + var onSelectDir:String->Void = function(targetPath:String):Void { var paths:Array = []; for (resource in resources) { @@ -230,7 +230,7 @@ class FileUtil writeBytesToPath(filePath, resource.data, force ? Force : Skip); } } - catch (e:Dynamic) + catch (_) { trace('Failed to write file (probably already exists): $filePath' + filePath); continue; @@ -240,7 +240,7 @@ class FileUtil onSaveAll(paths); } - browseForDirectory(null, onSelectDir, onCancel, defaultPath, "Choose directory to save all files to..."); + browseForDirectory(null, onSelectDir, onCancel, defaultPath, 'Choose directory to save all files to...'); return true; #elseif html5 @@ -260,14 +260,14 @@ class FileUtil ?force:Bool = false):Bool { // Create a ZIP file. - var zipBytes = createZIPFromEntries(resources); + var zipBytes:Bytes = createZIPFromEntries(resources); - var onSave = function(path:String) { + var onSave:String->Void = function(path:String) { onSave([path]); }; // Prompt the user to save the ZIP file. - saveFile(zipBytes, onSave, onCancel, defaultPath, "Save files as ZIP..."); + saveFile(zipBytes, onSave, onCancel, defaultPath, 'Save files as ZIP...'); return true; } @@ -282,7 +282,7 @@ class FileUtil { #if desktop // Create a ZIP file. - var zipBytes = createZIPFromEntries(resources); + var zipBytes:Bytes = createZIPFromEntries(resources); // Write the ZIP. writeBytesToPath(path, zipBytes, force ? Force : Skip); @@ -293,13 +293,70 @@ class FileUtil #end } + /** + * Read string file contents directly from a given path. + * Only works on desktop. + * + * @param path The path to the file. + * @return The file contents. + */ + public static function readStringFromPath(path:String):String + { + #if sys + return sys.io.File.getContent(path); + #else + return null; + #end + } + + /** + * Read bytes file contents directly from a given path. + * Only works on desktop. + * + * @param path The path to the file. + * @return The file contents. + */ + public static function readBytesFromPath(path:String):Bytes + { + #if sys + return Bytes.ofString(sys.io.File.getContent(path)); + #else + return null; + #end + } + + /** + * Read JSON file contents directly from a given path. + * Only works on desktop. + * + * @param path The path to the file. + * @return The JSON data. + */ + public static function readJSONFromPath(path:String):Dynamic + { + #if sys + try + { + return SerializerUtil.fromJSON(sys.io.File.getContent(path)); + } + catch (ex) + { + return null; + } + #else + return null; + #end + } + /** * Write string file contents directly to a given path. * Only works on desktop. - * + * + * @param path The path to the file. + * @param data The string to write. * @param mode Whether to Force, Skip, or Ask to overwrite an existing file. */ - public static function writeStringToPath(path:String, data:String, mode:FileWriteMode = Skip) + public static function writeStringToPath(path:String, data:String, mode:FileWriteMode = Skip):Void { #if sys createDirIfNotExists(Path.directory(path)); @@ -335,10 +392,12 @@ class FileUtil /** * Write byte file contents directly to a given path. * Only works on desktop. - * + * + * @param path The path to the file. + * @param data The bytes to write. * @param mode Whether to Force, Skip, or Ask to overwrite an existing file. */ - public static function writeBytesToPath(path:String, data:Bytes, mode:FileWriteMode = Skip) + public static function writeBytesToPath(path:String, data:Bytes, mode:FileWriteMode = Skip):Void { #if sys createDirIfNotExists(Path.directory(path)); @@ -374,8 +433,11 @@ class FileUtil /** * Write string file contents directly to the end of a file at the given path. * Only works on desktop. + * + * @param path The path to the file. + * @param data The string to append. */ - public static function appendStringToPath(path:String, data:String) + public static function appendStringToPath(path:String, data:String):Void { #if sys sys.io.File.append(path, false).writeString(data); @@ -387,8 +449,10 @@ class FileUtil /** * Create a directory if it doesn't already exist. * Only works on desktop. + * + * @param dir The path to the directory. */ - public static function createDirIfNotExists(dir:String) + public static function createDirIfNotExists(dir:String):Void { #if sys if (!sys.FileSystem.exists(dir)) @@ -404,6 +468,8 @@ class FileUtil /** * Get the path to a temporary directory we can use for writing files. * Only works on desktop. + * + * @return The path to the temporary directory. */ public static function getTempDir():String { @@ -421,9 +487,11 @@ class FileUtil if (path != null) break; } - return tempDir = Path.join([path, 'funkin/']); + tempDir = Path.join([path, 'funkin/']); + return tempDir; #else - return tempDir = '/tmp/funkin/'; + tempDir = '/tmp/funkin/'; + return tempDir; #end #else return null; @@ -432,15 +500,15 @@ class FileUtil /** * Create a Bytes object containing a ZIP file, containing the provided entries. - * + * * @param entries The entries to add to the ZIP file. * @return The ZIP file as a Bytes object. */ public static function createZIPFromEntries(entries:Array):Bytes { - var o = new haxe.io.BytesOutput(); + var o:haxe.io.BytesOutput = new haxe.io.BytesOutput(); - var zipWriter = new haxe.zip.Writer(o); + var zipWriter:haxe.zip.Writer = new haxe.zip.Writer(o); zipWriter.write(entries.list()); return o.getBytes(); @@ -448,15 +516,27 @@ class FileUtil /** * Create a ZIP file entry from a file name and its string contents. - * + * * @param name The name of the file. You can use slashes to create subdirectories. * @param content The string contents of the file. * @return The resulting entry. */ public static function makeZIPEntry(name:String, content:String):Entry { - var data = haxe.io.Bytes.ofString(content, UTF8); + var data:Bytes = haxe.io.Bytes.ofString(content, UTF8); + return makeZIPEntryFromBytes(name, data); + } + + /** + * Create a ZIP file entry from a file name and its string contents. + * + * @param name The name of the file. You can use slashes to create subdirectories. + * @param data The byte data of the file. + * @return The resulting entry. + */ + public static function makeZIPEntryFromBytes(name:String, data:haxe.io.Bytes):Entry + { return { fileName: name, fileSize: data.length, @@ -474,15 +554,15 @@ class FileUtil static function convertTypeFilter(typeFilter:Array):String { - var filter = null; + var filter:String = null; if (typeFilter != null) { - var filters = []; + var filters:Array = []; for (type in typeFilter) { - filters.push(StringTools.replace(StringTools.replace(type.extension, "*.", ""), ";", ",")); + filters.push(StringTools.replace(StringTools.replace(type.extension, '*.', ''), ';', ',')); } - filter = filters.join(";"); + filter = filters.join(';'); } return filter; From 91a9c385b3af75408ca1f06aa4c968774ac4cd0d Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 8 Jun 2023 16:47:48 -0400 Subject: [PATCH 03/14] WIP of note preview --- .../debug/charting/ChartEditorNotePreview.hx | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 source/funkin/ui/debug/charting/ChartEditorNotePreview.hx diff --git a/source/funkin/ui/debug/charting/ChartEditorNotePreview.hx b/source/funkin/ui/debug/charting/ChartEditorNotePreview.hx new file mode 100644 index 000000000..29c844c2f --- /dev/null +++ b/source/funkin/ui/debug/charting/ChartEditorNotePreview.hx @@ -0,0 +1,146 @@ +package funkin.ui.debug.charting; + +package funkin.ui.debug.charting; + +import funkin.play.song.SongData.SongEventData; +import funkin.play.song.SongData.SongNoteData; +import flixel.math.FlxMath; +import flixel.FlxSprite; +import flixel.util.FlxColor; +import flixel.util.FlxSpriteUtil; + +/** + * Handles the note scrollbar preview in the chart editor. + */ +class ChartEditorNotePreview extends FlxSprite +{ + // + // Constants + // + static final NOTE_WIDTH:Int = 5; + static final NOTE_HEIGHT:Int = 1; + static final WIDTH:Int = NOTE_WIDTH * 9; + + static final BG_COLOR:FlxColor = FlxColor.GRAY; + static final LEFT_COLOR:FlxColor = 0xFFFF22AA; + static final DOWN_COLOR:FlxColor = 0xFF00EEFF; + static final UP_COLOR:FlxColor = 0xFF00CC00; + static final RIGHT_COLOR:FlxColor = 0xFFCC1111; + static final EVENT_COLOR:FlxColor = 0xFF111111; + + var previewHeight:Int; + + public function new(height:Int) + { + super(0, 0); + this.previewHeight = height; + buildBackground(); + } + + /** + * Build the initial sprite for the preview. + */ + function buildBackground():Void + { + makeGraphic(WIDTH, 0, BG_COLOR); + } + + /** + * Erase all notes from the preview. + */ + public function erase():Void + { + drawRect(0, 0, WIDTH, previewHeight, BG_COLOR); + } + + /** + * Add a single note to the preview. + * @param note The data for the note. + * @param songLengthInMs The total length of the song in milliseconds. + */ + public function addNote(note:SongNoteData, songLengthInMs:Int):Void + { + var noteDir:Int = note.getDirection(); + var mustHit:Bool = note.getStrumlineIndex() == 0; + drawNote(noteDir, mustHit, Std.int(note.time), songLengthInMs); + } + + /** + * Add a song event to the preview. + * @param event The data for the event. + * @param songLengthInMs The total length of the song in milliseconds. + */ + public function addEvent(event:SongEventData, songLengthInMs:Int):Void + { + drawNote(-1, false, Std.int(event.time), songLengthInMs); + } + + /** + * Add an array of notes to the preview. + * @param notes The data for the notes. + * @param songLengthInMs The total length of the song in milliseconds. + */ + public function addNotes(notes:Array, songLengthInMs:Int):Void + { + for (note in notes) + { + addNote(note, songLengthInMs); + } + } + + /** + * Add an array of events to the preview. + * @param events The data for the events. + * @param songLengthInMs The total length of the song in milliseconds. + */ + public function addEvents(events:Array, songLengthInMs:Int):Void + { + for (event in events) + { + addEvent(event, songLengthInMs); + } + } + + /** + * Draws a note on the preview. + * @param dir Note data. + * @param mustHit False if opponent, true if player. + * @param strumTimeInMs Time in milliseconds to strum the note. + * @param songLengthInMs Length of the song in milliseconds. + */ + function drawNote(dir:Int, mustHit:Bool, strumTimeInMs:Int, songLengthInMs:Int):Void + { + var color:FlxColor = switch (dir) + { + case 0: LEFT_COLOR; + case 1: DOWN_COLOR; + case 2: UP_COLOR; + case 3: RIGHT_COLOR; + default: EVENT_COLOR; + }; + + var noteX:Float = NOTE_WIDTH * dir; + if (mustHit) noteX += NOTE_WIDTH * 4; + if (dir == -1) noteX = NOTE_WIDTH * 8; + + var noteY:Float = FlxMath.remapToRange(strumTimeInMs, 0, songLengthInMs, 0, previewHeight); + + drawRect(noteX, noteY, NOTE_WIDTH, NOTE_HEIGHT, color); + } + + function eraseNote(dir:Int, mustHit:Bool, strumTimeInMs:Int, songLengthInMs:Int):Void + { + var noteX:Float = NOTE_WIDTH * dir; + if (mustHit) noteX += NOTE_WIDTH * 4; + if (dir == -1) noteX = NOTE_WIDTH * 8; + + var noteY:Float = FlxMath.remapToRange(strumTimeInMs, 0, songLengthInMs, 0, previewHeight); + + drawRect(noteX, noteY, NOTE_WIDTH, NOTE_HEIGHT, BG_COLOR); + } + + inline function drawRect(noteX:Float, noteY:Float, width:Int, height:Int, color:FlxColor):Void + { + FlxSpriteUtil.drawRect(this, noteX, noteY, width, height, color); + } +} From ac876c2f590b83ffec80129887005f181f49d1c9 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 8 Jun 2023 16:48:13 -0400 Subject: [PATCH 04/14] Fixes to Character preview window --- .../ui/haxeui/components/CharacterPlayer.hx | 238 +++++++++--------- 1 file changed, 122 insertions(+), 116 deletions(-) diff --git a/source/funkin/ui/haxeui/components/CharacterPlayer.hx b/source/funkin/ui/haxeui/components/CharacterPlayer.hx index c45aac35e..0e6981535 100644 --- a/source/funkin/ui/haxeui/components/CharacterPlayer.hx +++ b/source/funkin/ui/haxeui/components/CharacterPlayer.hx @@ -1,25 +1,19 @@ package funkin.ui.haxeui.components; -import flixel.FlxSprite; -import flixel.graphics.frames.FlxAtlasFrames; -import flixel.graphics.frames.FlxFramesCollection; -import flixel.math.FlxRect; -import funkin.modding.events.ScriptEvent; -import funkin.modding.IScriptedClass.IPlayStateScriptedClass; +import funkin.modding.events.ScriptEvent.GhostMissNoteScriptEvent; +import funkin.modding.events.ScriptEvent.NoteScriptEvent; +import funkin.modding.events.ScriptEvent.SongTimeScriptEvent; +import funkin.modding.events.ScriptEvent.UpdateScriptEvent; +import haxe.ui.core.IDataComponent; import funkin.play.character.BaseCharacter; import funkin.play.character.CharacterData.CharacterDataParser; import haxe.ui.containers.Box; import haxe.ui.core.Component; -import haxe.ui.core.IDataComponent; -import haxe.ui.data.DataSource; import haxe.ui.events.AnimationEvent; -import haxe.ui.events.UIEvent; import haxe.ui.geom.Size; import haxe.ui.layouts.DefaultLayout; -import haxe.ui.styles.Style; -import openfl.Assets; -private typedef AnimationInfo = +typedef AnimationInfo = { var name:String; var prefix:String; @@ -29,6 +23,10 @@ private typedef AnimationInfo = var flipY:Null; // default false } +/** + * A variant of SparrowPlayer which loads a BaseCharacter instead. + * This allows it to play appropriate animations based on song events. + */ @:composite(Layout) class CharacterPlayer extends Box { @@ -37,7 +35,7 @@ class CharacterPlayer extends Box public function new(?defaultToBf:Bool = true) { super(); - this._overrideSkipTransformChildren = false; + _overrideSkipTransformChildren = false; if (defaultToBf) { @@ -45,52 +43,39 @@ class CharacterPlayer extends Box } } - var _charId:String; - public var charId(get, set):String; function get_charId():String { - return _charId; + return character.characterId; } function set_charId(value:String):String { - _charId = value; - loadCharacter(_charId); + loadCharacter(value); return value; } - var _redispatchLoaded:Bool = false; // possible haxeui bug: if listener is added after event is dispatched, event is "lost"... needs thinking about, is it smart to "collect and redispatch"? Not sure - var _redispatchStart:Bool = false; // possible haxeui bug: if listener is added after event is dispatched, event is "lost"... needs thinking about, is it smart to "collect and redispatch"? Not sure + public var charName(get, null):String; - public override function onReady() + function get_charName():String { - super.onReady(); - - invalidateComponentLayout(); - - if (_redispatchLoaded) - { - _redispatchLoaded = false; - dispatch(new AnimationEvent(AnimationEvent.LOADED)); - } - - if (_redispatchStart) - { - _redispatchStart = false; - dispatch(new AnimationEvent(AnimationEvent.START)); - } - - parentComponent._overrideSkipTransformChildren = false; + return character.characterName; } - public function loadCharacter(id:String) + // possible haxeui bug: if listener is added after event is dispatched, event is "lost"... is it smart to "collect and redispatch"? Not sure + var _redispatchLoaded:Bool = false; + // possible haxeui bug: if listener is added after event is dispatched, event is "lost"... is it smart to "collect and redispatch"? Not sure + var _redispatchStart:Bool = false; + var _characterLoaded:Bool = false; + + /** + * Loads a character by ID. + * @param id The ID of the character to load. + */ + public function loadCharacter(id:String):Void { - if (id == null) - { - return; - } + if (id == null) return; if (character != null) { @@ -99,34 +84,24 @@ class CharacterPlayer extends Box character = null; } - var newCharacter:BaseCharacter = CharacterDataParser.fetchCharacter(id); - - if (newCharacter == null) - { - return; - } + // Prevent script issues by fetching with debug=true. + var newCharacter:BaseCharacter = CharacterDataParser.fetchCharacter(id, true); + if (newCharacter == null) return; // Fail if character doesn't exist. + // Assign character. character = newCharacter; - if (_characterType != null) - { - character.characterType = _characterType; - } - if (flip) - { - character.flipX = !character.flipX; - } - character.scale.x *= _scale; - character.scale.y *= _scale; + // Set character properties. + if (characterType != null) character.characterType = characterType; + if (flip) character.flipX = !character.flipX; + if (targetScale != 1.0) character.setScale(targetScale); - character.animation.callback = function(name:String = "", frameNumber:Int = -1, frameIndex:Int = -1) - { + character.animation.callback = function(name:String = '', frameNumber:Int = -1, frameIndex:Int = -1) { @:privateAccess character.onAnimationFrame(name, frameNumber, frameIndex); dispatch(new AnimationEvent(AnimationEvent.FRAME)); }; - character.animation.finishCallback = function(name:String = "") - { + character.animation.finishCallback = function(name:String = '') { @:privateAccess character.onAnimationFinished(name); dispatch(new AnimationEvent(AnimationEvent.END)); @@ -145,28 +120,15 @@ class CharacterPlayer extends Box } } - override function repositionChildren() + /** + * The character type (such as BF, Dad, GF, etc). + */ + public var characterType(default, set):CharacterType; + + function set_characterType(value:CharacterType):CharacterType { - super.repositionChildren(); - - @:privateAccess - var animOffsets = character.animOffsets; - - character.x = this.screenX + ((this.width / 2) - (character.frameWidth / 2)); - character.x -= animOffsets[0]; - character.y = this.screenY + ((this.height / 2) - (character.frameHeight / 2)); - character.y -= animOffsets[1]; - } - - var _characterType:CharacterType; - - public function setCharacterType(value:CharacterType) - { - _characterType = value; - if (character != null) - { - character.characterType = value; - } + if (character != null) character.characterType = value; + return characterType = value; } public var flip(default, set):Bool; @@ -183,89 +145,133 @@ class CharacterPlayer extends Box return flip = value; } - var _scale:Float = 1.0; + public var targetScale(default, set):Float = 1.0; - public function setScale(value) + function set_targetScale(value:Float):Float { - _scale = value; + if (value == targetScale) return value; + if (character != null) { - character.scale.x *= _scale; - character.scale.y *= _scale; + character.setScale(value); } + + return targetScale = value; } - public function onUpdate(event:UpdateScriptEvent) + function onFrame(name:String, frameNumber:Int, frameIndex:Int):Void + { + dispatch(new AnimationEvent(AnimationEvent.FRAME)); + } + + function onFinish(name:String):Void + { + dispatch(new AnimationEvent(AnimationEvent.END)); + } + + override function repositionChildren():Void + { + super.repositionChildren(); + character.x = this.screenX; + character.y = this.screenY; + + // Apply animation offsets, so the character is positioned correctly based on the animation. + @:privateAccess var animOffsets:Array = character.animOffsets; + + character.x -= animOffsets[0] * targetScale * (flip ? -1 : 1); + character.y -= animOffsets[1] * targetScale; + } + + /** + * Called when an update event is hit in the song. + * Used to play character animations. + * @param event The event. + */ + public function onUpdate(event:UpdateScriptEvent):Void { if (character != null) character.onUpdate(event); } + /** + * Called when an beat is hit in the song + * Used to play character animations. + * @param event The event. + */ public function onBeatHit(event:SongTimeScriptEvent):Void { if (character != null) character.onBeatHit(event); - - this.repositionChildren(); } + /** + * Called when a step is hit in the song + * Used to play character animations. + * @param event The event. + */ public function onStepHit(event:SongTimeScriptEvent):Void { if (character != null) character.onStepHit(event); } + /** + * Called when a note is hit in the song + * Used to play character animations. + * @param event The event. + */ public function onNoteHit(event:NoteScriptEvent):Void { if (character != null) character.onNoteHit(event); - - this.repositionChildren(); } + /** + * Called when a note is missed in the song + * Used to play character animations. + * @param event The event. + */ public function onNoteMiss(event:NoteScriptEvent):Void { if (character != null) character.onNoteMiss(event); - - this.repositionChildren(); } + /** + * Called when a key is pressed but no note is hit in the song + * Used to play character animations. + * @param event The event. + */ public function onNoteGhostMiss(event:GhostMissNoteScriptEvent):Void { if (character != null) character.onNoteGhostMiss(event); - - this.repositionChildren(); } } @:access(funkin.ui.haxeui.components.CharacterPlayer) private class Layout extends DefaultLayout { - public override function repositionChildren() + public override function resizeChildren():Void { - var player = cast(_component, CharacterPlayer); - var sprite:BaseCharacter = player.character; - if (sprite == null) + super.resizeChildren(); + + var player:CharacterPlayer = cast(_component, CharacterPlayer); + var character:BaseCharacter = player.character; + if (character == null) { - return super.repositionChildren(); + return super.resizeChildren(); } - @:privateAccess - var animOffsets = sprite.animOffsets; - - sprite.x = _component.screenLeft + ((_component.width / 2) - (sprite.frameWidth / 2)); - sprite.x += animOffsets[0]; - sprite.y = _component.screenTop + ((_component.height / 2) - (sprite.frameHeight / 2)); - sprite.y += animOffsets[1]; + character.cornerPosition.set(0, 0); + // character.setGraphicSize(Std.int(innerWidth), Std.int(innerHeight)); } public override function calcAutoSize(exclusions:Array = null):Size { - var player = cast(_component, CharacterPlayer); - var sprite = player.character; - if (sprite == null) + var player:CharacterPlayer = cast(_component, CharacterPlayer); + var character:BaseCharacter = player.character; + if (character == null) { return super.calcAutoSize(exclusions); } - var size = new Size(); - size.width = sprite.frameWidth + paddingLeft + paddingRight; - size.height = sprite.frameHeight + paddingTop + paddingBottom; + var size:Size = new Size(); + size.width = character.width + paddingLeft + paddingRight; + size.height = character.height + paddingTop + paddingBottom; return size; } } From 3005aa1f3ba9bd02ea703f9a309ebca63fa59732 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 8 Jun 2023 16:48:34 -0400 Subject: [PATCH 05/14] Added legacy importer --- source/funkin/play/song/formats/FNFLegacy.hx | 131 +++++ .../charting/ChartEditorDialogHandler.hx | 488 ++++++++++++++++-- .../debug/charting/ChartEditorEventSprite.hx | 8 +- .../debug/charting/ChartEditorThemeHandler.hx | 28 +- .../charting/ChartEditorToolboxHandler.hx | 205 +++++--- 5 files changed, 722 insertions(+), 138 deletions(-) create mode 100644 source/funkin/play/song/formats/FNFLegacy.hx diff --git a/source/funkin/play/song/formats/FNFLegacy.hx b/source/funkin/play/song/formats/FNFLegacy.hx new file mode 100644 index 000000000..51dc602f3 --- /dev/null +++ b/source/funkin/play/song/formats/FNFLegacy.hx @@ -0,0 +1,131 @@ +package funkin.play.song.formats; + +typedef FNFLegacy = +{ + var song:LegacySongData; +} + +typedef LegacySongData = +{ + var player1:String; // Boyfriend + var player2:String; // Opponent + + var speed:LegacyScrollSpeeds; + var stageDefault:String; + var bpm:Float; + var notes:LegacyNoteData; + var song:String; // Song name +}; + +typedef LegacyScrollSpeeds = +{ + var easy:Float; + var normal:Float; + var hard:Float; +}; + +typedef LegacyNoteData = +{ + /** + * The easy difficulty. + */ + var ?easy:Array; + + /** + * The normal difficulty. + */ + var ?normal:Array; + + /** + * The hard difficulty. + */ + var ?hard:Array; +}; + +typedef LegacyNoteSection = +{ + /** + * Whether the section is a must-hit section. + * If true, 0-3 are boyfriends notes, 4-7 are opponents notes. + * If false, 0-3 are opponents notes, 4-7 are boyfriends notes. + */ + var mustHitSection:Bool; + + /** + * Array of note data: + * - Direction + * - Time (ms) + * - Sustain Duration (ms) + * - Note kind (true = "alt", or string) + */ + var sectionNotes:Array; + + var typeOfSection:Int; + var lengthInSteps:Int; +} + +/** + * Notes in the old format are stored as an Array + */ +abstract LegacyNote(Array) +{ + public var time(get, set):Float; + + function get_time():Float + { + return this[0]; + } + + function set_time(value:Float):Float + { + return this[0] = value; + } + + public var data(get, set):Int; + + function get_data():Int + { + return this[1]; + } + + function set_data(value:Int):Int + { + return this[1] = value; + } + + public function getData(mustHitSection:Bool):Int + { + if (mustHitSection) return this[1]; + + return (this[1] + 4) % 8; + } + + public var length(get, set):Float; + + function get_length():Float + { + if (this.length < 3) return 0.0; + return this[2]; + } + + function set_length(value:Float):Float + { + return this[2] = value; + } + + public var kind(get, set):String; + + function get_kind():String + { + if (this.length < 4) return 'normal'; + + if (Std.isOfType(this[3], Bool)) return this[3] ? 'alt' : 'normal'; + + return this[3]; + } + + function set_kind(value:String):String + { + return this[3] = value; + } +} diff --git a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx index 4240773e4..4044df5d8 100644 --- a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx +++ b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx @@ -1,13 +1,21 @@ package funkin.ui.debug.charting; +import funkin.play.character.CharacterData; +import funkin.util.Constants; +import funkin.util.SerializerUtil; +import funkin.play.song.SongData.SongChartData; +import funkin.play.song.SongData.SongMetadata; import flixel.util.FlxTimer; import funkin.input.Cursor; import funkin.play.character.BaseCharacter; import funkin.play.character.CharacterData.CharacterDataParser; import funkin.play.song.Song; +import funkin.play.song.SongMigrator; +import funkin.play.song.SongValidator; import funkin.play.song.SongData.SongDataParser; import funkin.play.song.SongData.SongPlayableChar; import funkin.play.song.SongData.SongTimeChange; +import funkin.util.FileUtil; import haxe.io.Path; import haxe.ui.components.Button; import haxe.ui.components.DropDown; @@ -40,6 +48,9 @@ class ChartEditorDialogHandler static final CHART_EDITOR_DIALOG_SONG_METADATA_CHARGROUP_LAYOUT:String = Paths.ui('chart-editor/dialogs/song-metadata-chargroup'); 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_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart'); + static final CHART_EDITOR_DIALOG_OPEN_CHART_ENTRY_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart-entry'); + static final CHART_EDITOR_DIALOG_IMPORT_CHART_LAYOUT:String = Paths.ui('chart-editor/dialogs/import-chart'); static final CHART_EDITOR_DIALOG_USER_GUIDE_LAYOUT:String = Paths.ui('chart-editor/dialogs/user-guide'); /** @@ -71,41 +82,32 @@ class ChartEditorDialogHandler // // Create Song Wizard // + openCreateSongWizard(state, false); + } - // Step 1. Upload Instrumental - var uploadInstDialog:Dialog = openUploadInstDialog(state, false); - uploadInstDialog.onDialogClosed = function(_event) { - state.isHaxeUIDialogOpen = false; - if (_event.button == DialogButton.APPLY) - { - // Step 2. Song Metadata - var songMetadataDialog:Dialog = openSongMetadataDialog(state); - songMetadataDialog.onDialogClosed = function(_event) { - state.isHaxeUIDialogOpen = false; - if (_event.button == DialogButton.APPLY) - { - // Step 3. Upload Vocals - // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard. - openUploadVocalsDialog(state, false); // var uploadVocalsDialog:Dialog - } - else - { - // User cancelled the wizard! Back to the welcome dialog. - openWelcomeDialog(state); - } - }; - } - else - { - // User cancelled the wizard! Back to the welcome dialog. - openWelcomeDialog(state); - } - }; + var linkImportChartLegacy:Link = dialog.findComponent('splashImportChartLegacy', Link); + linkImportChartLegacy.onClick = function(_event) { + // Hide the welcome dialog + dialog.hideDialog(DialogButton.CANCEL); + + // Open the "Import Chart" dialog + openImportChartWizard(state, 'legacy', false); + }; + + var buttonBrowse:Button = dialog.findComponent('splashBrowse', Button); + buttonBrowse.onClick = function(_event) { + // Hide the welcome dialog + dialog.hideDialog(DialogButton.CANCEL); + + // Open the "Open Chart" dialog + openBrowseWizard(state, false); } var splashTemplateContainer:VBox = dialog.findComponent('splashTemplateContainer', VBox); var songList:Array = SongDataParser.listSongIds(); + // Sort alphabetically + songList.sort((a, b) -> a > b ? 1 : -1); for (targetSongId in songList) { @@ -130,6 +132,120 @@ class ChartEditorDialogHandler return dialog; } + /** + * Open the wizard for opening an existing chart from individual files. + * @param state + * @param closable + */ + public static function openBrowseWizard(state:ChartEditorState, closable:Bool):Void + { + // Open the "Open Chart" wizard + // Step 1. Open Chart + var openChartDialog:Dialog = openChartDialog(state); + openChartDialog.onDialogClosed = function(_event) { + state.isHaxeUIDialogOpen = false; + if (_event.button == DialogButton.APPLY) + { + // Step 2. Upload instrumental + var uploadInstDialog:Dialog = openUploadInstDialog(state, closable); + uploadInstDialog.onDialogClosed = function(_event) { + state.isHaxeUIDialogOpen = false; + if (_event.button == DialogButton.APPLY) + { + // Step 3. Upload Vocals + // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard. + var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog + uploadVocalsDialog.onDialogClosed = function(_event) { + state.isHaxeUIDialogOpen = false; + state.postLoadInstrumental(); + } + } + else + { + // User cancelled the wizard! Back to the welcome dialog. + openWelcomeDialog(state); + } + }; + } + else + { + // User cancelled the wizard! Back to the welcome dialog. + openWelcomeDialog(state); + } + }; + } + + public static function openImportChartWizard(state:ChartEditorState, format:String, closable:Bool):Void + { + // Open the "Open Chart" wizard + // Step 1. Open Chart + var openChartDialog:Dialog = openImportChartDialog(state, format); + openChartDialog.onDialogClosed = function(_event) { + state.isHaxeUIDialogOpen = false; + if (_event.button == DialogButton.APPLY) + { + // Step 2. Upload instrumental + var uploadInstDialog:Dialog = openUploadInstDialog(state, closable); + uploadInstDialog.onDialogClosed = function(_event) { + state.isHaxeUIDialogOpen = false; + if (_event.button == DialogButton.APPLY) + { + // Step 3. Upload Vocals + // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard. + var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog + uploadVocalsDialog.onDialogClosed = function(_event) { + state.isHaxeUIDialogOpen = false; + state.postLoadInstrumental(); + } + } + else + { + // User cancelled the wizard! Back to the welcome dialog. + openWelcomeDialog(state); + } + }; + } + else + { + // User cancelled the wizard! Back to the welcome dialog. + openWelcomeDialog(state); + } + }; + } + + public static function openCreateSongWizard(state:ChartEditorState, closable:Bool):Void + { + // Step 1. Upload Instrumental + var uploadInstDialog:Dialog = openUploadInstDialog(state, closable); + uploadInstDialog.onDialogClosed = function(_event) { + state.isHaxeUIDialogOpen = false; + if (_event.button == DialogButton.APPLY) + { + // Step 2. Song Metadata + var songMetadataDialog:Dialog = openSongMetadataDialog(state); + songMetadataDialog.onDialogClosed = function(_event) { + state.isHaxeUIDialogOpen = false; + if (_event.button == DialogButton.APPLY) + { + // Step 3. Upload Vocals + // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard. + openUploadVocalsDialog(state, false); // var uploadVocalsDialog:Dialog + } + else + { + // User cancelled the wizard! Back to the welcome dialog. + openWelcomeDialog(state); + } + }; + } + else + { + // User cancelled the wizard! Back to the welcome dialog. + openWelcomeDialog(state); + } + }; + } + /** * Builds and opens a dialog where the user uploads an instrumental for the current song. * @param state The current chart editor state. @@ -214,11 +330,20 @@ class ChartEditorDialogHandler } else { + var message:String = if (!ChartEditorState.SUPPORTED_MUSIC_FORMATS.contains(path.ext)) + { + 'File format (${path.ext}) not supported for instrumental track (${path.file}.${path.ext})'; + } + else + { + 'Failed to load instrumental track (${path.file}.${path.ext})'; + } + // Tell the user the load was successful. NotificationManager.instance.addNotification( { title: 'Failure', - body: 'Failed to load instrumental track (${path.file}.${path.ext})', + body: message, type: NotificationType.Error, expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME }); @@ -418,12 +543,6 @@ class ChartEditorDialogHandler moveCharGroup(event.data.id); }; - if (key == null) - { - // Find the next available player character. - trace(charGroupPlayer.dataSource.data); - } - var charGroupOpponent:DropDown = charGroup.findComponent('charGroupOpponent', DropDown); charGroupOpponent.onChange = function(event:UIEvent) { charData.opponent = event.data.id; @@ -481,8 +600,8 @@ class ChartEditorDialogHandler for (charKey in charIdsForVocals) { trace('Adding vocal upload for character ${charKey}'); - var charMetadata:BaseCharacter = CharacterDataParser.fetchCharacter(charKey); - var charName:String = charMetadata.characterName; + var charMetadata:CharacterData = CharacterDataParser.fetchCharacterData(charKey); + var charName:String = charMetadata.name; var vocalsEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_UPLOAD_VOCALS_ENTRY_LAYOUT); @@ -509,11 +628,20 @@ class ChartEditorDialogHandler } else { + var message:String = if (!ChartEditorState.SUPPORTED_MUSIC_FORMATS.contains(path.ext)) + { + 'File format (${path.ext}) not supported for vocal track (${path.file}.${path.ext})'; + } + else + { + 'Failed to load vocal track (${path.file}.${path.ext})'; + } + // Vocals failed to load. NotificationManager.instance.addNotification( { title: 'Failure', - body: 'Failed to load vocal track (${path.file}.${path.ext})', + body: message, type: NotificationType.Error, expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME }); @@ -550,9 +678,287 @@ class ChartEditorDialogHandler return dialog; } + /** + * Builds and opens a dialog where the user upload the JSON files for a 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. + */ + @:haxe.warning('-WVarInit') + public static function openChartDialog(state:ChartEditorState, ?closable:Bool = true):Dialog + { + var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_OPEN_CHART_LAYOUT, true, closable); + + var buttonCancel:Button = dialog.findComponent('dialogCancel', Button); + buttonCancel.onClick = function(_event) { + dialog.hideDialog(DialogButton.CANCEL); + } + + var chartContainerA:Component = dialog.findComponent('chartContainerA'); + var chartContainerB:Component = dialog.findComponent('chartContainerB'); + + var songMetadata:Map = []; + var songChartData:Map = []; + + var buttonContinue:Button = dialog.findComponent('dialogContinue', Button); + buttonContinue.onClick = function(_event) { + state.loadSong(songMetadata, songChartData); + + dialog.hideDialog(DialogButton.APPLY); + } + + var onDropFileMetadataVariation:String->Label->String->Void; + var onClickMetadataVariation:String->Label->UIEvent->Void; + var onDropFileChartDataVariation:String->Label->String->Void; + var onClickChartDataVariation:String->Label->UIEvent->Void; + + var constructVariationEntries:Array->Void = function(variations:Array) { + // Clear the chart container. + while (chartContainerB.getComponentAt(0) != null) + { + chartContainerB.removeComponent(chartContainerB.getComponentAt(0)); + } + + // Build an entry for -chart.json. + var songDefaultChartDataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_ENTRY_LAYOUT); + var songDefaultChartDataEntryLabel:Label = songDefaultChartDataEntry.findComponent('chartEntryLabel', Label); + songDefaultChartDataEntryLabel.text = 'Drag and drop -chart.json file, or click to browse.'; + + songDefaultChartDataEntry.onClick = onClickChartDataVariation.bind(Constants.DEFAULT_VARIATION).bind(songDefaultChartDataEntryLabel); + addDropHandler(songDefaultChartDataEntry, onDropFileChartDataVariation.bind(Constants.DEFAULT_VARIATION).bind(songDefaultChartDataEntryLabel)); + chartContainerB.addComponent(songDefaultChartDataEntry); + + for (variation in variations) + { + // Build entries for -metadata-.json. + var songVariationMetadataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_ENTRY_LAYOUT); + var songVariationMetadataEntryLabel:Label = songVariationMetadataEntry.findComponent('chartEntryLabel', Label); + songVariationMetadataEntryLabel.text = 'Drag and drop -metadata-${variation}.json file, or click to browse.'; + + songVariationMetadataEntry.onClick = onClickMetadataVariation.bind(variation).bind(songVariationMetadataEntryLabel); + addDropHandler(songVariationMetadataEntry, onDropFileMetadataVariation.bind(variation).bind(songVariationMetadataEntryLabel)); + chartContainerB.addComponent(songVariationMetadataEntry); + + // Build entries for -chart-.json. + var songVariationChartDataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_ENTRY_LAYOUT); + var songVariationChartDataEntryLabel:Label = songVariationChartDataEntry.findComponent('chartEntryLabel', Label); + songVariationChartDataEntryLabel.text = 'Drag and drop -chart-${variation}.json file, or click to browse.'; + + songVariationChartDataEntry.onClick = onClickChartDataVariation.bind(variation).bind(songVariationChartDataEntryLabel); + addDropHandler(songVariationChartDataEntry, onDropFileChartDataVariation.bind(variation).bind(songVariationChartDataEntryLabel)); + chartContainerB.addComponent(songVariationChartDataEntry); + } + } + + onDropFileMetadataVariation = function(variation:String, label:Label, pathStr:String) { + var path:Path = new Path(pathStr); + trace('Dropped JSON file (${path})'); + + var songMetadataJson:Dynamic = FileUtil.readJSONFromPath(path.toString()); + var songMetadataVariation:SongMetadata = SongMigrator.migrateSongMetadata(songMetadataJson, 'import'); + songMetadataVariation = SongValidator.validateSongMetadata(songMetadataVariation, 'import'); + + songMetadata.set(variation, songMetadataVariation); + + // Tell the user the load was successful. + NotificationManager.instance.addNotification( + { + title: 'Success', + body: 'Loaded metadata file (${path.file}.${path.ext})', + type: NotificationType.Success, + expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + }); + + label.text = 'Metadata file (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}'; + + if (variation == Constants.DEFAULT_VARIATION) constructVariationEntries(songMetadataVariation.playData.songVariations); + }; + + onClickMetadataVariation = function(variation:String, label:Label, _event:UIEvent) { + Dialogs.openBinaryFile('Open Chart ($variation) Metadata', [ + {label: 'JSON File (.json)', extension: 'json'}], function(selectedFile) { + if (selectedFile != null) + { + trace('Selected file: ' + selectedFile.name); + + var songMetadataJson:Dynamic = SerializerUtil.fromJSONBytes(selectedFile.bytes); + var songMetadataVariation:SongMetadata = SongMigrator.migrateSongMetadata(songMetadataJson, 'import'); + songMetadataVariation = SongValidator.validateSongMetadata(songMetadataVariation, 'import'); + songMetadataVariation.variation = variation; + + songMetadata.set(variation, songMetadataVariation); + + // Tell the user the load was successful. + NotificationManager.instance.addNotification( + { + title: 'Success', + body: 'Loaded metadata file (${selectedFile.name})', + type: NotificationType.Success, + expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + }); + + label.text = 'Metadata file (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}'; + + if (variation == Constants.DEFAULT_VARIATION) constructVariationEntries(songMetadataVariation.playData.songVariations); + } + }); + } + + onDropFileChartDataVariation = function(variation:String, label:Label, pathStr:String) { + var path:Path = new Path(pathStr); + trace('Dropped JSON file (${path})'); + + var songChartDataJson:Dynamic = FileUtil.readJSONFromPath(path.toString()); + var songChartDataVariation:SongChartData = SongMigrator.migrateSongChartData(songChartDataJson, 'import'); + songChartDataVariation = SongValidator.validateSongChartData(songChartDataVariation, 'import'); + + songChartData.set(variation, songChartDataVariation); + + // Tell the user the load was successful. + NotificationManager.instance.addNotification( + { + title: 'Success', + body: 'Loaded chart data file (${path.file}.${path.ext})', + type: NotificationType.Success, + expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + }); + + label.text = 'Chart data file (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}'; + }; + + onClickChartDataVariation = function(variation:String, label:Label, _event:UIEvent) { + Dialogs.openBinaryFile('Open Chart ($variation) Metadata', [ + {label: 'JSON File (.json)', extension: 'json'}], function(selectedFile) { + if (selectedFile != null) + { + trace('Selected file: ' + selectedFile.name); + + var songChartDataJson:Dynamic = SerializerUtil.fromJSONBytes(selectedFile.bytes); + var songChartDataVariation:SongChartData = SongMigrator.migrateSongChartData(songChartDataJson, 'import'); + songChartDataVariation = SongValidator.validateSongChartData(songChartDataVariation, 'import'); + + songChartData.set(variation, songChartDataVariation); + + // Tell the user the load was successful. + NotificationManager.instance.addNotification( + { + title: 'Success', + body: 'Loaded chart data file (${selectedFile.name})', + type: NotificationType.Success, + expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + }); + + label.text = 'Chart data file (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}'; + } + }); + } + + var metadataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_ENTRY_LAYOUT); + var metadataEntryLabel:Label = metadataEntry.findComponent('chartEntryLabel', Label); + metadataEntryLabel.text = 'Drag and drop -metadata.json file, or click to browse.'; + + metadataEntry.onClick = onClickMetadataVariation.bind(Constants.DEFAULT_VARIATION).bind(metadataEntryLabel); + addDropHandler(metadataEntry, onDropFileMetadataVariation.bind(Constants.DEFAULT_VARIATION).bind(metadataEntryLabel)); + + chartContainerA.addComponent(metadataEntry); + + return dialog; + } + + /** + * Builds and opens a dialog where the user can import a chart from an existing file format. + * @param state The current chart editor state. + * @param format The format to import from. + * @param closable + * @return Dialog + */ + public static function openImportChartDialog(state:ChartEditorState, format:String, ?closable:Bool = true):Dialog + { + var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_IMPORT_CHART_LAYOUT, true, closable); + + var prettyFormat:String = switch (format) + { + case 'legacy': 'FNF Legacy'; + default: 'Unknown'; + } + + var fileFilter = switch (format) + { + case 'legacy': {label: 'JSON Data File (.json)', extension: 'json'}; + default: null; + } + + dialog.title = 'Import Chart - ${prettyFormat}'; + + var buttonCancel:Button = dialog.findComponent('dialogCancel', Button); + + buttonCancel.onClick = function(_event) { + dialog.hideDialog(DialogButton.CANCEL); + } + + var importBox:Box = dialog.findComponent('importBox', Box); + + importBox.onMouseOver = function(_event) { + importBox.swapClass('upload-bg', 'upload-bg-hover'); + Cursor.cursorMode = Pointer; + } + + importBox.onMouseOut = function(_event) { + importBox.swapClass('upload-bg-hover', 'upload-bg'); + Cursor.cursorMode = Default; + } + + var onDropFile:String->Void; + + importBox.onClick = function(_event) { + Dialogs.openBinaryFile('Import Chart - ${prettyFormat}', [fileFilter], function(selectedFile:SelectedFileInfo) { + if (selectedFile != null) + { + trace('Selected file: ' + selectedFile.fullPath); + var selectedFileJson:Dynamic = SerializerUtil.fromJSONBytes(selectedFile.bytes); + var songMetadata:SongMetadata = SongMigrator.migrateSongMetadataFromLegacy(selectedFileJson); + var songChartData:SongChartData = SongMigrator.migrateSongChartDataFromLegacy(selectedFileJson); + + state.loadSong([Constants.DEFAULT_VARIATION => songMetadata], [Constants.DEFAULT_VARIATION => songChartData]); + + dialog.hideDialog(DialogButton.APPLY); + NotificationManager.instance.addNotification( + { + title: 'Success', + body: 'Loaded chart file (${selectedFile.name})', + type: NotificationType.Success, + expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + }); + } + }); + } + + onDropFile = function(pathStr:String) { + var path:Path = new Path(pathStr); + var selectedFileJson:Dynamic = FileUtil.readJSONFromPath(path.toString()); + var songMetadata:SongMetadata = SongMigrator.migrateSongMetadataFromLegacy(selectedFileJson); + var songChartData:SongChartData = SongMigrator.migrateSongChartDataFromLegacy(selectedFileJson); + + state.loadSong([Constants.DEFAULT_VARIATION => songMetadata], [Constants.DEFAULT_VARIATION => songChartData]); + + dialog.hideDialog(DialogButton.APPLY); + NotificationManager.instance.addNotification( + { + title: 'Success', + body: 'Loaded chart file (${path.file}.${path.ext})', + type: NotificationType.Success, + expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + }); + }; + + addDropHandler(importBox, onDropFile); + + return dialog; + } + /** * Builds and opens a dialog displaying the user guide, providing guidance and help on how to use the chart editor. - * + * * @param state The current chart editor state. * @return The dialog that was opened. */ @@ -569,6 +975,8 @@ class ChartEditorDialogHandler static function openDialog(state:ChartEditorState, key:String, modal:Bool = true, closable:Bool = true):Dialog { var dialog:Dialog = cast state.buildComponent(key); + if (dialog == null) return null; + dialog.destroyOnClose = true; dialog.closable = closable; dialog.showDialog(modal); diff --git a/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx b/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx index 323d11abd..2016e4ccc 100644 --- a/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx +++ b/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx @@ -27,7 +27,7 @@ class ChartEditorEventSprite extends FlxSprite /** * The image used for all song events. Cached for performance. */ - var eventGraphic:BitmapData; + static var eventSpriteBasic:BitmapData; public function new(parent:ChartEditorState) { @@ -40,12 +40,12 @@ class ChartEditorEventSprite extends FlxSprite function buildGraphic():Void { - if (eventGraphic == null) + if (eventSpriteBasic == null) { - eventGraphic = Assets.getBitmapData(Paths.image('ui/chart-editor/event')); + eventSpriteBasic = Assets.getBitmapData(Paths.image('ui/chart-editor/event')); } - loadGraphic(eventGraphic); + loadGraphic(eventSpriteBasic); setGraphicSize(ChartEditorState.GRID_SIZE); this.updateHitbox(); } diff --git a/source/funkin/ui/debug/charting/ChartEditorThemeHandler.hx b/source/funkin/ui/debug/charting/ChartEditorThemeHandler.hx index 7bdf366bf..40c797169 100644 --- a/source/funkin/ui/debug/charting/ChartEditorThemeHandler.hx +++ b/source/funkin/ui/debug/charting/ChartEditorThemeHandler.hx @@ -26,7 +26,7 @@ class ChartEditorThemeHandler // An enum of typedefs or something? // ================================ static final BACKGROUND_COLOR_LIGHT:FlxColor = 0xFF673AB7; - static final BACKGROUND_COLOR_DARK:FlxColor = 0xFF673AB7; + static final BACKGROUND_COLOR_DARK:FlxColor = 0xFF361E60; // Color 1 of the grid pattern. Alternates with Color 2. static final GRID_COLOR_1_LIGHT:FlxColor = 0xFFE7E6E6; @@ -43,13 +43,11 @@ class ChartEditorThemeHandler // Vertical divider between characters. static final GRID_STRUMLINE_DIVIDER_COLOR_LIGHT:FlxColor = 0xFF111111; static final GRID_STRUMLINE_DIVIDER_COLOR_DARK:FlxColor = 0xFFC4C4C4; - // static final GRID_STRUMLINE_DIVIDER_WIDTH:Float = 2; static final GRID_STRUMLINE_DIVIDER_WIDTH:Float = ChartEditorState.GRID_SELECTION_BORDER_WIDTH; // Horizontal divider between measures. static final GRID_MEASURE_DIVIDER_COLOR_LIGHT:FlxColor = 0xFF111111; static final GRID_MEASURE_DIVIDER_COLOR_DARK:FlxColor = 0xFFC4C4C4; - // static final GRID_MEASURE_DIVIDER_WIDTH:Float = 2; static final GRID_MEASURE_DIVIDER_WIDTH:Float = ChartEditorState.GRID_SELECTION_BORDER_WIDTH; // Border on the square highlighting selected notes. @@ -66,6 +64,12 @@ class ChartEditorThemeHandler static final PLAYHEAD_BLOCK_BORDER_COLOR:FlxColor = 0xFF9D0011; static final PLAYHEAD_BLOCK_FILL_COLOR:FlxColor = 0xFFBD0231; + static final TOTAL_COLUMN_COUNT:Int = ChartEditorState.STRUMLINE_SIZE * 2 + 1; + + /** + * When the theme is changed, this function updates all of the UI elements to match the new theme. + * @param state The ChartEditorState to update. + */ public static function updateTheme(state:ChartEditorState):Void { updateBackground(state); @@ -73,6 +77,10 @@ class ChartEditorThemeHandler updateSelectionSquare(state); } + /** + * Updates the tint of the background sprite to match the current theme. + * @param state The ChartEditorState to update. + */ static function updateBackground(state:ChartEditorState):Void { state.menuBG.color = switch (state.currentTheme) @@ -85,7 +93,7 @@ class ChartEditorThemeHandler /** * Builds the checkerboard background image of the chart editor, and adds dividing lines to it. - * @param dark Whether to draw the grid in a dark color instead of a light one. + * @param state The ChartEditorState to update. */ static function updateGridBitmap(state:ChartEditorState):Void { @@ -107,8 +115,8 @@ class ChartEditorThemeHandler // 2 * (Strumline Size) + 1 grid squares wide, by (4 * quarter notes per measure) grid squares tall. // This gets reused to fill the screen. - var gridWidth:Int = Std.int(ChartEditorState.GRID_SIZE * (ChartEditorState.STRUMLINE_SIZE * 2 + 1)); - var gridHeight:Int = Std.int(ChartEditorState.GRID_SIZE * (Conductor.stepsPerMeasure)); + var gridWidth:Int = Std.int(ChartEditorState.GRID_SIZE * TOTAL_COLUMN_COUNT); + var gridHeight:Int = Std.int(ChartEditorState.GRID_SIZE * Conductor.stepsPerMeasure); state.gridBitmap = FlxGridOverlay.createGrid(ChartEditorState.GRID_SIZE, ChartEditorState.GRID_SIZE, gridWidth, gridHeight, true, gridColor1, gridColor2); // Selection borders @@ -143,7 +151,7 @@ class ChartEditorThemeHandler selectionBorderColor); // Selection borders across the middle. - for (i in 1...(ChartEditorState.STRUMLINE_SIZE * 2 + 1)) + for (i in 1...TOTAL_COLUMN_COUNT) { state.gridBitmap.fillRect(new Rectangle((ChartEditorState.GRID_SIZE * i) - (ChartEditorState.GRID_SELECTION_BORDER_WIDTH / 2), 0, ChartEditorState.GRID_SELECTION_BORDER_WIDTH, state.gridBitmap.height), @@ -167,7 +175,7 @@ class ChartEditorThemeHandler // Divider at top state.gridBitmap.fillRect(new Rectangle(0, 0, state.gridBitmap.width, GRID_MEASURE_DIVIDER_WIDTH / 2), gridMeasureDividerColor); // Divider at bottom - var dividerLineBY = state.gridBitmap.height - (GRID_MEASURE_DIVIDER_WIDTH / 2); + var dividerLineBY:Float = state.gridBitmap.height - (GRID_MEASURE_DIVIDER_WIDTH / 2); state.gridBitmap.fillRect(new Rectangle(0, dividerLineBY, state.gridBitmap.width, GRID_MEASURE_DIVIDER_WIDTH / 2), gridMeasureDividerColor); // Draw dividers between the strumlines. @@ -180,10 +188,10 @@ class ChartEditorThemeHandler }; // Divider at 1 * (Strumline Size) - var dividerLineAX = ChartEditorState.GRID_SIZE * (ChartEditorState.STRUMLINE_SIZE) - (GRID_STRUMLINE_DIVIDER_WIDTH / 2); + var dividerLineAX:Float = ChartEditorState.GRID_SIZE * (ChartEditorState.STRUMLINE_SIZE) - (GRID_STRUMLINE_DIVIDER_WIDTH / 2); state.gridBitmap.fillRect(new Rectangle(dividerLineAX, 0, GRID_STRUMLINE_DIVIDER_WIDTH, state.gridBitmap.height), gridStrumlineDividerColor); // Divider at 2 * (Strumline Size) - var dividerLineBX = ChartEditorState.GRID_SIZE * (ChartEditorState.STRUMLINE_SIZE * 2) - (GRID_STRUMLINE_DIVIDER_WIDTH / 2); + var dividerLineBX:Float = ChartEditorState.GRID_SIZE * (ChartEditorState.STRUMLINE_SIZE * 2) - (GRID_STRUMLINE_DIVIDER_WIDTH / 2); state.gridBitmap.fillRect(new Rectangle(dividerLineBX, 0, GRID_STRUMLINE_DIVIDER_WIDTH, state.gridBitmap.height), gridStrumlineDividerColor); if (state.gridTiledSprite != null) diff --git a/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx b/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx index 5a903481e..d849fa894 100644 --- a/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx +++ b/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx @@ -1,6 +1,5 @@ package funkin.ui.debug.charting; -import haxe.ui.data.ArrayDataSource; import funkin.play.character.BaseCharacter.CharacterType; import funkin.play.event.SongEvent; import funkin.play.event.SongEventData; @@ -12,15 +11,17 @@ import haxe.ui.components.CheckBox; import haxe.ui.components.DropDown; import haxe.ui.components.Label; import haxe.ui.components.NumberStepper; -import haxe.ui.components.NumberStepper; import haxe.ui.components.Slider; import haxe.ui.components.TextField; -import haxe.ui.containers.dialogs.Dialog; import haxe.ui.containers.Box; -import haxe.ui.containers.Frame; import haxe.ui.containers.Grid; import haxe.ui.containers.Group; +import haxe.ui.containers.VBox; +import haxe.ui.containers.dialogs.CollapsibleDialog; +import haxe.ui.containers.dialogs.Dialog.DialogButton; +import haxe.ui.containers.dialogs.Dialog.DialogEvent; import haxe.ui.core.Component; +import haxe.ui.data.ArrayDataSource; import haxe.ui.events.UIEvent; /** @@ -32,18 +33,26 @@ enum ChartEditorToolMode Place; } +/** + * Static functions which handle building themed UI elements for a provided ChartEditorState. + */ class ChartEditorToolboxHandler { public static function setToolboxState(state:ChartEditorState, id:String, shown:Bool):Void { - if (shown) showToolbox(state, id); + if (shown) + { + showToolbox(state, id); + } else + { hideToolbox(state, id); + } } - public static function showToolbox(state:ChartEditorState, id:String) + public static function showToolbox(state:ChartEditorState, id:String):Void { - var toolbox:Dialog = state.activeToolboxes.get(id); + var toolbox:CollapsibleDialog = state.activeToolboxes.get(id); if (toolbox == null) toolbox = initToolbox(state, id); @@ -59,7 +68,7 @@ class ChartEditorToolboxHandler public static function hideToolbox(state:ChartEditorState, id:String):Void { - var toolbox:Dialog = state.activeToolboxes.get(id); + var toolbox:CollapsibleDialog = state.activeToolboxes.get(id); if (toolbox == null) toolbox = initToolbox(state, id); @@ -73,13 +82,27 @@ class ChartEditorToolboxHandler } } - public static function minimizeToolbox(state:ChartEditorState, id:String):Void {} - - public static function maximizeToolbox(state:ChartEditorState, id:String):Void {} - - public static function initToolbox(state:ChartEditorState, id:String):Dialog + public static function minimizeToolbox(state:ChartEditorState, id:String):Void { - var toolbox:Dialog = null; + var toolbox:CollapsibleDialog = state.activeToolboxes.get(id); + + if (toolbox == null) return; + + toolbox.minimized = true; + } + + public static function maximizeToolbox(state:ChartEditorState, id:String):Void + { + var toolbox:CollapsibleDialog = state.activeToolboxes.get(id); + + if (toolbox == null) return; + + toolbox.minimized = false; + } + + public static function initToolbox(state:ChartEditorState, id:String):CollapsibleDialog + { + var toolbox:CollapsibleDialog = null; switch (id) { case ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT: @@ -95,9 +118,9 @@ class ChartEditorToolboxHandler case ChartEditorState.CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT: toolbox = buildToolboxCharactersLayout(state); case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT: - toolbox = buildToolboxPlayerPreviewLayout(state); + toolbox = null; // buildToolboxPlayerPreviewLayout(state); case ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT: - toolbox = buildToolboxOpponentPreviewLayout(state); + toolbox = null; // buildToolboxOpponentPreviewLayout(state); default: // This happens if you try to load an unknown layout. trace('ChartEditorToolboxHandler.initToolbox() - Unknown toolbox ID: $id'); @@ -114,9 +137,15 @@ class ChartEditorToolboxHandler return toolbox; } - public static function getToolbox(state:ChartEditorState, id:String):Dialog + /** + * Retrieve a toolbox by its layout's asset ID. + * @param state The ChartEditorState instance. + * @param id The asset ID of the toolbox layout. + * @return The toolbox. + */ + public static function getToolbox(state:ChartEditorState, id:String):CollapsibleDialog { - var toolbox:Dialog = state.activeToolboxes.get(id); + var toolbox:CollapsibleDialog = state.activeToolboxes.get(id); // Initialize the toolbox without showing it. if (toolbox == null) toolbox = initToolbox(state, id); @@ -124,9 +153,9 @@ class ChartEditorToolboxHandler return toolbox; } - static function buildToolboxToolsLayout(state:ChartEditorState):Dialog + static function buildToolboxToolsLayout(state:ChartEditorState):CollapsibleDialog { - var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT); + var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT); if (toolbox == null) return null; @@ -134,15 +163,15 @@ class ChartEditorToolboxHandler toolbox.x = 50; toolbox.y = 50; - toolbox.onDialogClosed = (event:DialogEvent) -> { + toolbox.onDialogClosed = function(event:DialogEvent) { state.setUICheckboxSelected('menubarItemToggleToolboxTools', false); } - var toolsGroup:Group = toolbox.findComponent("toolboxToolsGroup", Group); + var toolsGroup:Group = toolbox.findComponent('toolboxToolsGroup', Group); if (toolsGroup == null) return null; - toolsGroup.onChange = (event:UIEvent) -> { + toolsGroup.onChange = function(event:UIEvent) { switch (event.target.id) { case 'toolboxToolsGroupSelect': @@ -157,9 +186,9 @@ class ChartEditorToolboxHandler return toolbox; } - static function buildToolboxNoteDataLayout(state:ChartEditorState):Dialog + static function buildToolboxNoteDataLayout(state:ChartEditorState):CollapsibleDialog { - var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT); + var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT); if (toolbox == null) return null; @@ -167,16 +196,16 @@ class ChartEditorToolboxHandler toolbox.x = 75; toolbox.y = 100; - toolbox.onDialogClosed = (event:DialogEvent) -> { + toolbox.onDialogClosed = function(event:DialogEvent) { state.setUICheckboxSelected('menubarItemToggleToolboxNotes', false); } - var toolboxNotesNoteKind:DropDown = toolbox.findComponent("toolboxNotesNoteKind", DropDown); - var toolboxNotesCustomKindLabel:Label = toolbox.findComponent("toolboxNotesCustomKindLabel", Label); - var toolboxNotesCustomKind:TextField = toolbox.findComponent("toolboxNotesCustomKind", TextField); + var toolboxNotesNoteKind:DropDown = toolbox.findComponent('toolboxNotesNoteKind', DropDown); + var toolboxNotesCustomKindLabel:Label = toolbox.findComponent('toolboxNotesCustomKindLabel', Label); + var toolboxNotesCustomKind:TextField = toolbox.findComponent('toolboxNotesCustomKind', TextField); - toolboxNotesNoteKind.onChange = (event:UIEvent) -> { - var isCustom = (event.data.id == '~CUSTOM~'); + toolboxNotesNoteKind.onChange = function(event:UIEvent) { + var isCustom:Bool = (event.data.id == '~CUSTOM~'); if (isCustom) { @@ -194,16 +223,16 @@ class ChartEditorToolboxHandler } } - toolboxNotesCustomKind.onChange = (event:UIEvent) -> { + toolboxNotesCustomKind.onChange = function(event:UIEvent) { state.selectedNoteKind = toolboxNotesCustomKind.text; } return toolbox; } - static function buildToolboxEventDataLayout(state:ChartEditorState):Dialog + static function buildToolboxEventDataLayout(state:ChartEditorState):CollapsibleDialog { - var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT); + var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT); if (toolbox == null) return null; @@ -211,12 +240,12 @@ class ChartEditorToolboxHandler toolbox.x = 100; toolbox.y = 150; - toolbox.onDialogClosed = (event:DialogEvent) -> { + toolbox.onDialogClosed = function(event:DialogEvent) { state.setUICheckboxSelected('menubarItemToggleToolboxEvents', false); } - var toolboxEventsEventKind:DropDown = toolbox.findComponent("toolboxEventsEventKind", DropDown); - var toolboxEventsDataGrid:Grid = toolbox.findComponent("toolboxEventsDataGrid", Grid); + var toolboxEventsEventKind:DropDown = toolbox.findComponent('toolboxEventsEventKind', DropDown); + var toolboxEventsDataGrid:Grid = toolbox.findComponent('toolboxEventsDataGrid', Grid); toolboxEventsEventKind.dataSource = new ArrayDataSource(); @@ -227,7 +256,7 @@ class ChartEditorToolboxHandler toolboxEventsEventKind.dataSource.add({text: event.getTitle(), value: event.id}); } - toolboxEventsEventKind.onChange = (event:UIEvent) -> { + toolboxEventsEventKind.onChange = function(event:UIEvent) { var eventType:String = event.data.value; trace('ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Event type changed: $eventType'); @@ -281,9 +310,9 @@ class ChartEditorToolboxHandler numberStepper.value = field.defaultValue; input = numberStepper; case BOOL: - var checkBox = new CheckBox(); + var checkBox:CheckBox = new CheckBox(); checkBox.id = field.name; - checkBox.selected = field.defaultValue == true; + checkBox.selected = field.defaultValue; input = checkBox; case ENUM: var dropDown:DropDown = new DropDown(); @@ -293,7 +322,7 @@ class ChartEditorToolboxHandler // Add entries to the dropdown. for (optionName in field.keys.keys()) { - var optionValue = field.keys.get(optionName); + var optionValue:String = field.keys.get(optionName); trace('$optionName : $optionValue'); dropDown.dataSource.add({value: optionValue, text: optionName}); } @@ -314,7 +343,7 @@ class ChartEditorToolboxHandler target.addComponent(input); - input.onChange = (event:UIEvent) -> { + input.onChange = function(event:UIEvent) { trace('ChartEditorToolboxHandler.buildEventDataFormFromSchema() - ${event.target.id} = ${event.target.value}'); if (event.target.value == null) state.selectedEventData.remove(event.target.id); @@ -324,9 +353,9 @@ class ChartEditorToolboxHandler } } - static function buildToolboxDifficultyLayout(state:ChartEditorState):Dialog + static function buildToolboxDifficultyLayout(state:ChartEditorState):CollapsibleDialog { - var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT); + var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT); if (toolbox == null) return null; @@ -334,36 +363,36 @@ class ChartEditorToolboxHandler toolbox.x = 125; toolbox.y = 200; - toolbox.onDialogClosed = (event:DialogEvent) -> { + toolbox.onDialogClosed = function(event:UIEvent) { state.setUICheckboxSelected('menubarItemToggleToolboxDifficulty', false); } - var difficultyToolboxSaveMetadata:Button = toolbox.findComponent("difficultyToolboxSaveMetadata", Button); - var difficultyToolboxSaveChart:Button = toolbox.findComponent("difficultyToolboxSaveChart", Button); - var difficultyToolboxSaveAll:Button = toolbox.findComponent("difficultyToolboxSaveAll", Button); - var difficultyToolboxLoadMetadata:Button = toolbox.findComponent("difficultyToolboxLoadMetadata", Button); - var difficultyToolboxLoadChart:Button = toolbox.findComponent("difficultyToolboxLoadChart", Button); + var difficultyToolboxSaveMetadata:Button = toolbox.findComponent('difficultyToolboxSaveMetadata', Button); + var difficultyToolboxSaveChart:Button = toolbox.findComponent('difficultyToolboxSaveChart', Button); + var difficultyToolboxSaveAll:Button = toolbox.findComponent('difficultyToolboxSaveAll', Button); + var difficultyToolboxLoadMetadata:Button = toolbox.findComponent('difficultyToolboxLoadMetadata', Button); + var difficultyToolboxLoadChart:Button = toolbox.findComponent('difficultyToolboxLoadChart', Button); - difficultyToolboxSaveMetadata.onClick = (event:UIEvent) -> { + difficultyToolboxSaveMetadata.onClick = function(event:UIEvent) { SongSerializer.exportSongMetadata(state.currentSongMetadata); }; - difficultyToolboxSaveChart.onClick = (event:UIEvent) -> { + difficultyToolboxSaveChart.onClick = function(event:UIEvent) { SongSerializer.exportSongChartData(state.currentSongChartData); }; - difficultyToolboxSaveAll.onClick = (event:UIEvent) -> { + difficultyToolboxSaveAll.onClick = function(event:UIEvent) { state.exportAllSongData(); }; - difficultyToolboxLoadMetadata.onClick = (event:UIEvent) -> { + difficultyToolboxLoadMetadata.onClick = function(event:UIEvent) { // Replace metadata for current variation. SongSerializer.importSongMetadataAsync(function(songMetadata) { state.currentSongMetadata = songMetadata; }); }; - difficultyToolboxLoadChart.onClick = (event:UIEvent) -> { + difficultyToolboxLoadChart.onClick = function(event:UIEvent) { // Replace chart data for current variation. SongSerializer.importSongChartDataAsync(function(songChartData) { state.currentSongChartData = songChartData; @@ -376,9 +405,9 @@ class ChartEditorToolboxHandler return toolbox; } - static function buildToolboxMetadataLayout(state:ChartEditorState):Dialog + static function buildToolboxMetadataLayout(state:ChartEditorState):CollapsibleDialog { - var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT); + var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT); if (toolbox == null) return null; @@ -386,13 +415,13 @@ class ChartEditorToolboxHandler toolbox.x = 150; toolbox.y = 250; - toolbox.onDialogClosed = (event:DialogEvent) -> { + toolbox.onDialogClosed = function(event:UIEvent) { state.setUICheckboxSelected('menubarItemToggleToolboxMetadata', false); } var inputSongName:TextField = toolbox.findComponent('inputSongName', TextField); - inputSongName.onChange = (event:UIEvent) -> { - var valid = event.target.text != null && event.target.text != ""; + inputSongName.onChange = function(event:UIEvent) { + var valid:Bool = event.target.text != null && event.target.text != ''; if (valid) { @@ -404,10 +433,11 @@ class ChartEditorToolboxHandler state.currentSongMetadata.songName = null; } }; + inputSongName.value = state.currentSongMetadata.songName; var inputSongArtist:TextField = toolbox.findComponent('inputSongArtist', TextField); - inputSongArtist.onChange = (event:UIEvent) -> { - var valid = event.target.text != null && event.target.text != ""; + inputSongArtist.onChange = function(event:UIEvent) { + var valid:Bool = event.target.text != null && event.target.text != ''; if (valid) { @@ -419,28 +449,31 @@ class ChartEditorToolboxHandler state.currentSongMetadata.artist = null; } }; + inputSongArtist.value = state.currentSongMetadata.artist; var inputStage:DropDown = toolbox.findComponent('inputStage', DropDown); - inputStage.onChange = (event:UIEvent) -> { - var valid = event.data != null && event.data.id != null; + inputStage.onChange = function(event:UIEvent) { + var valid:Bool = event.data != null && event.data.id != null; if (valid) { state.currentSongMetadata.playData.stage = event.data.id; } }; + inputStage.value = state.currentSongMetadata.playData.stage; var inputNoteSkin:DropDown = toolbox.findComponent('inputNoteSkin', DropDown); - inputNoteSkin.onChange = (event:UIEvent) -> { + inputNoteSkin.onChange = function(event:UIEvent) { if (event.data.id == null) return; state.currentSongMetadata.playData.noteSkin = event.data.id; }; + inputNoteSkin.value = state.currentSongMetadata.playData.noteSkin; var inputBPM:NumberStepper = toolbox.findComponent('inputBPM', NumberStepper); - inputBPM.onChange = (event:UIEvent) -> { + inputBPM.onChange = function(event:UIEvent) { if (event.value == null || event.value <= 0) return; - var timeChanges = state.currentSongMetadata.timeChanges; + var timeChanges:Array = state.currentSongMetadata.timeChanges; if (timeChanges == null || timeChanges.length == 0) { timeChanges = [new SongTimeChange(-1, 0, event.value, 4, 4, [4, 4, 4, 4])]; @@ -454,28 +487,30 @@ class ChartEditorToolboxHandler state.currentSongMetadata.timeChanges = timeChanges; }; + inputBPM.value = state.currentSongMetadata.timeChanges[0].bpm; var inputScrollSpeed:Slider = toolbox.findComponent('inputScrollSpeed', Slider); - inputScrollSpeed.onChange = (event:UIEvent) -> { - var valid = event.target.value != null && event.target.value > 0; + inputScrollSpeed.onChange = function(event:UIEvent) { + var valid:Bool = event.target.value != null && event.target.value > 0; if (valid) { inputScrollSpeed.removeClass('invalid-value'); - state.currentSongChartData.scrollSpeed = event.target.value; + state.currentSongChartScrollSpeed = event.target.value; } else { - state.currentSongChartData.scrollSpeed = null; + state.currentSongChartScrollSpeed = 1.0; } }; + inputScrollSpeed.value = state.currentSongChartData.scrollSpeed; return toolbox; } - static function buildToolboxCharactersLayout(state:ChartEditorState):Dialog + static function buildToolboxCharactersLayout(state:ChartEditorState):CollapsibleDialog { - var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT); + var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT); if (toolbox == null) return null; @@ -483,16 +518,16 @@ class ChartEditorToolboxHandler toolbox.x = 175; toolbox.y = 300; - toolbox.onDialogClosed = (event:DialogEvent) -> { + toolbox.onDialogClosed = function(event:DialogEvent) { state.setUICheckboxSelected('menubarItemToggleToolboxCharacters', false); } return toolbox; } - static function buildToolboxPlayerPreviewLayout(state:ChartEditorState):Dialog + static function buildToolboxPlayerPreviewLayout(state:ChartEditorState):CollapsibleDialog { - var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT); + var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT); if (toolbox == null) return null; @@ -500,23 +535,23 @@ class ChartEditorToolboxHandler toolbox.x = 200; toolbox.y = 350; - toolbox.onDialogClosed = (event:DialogEvent) -> { + toolbox.onDialogClosed = function(event:DialogEvent) { state.setUICheckboxSelected('menubarItemToggleToolboxPlayerPreview', false); } var charPlayer:CharacterPlayer = toolbox.findComponent('charPlayer'); // TODO: We need to implement character swapping in ChartEditorState. charPlayer.loadCharacter('bf'); - // charPlayer.setScale(0.5); - charPlayer.setCharacterType(CharacterType.BF); + charPlayer.characterType = CharacterType.BF; charPlayer.flip = true; + charPlayer.targetScale = 0.5; return toolbox; } - static function buildToolboxOpponentPreviewLayout(state:ChartEditorState):Dialog + static function buildToolboxOpponentPreviewLayout(state:ChartEditorState):CollapsibleDialog { - var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT); + var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT); if (toolbox == null) return null; @@ -524,16 +559,18 @@ class ChartEditorToolboxHandler toolbox.x = 200; toolbox.y = 350; - toolbox.onDialogClosed = (event:DialogEvent) -> { + var container:VBox = toolbox.findComponent('charPlayerContainer', VBox); + + toolbox.onDialogClosed = function(event:DialogEvent) { state.setUICheckboxSelected('menubarItemToggleToolboxOpponentPreview', false); } var charPlayer:CharacterPlayer = toolbox.findComponent('charPlayer'); // TODO: We need to implement character swapping in ChartEditorState. charPlayer.loadCharacter('dad'); - // charPlayer.setScale(0.5); - charPlayer.setCharacterType(CharacterType.DAD); + charPlayer.characterType = CharacterType.DAD; charPlayer.flip = false; + charPlayer.targetScale = 0.5; return toolbox; } From f959cbd9508d85c7961030119261c501ce79981c Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 8 Jun 2023 16:52:44 -0400 Subject: [PATCH 06/14] ACTUALLY move MusicBeatSubState --- source/funkin/{MusicBeatSubstate.hx => MusicBeatSubState.hx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename source/funkin/{MusicBeatSubstate.hx => MusicBeatSubState.hx} (100%) diff --git a/source/funkin/MusicBeatSubstate.hx b/source/funkin/MusicBeatSubState.hx similarity index 100% rename from source/funkin/MusicBeatSubstate.hx rename to source/funkin/MusicBeatSubState.hx From c3577b32efa9882c44509363cc866e46bd62e3a9 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 8 Jun 2023 16:53:52 -0400 Subject: [PATCH 07/14] Port changes to ChartEditorState --- .../debug/charting/ChartEditorNotePreview.hx | 2 - .../ui/debug/charting/ChartEditorState.hx | 763 +++++++++++++----- 2 files changed, 552 insertions(+), 213 deletions(-) diff --git a/source/funkin/ui/debug/charting/ChartEditorNotePreview.hx b/source/funkin/ui/debug/charting/ChartEditorNotePreview.hx index 29c844c2f..27951f079 100644 --- a/source/funkin/ui/debug/charting/ChartEditorNotePreview.hx +++ b/source/funkin/ui/debug/charting/ChartEditorNotePreview.hx @@ -1,7 +1,5 @@ package funkin.ui.debug.charting; -package funkin.ui.debug.charting; - import funkin.play.song.SongData.SongEventData; import funkin.play.song.SongData.SongNoteData; import flixel.math.FlxMath; diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 4e050b19b..c24c2db1b 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -1,25 +1,28 @@ package funkin.ui.debug.charting; import funkin.ui.debug.charting.ChartEditorCommand; -import flixel.input.keyboard.FlxKey; -import funkin.input.TurboKeyHandler; -import haxe.ui.notifications.NotificationType; -import haxe.ui.notifications.NotificationManager; -import haxe.DynamicAccess; -import haxe.io.Path; +import funkin.play.song.SongData.SongPlayableChar; +import funkin.play.character.BaseCharacter.CharacterType; +import haxe.ui.containers.dialogs.CollapsibleDialog; +import openfl.Assets; import flixel.addons.display.FlxSliceSprite; import flixel.addons.display.FlxTiledSprite; import flixel.FlxSprite; import flixel.group.FlxSpriteGroup; +import flixel.input.keyboard.FlxKey; import flixel.math.FlxPoint; import flixel.math.FlxRect; -import flixel.sound.FlxSound; +import flixel.system.FlxSound; +import flixel.tweens.FlxEase; +import flixel.tweens.FlxTween; +import flixel.tweens.misc.VarTween; import flixel.util.FlxColor; import flixel.util.FlxSort; import flixel.util.FlxTimer; import funkin.audio.visualize.PolygonSpectogram; -import funkin.audio.VoicesGroup; +import funkin.audio.VocalGroup; import funkin.input.Cursor; +import funkin.input.TurboKeyHandler; import funkin.modding.events.ScriptEvent; import funkin.play.HealthIcon; import funkin.play.song.Song; @@ -34,12 +37,15 @@ import funkin.ui.debug.charting.ChartEditorToolboxHandler.ChartEditorToolMode; import funkin.ui.haxeui.components.CharacterPlayer; import funkin.ui.haxeui.HaxeUIState; import funkin.util.Constants; -import funkin.util.FileUtil; import funkin.util.DateUtil; +import funkin.util.FileUtil; import funkin.util.SerializerUtil; +import funkin.util.WindowUtil; +import haxe.DynamicAccess; +import haxe.io.Bytes; +import haxe.io.Path; import haxe.ui.components.Label; import haxe.ui.components.Slider; -import haxe.ui.containers.dialogs.Dialog; import haxe.ui.containers.menus.MenuItem; import haxe.ui.containers.TreeView; import haxe.ui.containers.TreeViewNode; @@ -47,7 +53,8 @@ import haxe.ui.core.Component; import haxe.ui.core.Screen; import haxe.ui.events.DragEvent; import haxe.ui.events.UIEvent; -import funkin.util.WindowUtil; +import haxe.ui.notifications.NotificationManager; +import haxe.ui.notifications.NotificationType; import openfl.display.BitmapData; import openfl.geom.Rectangle; @@ -58,10 +65,11 @@ using Lambda; * Built with HaxeUI for use by both developers and modders. * * Some functionality is moved to other classes to help maintain my sanity. - * + * * @author MasterEric */ // Give other classes access to private instance fields + @:allow(funkin.ui.debug.charting.ChartEditorCommand) @:allow(funkin.ui.debug.charting.ChartEditorDialogHandler) @:allow(funkin.ui.debug.charting.ChartEditorThemeHandler) @@ -73,19 +81,19 @@ class ChartEditorState extends HaxeUIState */ // ============================== // XML Layouts - static final CHART_EDITOR_LAYOUT = Paths.ui('chart-editor/main-view'); + static final CHART_EDITOR_LAYOUT:String = Paths.ui('chart-editor/main-view'); - static final CHART_EDITOR_NOTIFBAR_LAYOUT = Paths.ui('chart-editor/components/notifbar'); - static final CHART_EDITOR_PLAYBARHEAD_LAYOUT = Paths.ui('chart-editor/components/playbar-head'); + static final CHART_EDITOR_NOTIFBAR_LAYOUT:String = Paths.ui('chart-editor/components/notifbar'); + static final CHART_EDITOR_PLAYBARHEAD_LAYOUT:String = Paths.ui('chart-editor/components/playbar-head'); - static final CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT = Paths.ui('chart-editor/toolbox/tools'); - static final CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT = Paths.ui('chart-editor/toolbox/notedata'); - static final CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT = Paths.ui('chart-editor/toolbox/eventdata'); - static final CHART_EDITOR_TOOLBOX_METADATA_LAYOUT = Paths.ui('chart-editor/toolbox/metadata'); - static final CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT = Paths.ui('chart-editor/toolbox/difficulty'); - static final CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT = Paths.ui('chart-editor/toolbox/characters'); - static final CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT = Paths.ui('chart-editor/toolbox/player-preview'); - static final CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT = Paths.ui('chart-editor/toolbox/opponent-preview'); + static final CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT:String = Paths.ui('chart-editor/toolbox/tools'); + static final CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/notedata'); + static final CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/eventdata'); + static final CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/metadata'); + static final CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT:String = Paths.ui('chart-editor/toolbox/difficulty'); + static final CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT:String = Paths.ui('chart-editor/toolbox/characters'); + static final CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT:String = Paths.ui('chart-editor/toolbox/player-preview'); + static final CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT:String = Paths.ui('chart-editor/toolbox/opponent-preview'); // Validation static final SUPPORTED_MUSIC_FORMATS:Array = ['ogg']; @@ -104,12 +112,12 @@ class ChartEditorState extends HaxeUIState /** * Number of notes in each player's strumline. */ - public static final STRUMLINE_SIZE = 4; + public static final STRUMLINE_SIZE:Int = 4; /** * The height of the menu bar in the layout. */ - static final MENU_BAR_HEIGHT = 32; + static final MENU_BAR_HEIGHT:Int = 32; /** * Duration to wait before autosaving the chart. @@ -126,10 +134,10 @@ class ChartEditorState extends HaxeUIState */ static final NOTIFICATION_DISMISS_TIME:Int = 5000; - // Start performing rapid undo after this many seconds. - static final RAPID_UNDO_DELAY:Float = 0.4; - // Perform a rapid undo every this many seconds. - static final RAPID_UNDO_INTERVAL:Float = 0.1; + /** + * Duration, in seconds, for the scroll easing animation. + */ + static final SCROLL_EASE_DURATION:Float = 0.2; // UI Element Colors // Background color tint. @@ -189,12 +197,12 @@ class ChartEditorState extends HaxeUIState function get_scrollPositionInMs():Float { - return scrollPositionInSteps * Conductor.stepCrochet; + return scrollPositionInSteps * Conductor.stepLengthMs; } function set_scrollPositionInMs(value:Float):Float { - scrollPositionInPixels = value / Conductor.stepCrochet; + scrollPositionInPixels = value / Conductor.stepLengthMs; return value; } @@ -223,7 +231,7 @@ class ChartEditorState extends HaxeUIState function get_playheadPositionInMs():Float { - return playheadPositionInSteps * Conductor.stepCrochet; + return playheadPositionInSteps * Conductor.stepLengthMs; } /** @@ -263,7 +271,7 @@ class ChartEditorState extends HaxeUIState function get_songLengthInMs():Float { - return songLengthInSteps * Conductor.stepCrochet; + return songLengthInSteps * Conductor.stepLengthMs; } function set_songLengthInMs(value:Float):Float @@ -335,6 +343,11 @@ class ChartEditorState extends HaxeUIState */ var currentOpponentCharacterPlayer:CharacterPlayer = null; + /** + * The currently selected live input style. + */ + var currentLiveInputStyle:LiveInputStyle = LiveInputStyle.None; + /** * Whether the current view is in downscroll mode. */ @@ -428,6 +441,22 @@ class ChartEditorState extends HaxeUIState return selectedDifficulty; } + /** + * The character ID for the character which is currently selected. + */ + var selectedCharacter(default, set):String = Constants.DEFAULT_CHARACTER; + + function set_selectedCharacter(value:String):String + { + selectedCharacter = value; + + // Make sure view is updated when the character changes. + noteDisplayDirty = true; + notePreviewDirty = true; + + return selectedCharacter; + } + /** * Whether the user is currently in Pattern Mode. * This overrides the chart editor's normal behavior. @@ -503,6 +532,18 @@ class ChartEditorState extends HaxeUIState */ var characterSelectDirty:Bool = true; + /** + * Whether the player preview toolbox have been modified and need to be updated. + * This happens when we switch characters. + */ + var playerPreviewDirty:Bool = true; + + /** + * Whether the opponent preview toolbox have been modified and need to be updated. + * This happens when we switch characters. + */ + var opponentPreviewDirty:Bool = true; + var isInPlaytestMode:Bool = false; /** @@ -581,7 +622,7 @@ class ChartEditorState extends HaxeUIState * The Dialog components representing the currently available tool windows. * Dialogs are retained here even when collapsed or hidden. */ - var activeToolboxes:Map = new Map(); + var activeToolboxes:Map = new Map(); /** * AUDIO AND SOUND DATA @@ -594,18 +635,19 @@ class ChartEditorState extends HaxeUIState var audioInstTrack:FlxSound; /** - * The audio track for the vocals. + * The raw byte data for the instrumental audio track. */ - var audioVocalTrackGroup:VoicesGroup; + var audioInstTrackData:Bytes = null; /** - * A map of the audio tracks for each character's vocals. - * - Keys are the character IDs. - * - Values are the FlxSound objects to play that character's vocals. - * - * When switching characters, the elements of the VoicesGroup will be swapped to match the new character. + * The audio track for the vocals. */ - var audioVocalTracks:Map = new Map(); + var audioVocalTrackGroup:VocalGroup; + + /** + * The raw byte data for the vocal audio tracks. + */ + var audioVocalTrackData:Map = []; /** * CHART DATA @@ -640,7 +682,7 @@ class ChartEditorState extends HaxeUIState function get_currentSongMetadata():SongMetadata { - var result = songMetadata.get(selectedVariation); + var result:SongMetadata = songMetadata.get(selectedVariation); if (result == null) { result = new SongMetadata('Dad Battle', 'Kawai Sprite', selectedVariation); @@ -662,7 +704,7 @@ class ChartEditorState extends HaxeUIState function get_currentSongChartData():SongChartData { - var result = songChartData.get(selectedVariation); + var result:SongChartData = songChartData.get(selectedVariation); if (result == null) { result = new SongChartData(1.0, [], []); @@ -684,7 +726,7 @@ class ChartEditorState extends HaxeUIState function get_currentSongChartScrollSpeed():Float { - var result = currentSongChartData.scrollSpeed.get(selectedDifficulty); + var result:Null = currentSongChartData.scrollSpeed.get(selectedDifficulty); if (result == null) { // Initialize to the default value if not set. @@ -707,11 +749,12 @@ class ChartEditorState extends HaxeUIState function get_currentSongChartNoteData():Array { - var result = currentSongChartData.notes.get(selectedDifficulty); + var result:Array = currentSongChartData.notes.get(selectedDifficulty); if (result == null) { // Initialize to the default value if not set. result = []; + trace('Initializing blank note data for difficulty ' + selectedDifficulty); currentSongChartData.notes.set(selectedDifficulty, result); return result; } @@ -819,6 +862,59 @@ class ChartEditorState extends HaxeUIState return currentSongMetadata.artist = value; } + var currentSongPlayableCharacters(get, null):Array; + + function get_currentSongPlayableCharacters():Array + { + return currentSongMetadata.playData.playableChars.keys().array(); + } + + var currentSongCharacterPlayer(get, set):String; + + function get_currentSongCharacterPlayer():String + { + // Validate selected character before returning it. + if (!currentSongPlayableCharacters.contains(selectedCharacter)) + { + trace('Invalid character selected: ' + selectedCharacter); + selectedCharacter = currentSongPlayableCharacters[0]; + } + + return selectedCharacter; + } + + function set_currentSongCharacterPlayer(value:String):String + { + if (!currentSongPlayableCharacters.contains(value)) + { + trace('Invalid character selected: ' + value); + return value; + } + + return selectedCharacter = value; + } + + var currentSongCharacterOpponent(get, set):String; + + function get_currentSongCharacterOpponent():String + { + // Validate selected character before returning it. + if (!currentSongPlayableCharacters.contains(selectedCharacter)) + { + trace('Invalid character selected: ' + selectedCharacter); + selectedCharacter = currentSongPlayableCharacters[0]; + } + + var playableCharData:SongPlayableChar = currentSongMetadata.playData.playableChars.get(selectedCharacter); + return playableCharData.opponent; + } + + function set_currentSongCharacterOpponent(value:String):String + { + var playableCharData:SongPlayableChar = currentSongMetadata.playData.playableChars.get(selectedCharacter); + return playableCharData.opponent = value; + } + /** * RENDER OBJECTS */ @@ -866,17 +962,11 @@ class ChartEditorState extends HaxeUIState */ var gridSpectrogram:PolygonSpectogram; - /** - * The rectangle used for the note preview area. - * Should span the full height of the song. We scribble on this to draw the preview. - */ - var notePreviewBitmap:BitmapData; - /** * The sprite used to display the note preview area. * We move this up and down to scroll the preview. */ - var notePreviewSprite:FlxSprite; + var notePreview:ChartEditorNotePreview; /** * The rectangular sprite used for rendering the selection box. @@ -935,6 +1025,8 @@ class ChartEditorState extends HaxeUIState currentTheme = ChartEditorTheme.Light; buildGrid(); + // buildSpectrogram(audioInstTrack); + buildNotePreview(); buildSelectionBox(); // Add the HaxeUI components after the grid so they're on top. @@ -961,7 +1053,7 @@ class ChartEditorState extends HaxeUIState // Initialize the song chart data. songChartData = new Map(); - audioVocalTrackGroup = new VoicesGroup(); + audioVocalTrackGroup = new VocalGroup(); } /** @@ -990,13 +1082,13 @@ class ChartEditorState extends HaxeUIState gridGhostNote = new ChartEditorNoteSprite(this); gridGhostNote.alpha = 0.6; - gridGhostNote.noteData = new SongNoteData(-1, -1, 0, ""); + gridGhostNote.noteData = new SongNoteData(-1, -1, 0, ''); gridGhostNote.visible = false; add(gridGhostNote); gridGhostEvent = new ChartEditorEventSprite(this); gridGhostEvent.alpha = 0.6; - gridGhostEvent.eventData = new SongEventData(-1, "", {}); + gridGhostEvent.eventData = new SongEventData(-1, '', {}); gridGhostEvent.visible = false; add(gridGhostEvent); @@ -1010,15 +1102,15 @@ class ChartEditorState extends HaxeUIState gridPlayhead = new FlxSpriteGroup(); add(gridPlayhead); - var playheadWidth = GRID_SIZE * (STRUMLINE_SIZE * 2 + 1) + (PLAYHEAD_SCROLL_AREA_WIDTH * 2); - var playheadBaseYPos = MENU_BAR_HEIGHT + GRID_TOP_PAD; + var playheadWidth:Int = GRID_SIZE * (STRUMLINE_SIZE * 2 + 1) + (PLAYHEAD_SCROLL_AREA_WIDTH * 2); + var playheadBaseYPos:Float = MENU_BAR_HEIGHT + GRID_TOP_PAD; gridPlayhead.setPosition(gridTiledSprite.x, playheadBaseYPos); - var playheadSprite = new FlxSprite().makeGraphic(playheadWidth, PLAYHEAD_HEIGHT, PLAYHEAD_COLOR); + var playheadSprite:FlxSprite = new FlxSprite().makeGraphic(playheadWidth, PLAYHEAD_HEIGHT, PLAYHEAD_COLOR); playheadSprite.x = -PLAYHEAD_SCROLL_AREA_WIDTH; playheadSprite.y = 0; gridPlayhead.add(playheadSprite); - var playheadBlock = ChartEditorThemeHandler.buildPlayheadBlock(); + var playheadBlock:FlxSprite = ChartEditorThemeHandler.buildPlayheadBlock(); playheadBlock.x = -PLAYHEAD_SCROLL_AREA_WIDTH; playheadBlock.y = -PLAYHEAD_HEIGHT / 2; gridPlayhead.add(playheadBlock); @@ -1048,7 +1140,7 @@ class ChartEditorState extends HaxeUIState setSelectionBoxBounds(); } - function setSelectionBoxBounds(?bounds:FlxRect = null):Void + function setSelectionBoxBounds(bounds:FlxRect = null):Void { if (bounds == null) { @@ -1066,17 +1158,21 @@ class ChartEditorState extends HaxeUIState } } + function buildNotePreview():Void + { + var height:Int = FlxG.height - MENU_BAR_HEIGHT - GRID_TOP_PAD - 200; + notePreview = new ChartEditorNotePreview(height); + notePreview.y = MENU_BAR_HEIGHT + GRID_TOP_PAD; + add(notePreview); + } + function buildSpectrogram(target:FlxSound):Void { - gridSpectrogram = new PolygonSpectogram(target, SPECTROGRAM_COLOR, FlxG.height / 2, Math.floor(FlxG.height / 2)); - // Halfway through the grid. - // gridSpectrogram.x = gridTiledSprite.x + STRUMLINE_SIZE * GRID_SIZE; - // gridSpectrogram.y = gridTiledSprite.y; - gridSpectrogram.x = 200; - gridSpectrogram.y = 200; - gridSpectrogram.visType = STATIC; // We move the spectrogram manually. + gridSpectrogram = new PolygonSpectogram(FlxG.sound.music, FlxColor.RED, FlxG.height / 2, Math.floor(FlxG.height / 2)); + gridSpectrogram.x += 170; + gridSpectrogram.scrollFactor.set(); gridSpectrogram.waveAmplitude = 50; - gridSpectrogram.scrollFactor.set(0, 0); + gridSpectrogram.visType = UPDATED; add(gridSpectrogram); } @@ -1113,7 +1209,7 @@ class ChartEditorState extends HaxeUIState playbarHead.allowFocus = false; playbarHead.width = FlxG.width; playbarHead.height = 10; - playbarHead.styleString = "padding-left: 0px; padding-right: 0px; border-left: 0px; border-right: 0px;"; + playbarHead.styleString = 'padding-left: 0px; padding-right: 0px; border-left: 0px; border-right: 0px;'; playbarHead.onDragStart = function(_:DragEvent) { playbarHeadDragging = true; @@ -1142,11 +1238,17 @@ class ChartEditorState extends HaxeUIState if (playbarHeadDraggingWasPlaying) { playbarHeadDraggingWasPlaying = false; - startAudioPlayback(); + // Disabled code to resume song playback on drag. + // startAudioPlayback(); } } add(playbarHeadLayout); + + // Setup notifications. + @:privateAccess + // NotificationManager.GUTTER_SIZE = 56; + NotificationManager.GUTTER_SIZE = 20; } /** @@ -1165,8 +1267,10 @@ class ChartEditorState extends HaxeUIState // Add functionality to the menu items. addUIClickListener('menubarItemNewChart', _ -> ChartEditorDialogHandler.openWelcomeDialog(this, true)); + addUIClickListener('menubarItemOpenChart', _ -> ChartEditorDialogHandler.openBrowseWizard(this, true)); addUIClickListener('menubarItemSaveChartAs', _ -> exportAllSongData()); addUIClickListener('menubarItemLoadInst', _ -> ChartEditorDialogHandler.openUploadInstDialog(this, true)); + addUIClickListener('menubarItemImportChart', _ -> ChartEditorDialogHandler.openImportChartDialog(this, 'legacy', true)); addUIClickListener('menubarItemUndo', _ -> undoLastCommand()); @@ -1174,10 +1278,21 @@ class ChartEditorState extends HaxeUIState addUIClickListener('menubarItemCopy', function(_) { // Doesn't use a command because it's not undoable. + + // Calculate a single time offset for all the notes and events. + var timeOffset:Null = currentNoteSelection.length > 0 ? Std.int(currentNoteSelection[0].time) : null; + if (currentEventSelection.length > 0) + { + if (timeOffset == null || currentEventSelection[0].time < timeOffset) + { + timeOffset = Std.int(currentEventSelection[0].time); + } + } + SongDataUtils.writeItemsToClipboard( { - notes: SongDataUtils.buildNoteClipboard(currentNoteSelection), - events: SongDataUtils.buildEventClipboard(currentEventSelection), + notes: SongDataUtils.buildNoteClipboard(currentNoteSelection, timeOffset), + events: SongDataUtils.buildEventClipboard(currentEventSelection, timeOffset), }); }); @@ -1215,6 +1330,10 @@ class ChartEditorState extends HaxeUIState // addUIClickListener('menubarItemSelectBeforeCursor', _ -> doSomething()); // addUIClickListener('menubarItemSelectAfterCursor', _ -> doSomething()); + addUIChangeListener('menubarItemInputStyleGroup', function(event:UIEvent) { + trace('Change input style: ${event.target}'); + }); + addUIClickListener('menubarItemAbout', _ -> ChartEditorDialogHandler.openAboutDialog(this)); addUIClickListener('menubarItemUserGuide', _ -> ChartEditorDialogHandler.openUserGuideDialog(this)); @@ -1258,11 +1377,13 @@ class ChartEditorState extends HaxeUIState var playbackSpeedLabel:Label = findComponent('menubarLabelPlaybackSpeed', Label); addUIChangeListener('menubarItemPlaybackSpeed', function(event:UIEvent) { var pitch:Float = event.value * 2.0 / 100.0; + pitch = Math.floor(pitch / 0.25) * 0.25; // Round to nearest 0.25. #if FLX_PITCH if (audioInstTrack != null) audioInstTrack.pitch = pitch; if (audioVocalTrackGroup != null) audioVocalTrackGroup.pitch = pitch; #end - playbackSpeedLabel.text = 'Playback Speed - ${Std.int(pitch * 100) / 100}x'; + var pitchDisplay:Float = Std.int(pitch * 100) / 100; // Round to 2 decimal places. + playbackSpeedLabel.text = 'Playback Speed - ${pitchDisplay}x'; }); addUIChangeListener('menubarItemToggleToolboxTools', @@ -1371,21 +1492,7 @@ class ChartEditorState extends HaxeUIState // DEBUG #if debug - if (FlxG.keys.justPressed.F) - { - NotificationManager.instance.addNotification( - { - title: 'This is a Notification', - body: 'Hello, world!', - type: NotificationType.Info, - expiryMs: NOTIFICATION_DISMISS_TIME - // styleNames: 'cssStyleName', - // icon: 'assetPath', - // actions: ['action1', 'action2'] - }); - } - - if (FlxG.keys.justPressed.E) + if (FlxG.keys.justPressed.E && !isHaxeUIDialogOpen) { currentSongMetadata.timeChanges[0].timeSignatureNum = (currentSongMetadata.timeChanges[0].timeSignatureNum == 4 ? 3 : 4); } @@ -1394,9 +1501,9 @@ class ChartEditorState extends HaxeUIState // Right align the BF health icon. // Base X position to the right of the grid. - var baseHealthIconXPos = gridTiledSprite.x + GRID_SIZE * (STRUMLINE_SIZE * 2 + 1) + 15; + var baseHealthIconXPos:Float = gridTiledSprite.x + GRID_SIZE * (STRUMLINE_SIZE * 2 + 1) + 15; // Will be 0 when not bopping. When bopping, will increase to push the icon left. - var healthIconOffset = healthIconBF.width - (HealthIcon.HEALTH_ICON_SIZE * 0.5); + var healthIconOffset:Float = healthIconBF.width - (HealthIcon.HEALTH_ICON_SIZE * 0.5); healthIconBF.x = baseHealthIconXPos - healthIconOffset; } @@ -1430,8 +1537,9 @@ class ChartEditorState extends HaxeUIState healthIconBF.onStepHit(Conductor.currentStep); } - // if (shouldPlayMetronome) - // playMetronomeTick(false); + // Updating these every step keeps it more accurate. + // playerPreviewDirty = true; + // opponentPreviewDirty = true; return true; } @@ -1444,29 +1552,36 @@ class ChartEditorState extends HaxeUIState // Don't scroll when the cursor is over the UI. if (isCursorOverHaxeUI) return; - // Amount to scroll the grid. - var scrollAmount:Float = 0; - // Amount to scroll the playhead relative to the grid. - var playheadAmount:Float = 0; - var shouldPause:Bool = false; + var scrollAmount:Float = 0; // Amount to scroll the grid. + var playheadAmount:Float = 0; // Amount to scroll the playhead relative to the grid. + var shouldPause:Bool = false; // Whether to pause the song when scrolling. + var shouldEase:Bool = false; // Whether to ease the scroll. // Up Arrow = Scroll Up - if (upKeyHandler.activated) + if (upKeyHandler.activated && currentLiveInputStyle != LiveInputStyle.WASD) { scrollAmount = -GRID_SIZE * 0.25 * 5.0; shouldPause = true; } // Down Arrow = Scroll Down - if (downKeyHandler.activated) + if (downKeyHandler.activated && currentLiveInputStyle != LiveInputStyle.WASD) { scrollAmount = GRID_SIZE * 0.25 * 5.0; shouldPause = true; } - // PAGE UP = Jump Up 1 Measure + // PAGE UP = Jump up to nearest measure if (pageUpKeyHandler.activated) { - scrollAmount = -GRID_SIZE * 4 * Conductor.beatsPerMeasure; + var measureHeight:Float = GRID_SIZE * 4 * Conductor.beatsPerMeasure; + var targetScrollPosition:Float = Math.floor(scrollPositionInPixels / measureHeight) * measureHeight; + // If we would move less than one grid, instead move to the top of the previous measure. + if (Math.abs(targetScrollPosition - scrollPositionInPixels) < GRID_SIZE) + { + targetScrollPosition -= GRID_SIZE * 4 * Conductor.beatsPerMeasure; + } + scrollAmount = targetScrollPosition - scrollPositionInPixels; + shouldPause = true; } if (playbarButtonPressed == 'playbarBack') @@ -1476,10 +1591,18 @@ class ChartEditorState extends HaxeUIState shouldPause = true; } - // PAGE DOWN = Jump Down 1 Measure + // PAGE DOWN = Jump down to nearest measure if (pageDownKeyHandler.activated) { - scrollAmount = GRID_SIZE * 4 * Conductor.beatsPerMeasure; + var measureHeight:Float = GRID_SIZE * 4 * Conductor.beatsPerMeasure; + var targetScrollPosition:Float = Math.ceil(scrollPositionInPixels / measureHeight) * measureHeight; + // If we would move less than one grid, instead move to the top of the next measure. + if (Math.abs(targetScrollPosition - scrollPositionInPixels) < GRID_SIZE) + { + targetScrollPosition += GRID_SIZE * 4 * Conductor.beatsPerMeasure; + } + scrollAmount = targetScrollPosition - scrollPositionInPixels; + shouldPause = true; } if (playbarButtonPressed == 'playbarForward') @@ -1557,12 +1680,26 @@ class ChartEditorState extends HaxeUIState shouldPause = true; } - // Apply the scroll amount. - this.scrollPositionInPixels += scrollAmount; - this.playheadPositionInPixels += playheadAmount; + if (Math.abs(scrollAmount) > GRID_SIZE * 8) + { + shouldEase = true; + } // Resync the conductor and audio tracks. - if (scrollAmount != 0 || playheadAmount != 0) moveSongToScrollPosition(); + if (scrollAmount != 0 || playheadAmount != 0) + { + this.playheadPositionInPixels += playheadAmount; + if (shouldEase) + { + easeSongToScrollPosition(this.scrollPositionInPixels + scrollAmount); + } + else + { + // Apply the scroll amount. + this.scrollPositionInPixels += scrollAmount; + moveSongToScrollPosition(); + } + } if (shouldPause) stopAudioPlayback(); } @@ -1608,8 +1745,8 @@ class ChartEditorState extends HaxeUIState function handleCursor():Void { // Note: If a menu is open in HaxeUI, don't handle cursor behavior. - var shouldHandleCursor = !isCursorOverHaxeUI || (selectionBoxStartPos != null); - var eventColumn = (STRUMLINE_SIZE * 2 + 1) - 1; + var shouldHandleCursor:Bool = !isCursorOverHaxeUI || (selectionBoxStartPos != null); + var eventColumn:Int = (STRUMLINE_SIZE * 2 + 1) - 1; if (shouldHandleCursor) { @@ -1619,7 +1756,7 @@ class ChartEditorState extends HaxeUIState var cursorX:Float = FlxG.mouse.screenX - gridTiledSprite.x; var cursorY:Float = FlxG.mouse.screenY - gridTiledSprite.y; - var overlapsSelectionBorder = overlapsGrid + var overlapsSelectionBorder:Bool = overlapsGrid && (cursorX % 40) < (GRID_SELECTION_BORDER_WIDTH / 2) || (cursorX % 40) > (40 - (GRID_SELECTION_BORDER_WIDTH / 2)) || (cursorY % 40) < (GRID_SELECTION_BORDER_WIDTH / 2) || (cursorY % 40) > (40 - (GRID_SELECTION_BORDER_WIDTH / 2)); @@ -1634,6 +1771,13 @@ class ChartEditorState extends HaxeUIState { selectionBoxStartPos = new FlxPoint(FlxG.mouse.screenX, FlxG.mouse.screenY); } + else + { + trace('Clicked outside grid, deselecting all items.'); + + // Deselect all items. + performCommand(new DeselectAllItemsCommand(currentNoteSelection, currentEventSelection)); + } } if (gridPlayheadScrollAreaPressed) @@ -1667,7 +1811,7 @@ class ChartEditorState extends HaxeUIState // The song position of the cursor, in steps. var cursorFractionalStep:Float = cursorY / GRID_SIZE / (16 / noteSnapQuant); var cursorStep:Int = Std.int(Math.floor(cursorFractionalStep)); - var cursorMs:Float = cursorStep * Conductor.stepCrochet * (16 / noteSnapQuant); + var cursorMs:Float = cursorStep * Conductor.stepLengthMs * (16 / noteSnapQuant); // The direction value for the column at the cursor. var cursorColumn:Int = Math.floor(cursorX / GRID_SIZE); if (cursorColumn < 0) cursorColumn = 0; @@ -1705,7 +1849,7 @@ class ChartEditorState extends HaxeUIState // We released the mouse. Select the notes in the box. var cursorFractionalStepStart:Float = cursorYStart / GRID_SIZE; var cursorStepStart:Int = Math.floor(cursorFractionalStepStart); - var cursorMsStart:Float = cursorStepStart * Conductor.stepCrochet; + var cursorMsStart:Float = cursorStepStart * Conductor.stepLengthMs; var cursorColumnBase:Int = Math.floor(cursorX / GRID_SIZE); var cursorColumnBaseStart:Int = Math.floor(cursorXStart / GRID_SIZE); @@ -1770,6 +1914,13 @@ class ChartEditorState extends HaxeUIState else { // We made a selection box, but it didn't select anything. + + if (!FlxG.keys.pressed.CONTROL) + { + trace('Clicked and dragged outside grid, deselecting all items.'); + // Deselect all items. + performCommand(new DeselectAllItemsCommand(currentNoteSelection, currentEventSelection)); + } } } else @@ -1783,8 +1934,26 @@ class ChartEditorState extends HaxeUIState } else { + // Scroll the screen if the mouse is above or below the grid. + if (FlxG.mouse.screenY < MENU_BAR_HEIGHT) + { + // Scroll up. + var diff:Float = MENU_BAR_HEIGHT - FlxG.mouse.screenY; + scrollPositionInPixels -= diff * 0.5; // Too fast! + trace('Scroll up: ' + diff); + moveSongToScrollPosition(); + } + else if (FlxG.mouse.screenY > playbarHeadLayout.y) + { + // Scroll down. + var diff:Float = FlxG.mouse.screenY - playbarHeadLayout.y; + scrollPositionInPixels += diff * 0.5; // Too fast! + trace('Scroll down: ' + diff); + moveSongToScrollPosition(); + } + // Render the selection box. - var selectionRect = new FlxRect(); + var selectionRect:FlxRect = new FlxRect(); selectionRect.x = Math.min(FlxG.mouse.screenX, selectionBoxStartPos.x); selectionRect.y = Math.min(FlxG.mouse.screenY, selectionBoxStartPos.y); selectionRect.width = Math.abs(FlxG.mouse.screenX - selectionBoxStartPos.x); @@ -1869,7 +2038,14 @@ class ChartEditorState extends HaxeUIState } else { - // If we clicked and released outside the grid, do nothing. + // If we clicked and released outside the grid. + + if (!FlxG.keys.pressed.CONTROL) + { + trace('Clicked outside grid, deselecting all items.'); + // Deselect all items. + performCommand(new DeselectAllItemsCommand(currentNoteSelection, currentEventSelection)); + } } } } @@ -1877,12 +2053,12 @@ class ChartEditorState extends HaxeUIState { // Handle extending the note as you drag. - // Since use Math.floor and stepCrochet here, the hold notes will be beat snapped. - var dragLengthSteps:Float = Math.floor((cursorMs - currentPlaceNoteData.time) / Conductor.stepCrochet); + // Since use Math.floor and stepLengthMs here, the hold notes will be beat snapped. + var dragLengthSteps:Float = Math.floor((cursorMs - currentPlaceNoteData.time) / Conductor.stepLengthMs); // Without this, the newly placed note feels too short compared to the user's input. var INCREMENT:Float = 1.0; - var dragLengthMs:Float = (dragLengthSteps + INCREMENT) * Conductor.stepCrochet; + var dragLengthMs:Float = (dragLengthSteps + INCREMENT) * Conductor.stepLengthMs; // TODO: Add and update some sort of preview? @@ -2187,10 +2363,10 @@ class ChartEditorState extends HaxeUIState } // Get the position the note should be at. - var noteTimePixels:Float = noteData.time / Conductor.stepCrochet * GRID_SIZE; + var noteTimePixels:Float = noteData.time / Conductor.stepLengthMs * GRID_SIZE; // Make sure the note appears when scrolling up. - var modifiedViewAreaTop = viewAreaTop - GRID_SIZE; + var modifiedViewAreaTop:Float = viewAreaTop - GRID_SIZE; if (noteTimePixels < modifiedViewAreaTop || noteTimePixels > viewAreaBottom) continue; @@ -2213,7 +2389,7 @@ class ChartEditorState extends HaxeUIState { // If the note is a hold, we need to make sure it's long enough. var noteLengthMs:Float = noteSprite.noteData.length; - var noteLengthSteps:Float = (noteLengthMs / Conductor.stepCrochet); + var noteLengthSteps:Float = (noteLengthMs / Conductor.stepLengthMs); var lastNoteSprite:ChartEditorNoteSprite = noteSprite; while (noteLengthSteps > 0) @@ -2237,7 +2413,7 @@ class ChartEditorState extends HaxeUIState // Make sure the last note sprite shows the end cap properly. lastNoteSprite.childNoteSprite = null; - // var noteLengthPixels:Float = (noteLengthMs / Conductor.stepCrochet + 1) * GRID_SIZE; + // var noteLengthPixels:Float = (noteLengthMs / Conductor.stepLengthMs + 1) * GRID_SIZE; // add(new FlxSprite(noteSprite.x, noteSprite.y - renderedNotes.y + noteLengthPixels).makeGraphic(40, 2, 0xFFFF0000)); } } @@ -2252,10 +2428,10 @@ class ChartEditorState extends HaxeUIState } // Get the position the event should be at. - var eventTimePixels:Float = eventData.time / Conductor.stepCrochet * GRID_SIZE; + var eventTimePixels:Float = eventData.time / Conductor.stepLengthMs * GRID_SIZE; // Make sure the event appears when scrolling up. - var modifiedViewAreaTop = viewAreaTop - GRID_SIZE; + var modifiedViewAreaTop:Float = viewAreaTop - GRID_SIZE; if (eventTimePixels < modifiedViewAreaTop || eventTimePixels > viewAreaBottom) continue; @@ -2334,8 +2510,8 @@ class ChartEditorState extends HaxeUIState playbarHeadLayout.x = 4; playbarHeadLayout.y = FlxG.height - 48 - 8; - var songPos = Conductor.songPosition; - var songRemaining = songLengthInMs - songPos; + var songPos:Float = Conductor.songPosition; + var songRemaining:Float = Math.max(songLengthInMs - songPos, 0.0); // Move the playhead to match the song position, if we aren't dragging it. if (!playbarHeadDragging) @@ -2480,7 +2656,7 @@ class ChartEditorState extends HaxeUIState difficultySelectDirty = false; // Manage the Select Difficulty tree view. - var difficultyToolbox = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT); + var difficultyToolbox:CollapsibleDialog = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT); if (difficultyToolbox == null) return; var treeView:TreeView = difficultyToolbox.findComponent('difficultyToolboxTree'); @@ -2489,30 +2665,28 @@ class ChartEditorState extends HaxeUIState // Clear the tree view so we can rebuild it. treeView.clearNodes(); - var treeSong = treeView.addNode({id: 'stv_song', text: 'S: $currentSongName', icon: "haxeui-core/styles/default/haxeui_tiny.png"}); + var treeSong:TreeViewNode = treeView.addNode({id: 'stv_song', text: 'S: $currentSongName', icon: 'haxeui-core/styles/default/haxeui_tiny.png'}); treeSong.expanded = true; for (curVariation in availableVariations) { var variationMetadata:SongMetadata = songMetadata.get(curVariation); - var treeVariation = treeSong.addNode( + var treeVariation:TreeViewNode = treeSong.addNode( { id: 'stv_variation_$curVariation', - text: 'V: ${curVariation.toTitleCase()}', - // icon: "haxeui-core/styles/default/haxeui_tiny.png" + text: 'V: ${curVariation.toTitleCase()}' }); treeVariation.expanded = true; - var difficultyList = variationMetadata.playData.difficulties; + var difficultyList:Array = variationMetadata.playData.difficulties; for (difficulty in difficultyList) { - var treeDifficulty = treeVariation.addNode( + var _treeDifficulty:TreeViewNode = treeVariation.addNode( { id: 'stv_difficulty_${curVariation}_$difficulty', - text: 'D: ${difficulty.toTitleCase()}', - // icon: "haxeui-core/styles/default/haxeui_tiny.png" + text: 'D: ${difficulty.toTitleCase()}' }); } } @@ -2525,25 +2699,71 @@ class ChartEditorState extends HaxeUIState function handlePlayerPreviewToolbox():Void { // Manage the Select Difficulty tree view. - var charPreviewToolbox = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT); + var charPreviewToolbox:CollapsibleDialog = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT); if (charPreviewToolbox == null) return; var charPlayer:CharacterPlayer = charPreviewToolbox.findComponent('charPlayer'); if (charPlayer == null) return; currentPlayerCharacterPlayer = charPlayer; + + if (playerPreviewDirty) + { + playerPreviewDirty = false; + + if (currentSongCharacterPlayer != charPlayer.charId) + { + healthIconBF.characterId = currentSongCharacterPlayer; + + charPlayer.loadCharacter(currentSongCharacterPlayer); + charPlayer.characterType = CharacterType.BF; + charPlayer.flip = true; + charPlayer.targetScale = 0.5; + + charPreviewToolbox.title = 'Player Preview - ${charPlayer.charName}'; + } + + if (charPreviewToolbox != null && !charPreviewToolbox.minimized) + { + charPreviewToolbox.width = charPlayer.width + 32; + charPreviewToolbox.height = charPlayer.height + 64; + } + } } function handleOpponentPreviewToolbox():Void { // Manage the Select Difficulty tree view. - var charPreviewToolbox = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT); + var charPreviewToolbox:CollapsibleDialog = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT); if (charPreviewToolbox == null) return; var charPlayer:CharacterPlayer = charPreviewToolbox.findComponent('charPlayer'); if (charPlayer == null) return; currentOpponentCharacterPlayer = charPlayer; + + if (opponentPreviewDirty) + { + opponentPreviewDirty = false; + + if (currentSongCharacterOpponent != charPlayer.charId) + { + healthIconDad.characterId = currentSongCharacterOpponent; + + charPlayer.loadCharacter(currentSongCharacterOpponent); + charPlayer.characterType = CharacterType.DAD; + charPlayer.flip = false; + charPlayer.targetScale = 0.5; + + charPreviewToolbox.title = 'Opponent Preview - ${charPlayer.charName}'; + } + + if (charPreviewToolbox != null && !charPreviewToolbox.minimized) + { + charPreviewToolbox.width = charPlayer.width + 32; + charPreviewToolbox.height = charPlayer.height + 64; + } + } } override function dispatchEvent(event:ScriptEvent):Void @@ -2588,7 +2808,8 @@ class ChartEditorState extends HaxeUIState if (treeView == null) return null; - var result = treeView.findNodeByPath('stv_song/stv_variation_$selectedVariation/stv_difficulty_${selectedVariation}_$selectedDifficulty', 'id'); + var result:TreeViewNode = treeView.findNodeByPath('stv_song/stv_variation_$selectedVariation/stv_difficulty_${selectedVariation}_$selectedDifficulty', + 'id'); if (result == null) return null; @@ -2612,8 +2833,8 @@ class ChartEditorState extends HaxeUIState switch (targetNode.data.id.split('_')[1]) { case 'difficulty': - var variation = targetNode.data.id.split('_')[2]; - var difficulty = targetNode.data.id.split('_')[3]; + var variation:String = targetNode.data.id.split('_')[2]; + var difficulty:String = targetNode.data.id.split('_')[3]; if (variation != null && difficulty != null) { @@ -2660,12 +2881,10 @@ class ChartEditorState extends HaxeUIState { notePreviewDirty = false; - var PREVIEW_WIDTH:Int = GRID_SIZE * 2; - var STEP_HEIGHT:Int = 1; - var PREVIEW_HEIGHT:Int = Std.int(Conductor.getTimeInSteps(audioInstTrack.length) * STEP_HEIGHT); - - notePreviewBitmap = new BitmapData(PREVIEW_WIDTH, PREVIEW_HEIGHT, true); - notePreviewBitmap.fillRect(new Rectangle(0, 0, PREVIEW_WIDTH, PREVIEW_HEIGHT), PREVIEW_BG_COLOR); + // TODO: Only update the notes that have changed. + notePreview.erase(); + notePreview.addNotes(currentSongChartNoteData, Std.int(songLengthInMs)); + notePreview.addEvents(currentSongChartEventData, Std.int(songLengthInMs)); } } @@ -2694,7 +2913,7 @@ class ChartEditorState extends HaxeUIState { // Disable the Undo button. undoButton.disabled = true; - undoButton.text = "Undo"; + undoButton.text = 'Undo'; } else { @@ -2705,7 +2924,7 @@ class ChartEditorState extends HaxeUIState } else { - trace("undoButton is null"); + trace('undoButton is null'); } var redoButton:MenuItem = findComponent('menubarItemRedo', MenuItem); @@ -2716,7 +2935,7 @@ class ChartEditorState extends HaxeUIState { // Disable the Redo button. redoButton.disabled = true; - redoButton.text = "Redo"; + redoButton.text = 'Redo'; } else { @@ -2727,7 +2946,7 @@ class ChartEditorState extends HaxeUIState } else { - trace("redoButton is null"); + trace('redoButton is null'); } } } @@ -2743,13 +2962,16 @@ class ChartEditorState extends HaxeUIState { // If middle mouse panning during song playback, we move ONLY the playhead, without scrolling. Neat! - var oldStepTime = Conductor.currentStepTime; - var oldSongPosition = Conductor.songPosition; + var oldStepTime:Float = Conductor.currentStepTime; + var oldSongPosition:Float = Conductor.songPosition; Conductor.update(audioInstTrack.time); handleHitsounds(oldSongPosition, Conductor.songPosition); // Resync vocals. - if (Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100) audioVocalTrackGroup.time = audioInstTrack.time; - var diffStepTime = Conductor.currentStepTime - oldStepTime; + if (audioVocalTrackGroup != null && Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100) + { + audioVocalTrackGroup.time = audioInstTrack.time; + } + var diffStepTime:Float = Conductor.currentStepTime - oldStepTime; // Move the playhead. playheadPositionInPixels += diffStepTime * GRID_SIZE; @@ -2759,12 +2981,14 @@ class ChartEditorState extends HaxeUIState else { // Else, move the entire view. - var oldSongPosition = Conductor.songPosition; + var oldSongPosition:Float = Conductor.songPosition; Conductor.update(audioInstTrack.time); handleHitsounds(oldSongPosition, Conductor.songPosition); // Resync vocals. - if (audioVocalTrackGroup != null - && Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100) audioVocalTrackGroup.time = audioInstTrack.time; + if (audioVocalTrackGroup != null && Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100) + { + audioVocalTrackGroup.time = audioInstTrack.time; + } // We need time in fractional steps here to allow the song to actually play. // Also account for a potentially offset playhead. @@ -2830,7 +3054,6 @@ class ChartEditorState extends HaxeUIState { if (audioInstTrack != null) audioInstTrack.play(); if (audioVocalTrackGroup != null) audioVocalTrackGroup.play(); - if (audioVocalTrackGroup != null) audioVocalTrackGroup.play(); setComponentText('playbarPlay', '||'); } @@ -2839,7 +3062,6 @@ class ChartEditorState extends HaxeUIState { if (audioInstTrack != null) audioInstTrack.pause(); if (audioVocalTrackGroup != null) audioVocalTrackGroup.pause(); - if (audioVocalTrackGroup != null) audioVocalTrackGroup.pause(); setComponentText('playbarPlay', '>'); } @@ -2861,23 +3083,42 @@ class ChartEditorState extends HaxeUIState function handlePlayhead():Void { // Place notes at the playhead. - // TODO: Add the ability to switch modes. - if (true) + switch (currentLiveInputStyle) { - if (FlxG.keys.justPressed.ONE) placeNoteAtPlayhead(0); - if (FlxG.keys.justPressed.TWO) placeNoteAtPlayhead(1); - if (FlxG.keys.justPressed.THREE) placeNoteAtPlayhead(2); - if (FlxG.keys.justPressed.FOUR) placeNoteAtPlayhead(3); - if (FlxG.keys.justPressed.FIVE) placeNoteAtPlayhead(4); - if (FlxG.keys.justPressed.SIX) placeNoteAtPlayhead(5); - if (FlxG.keys.justPressed.SEVEN) placeNoteAtPlayhead(6); - if (FlxG.keys.justPressed.EIGHT) placeNoteAtPlayhead(7); + case LiveInputStyle.WASD: + if (FlxG.keys.justPressed.A) placeNoteAtPlayhead(0); + if (FlxG.keys.justPressed.S) placeNoteAtPlayhead(1); + if (FlxG.keys.justPressed.W) placeNoteAtPlayhead(2); + if (FlxG.keys.justPressed.D) placeNoteAtPlayhead(3); + + if (FlxG.keys.justPressed.LEFT) placeNoteAtPlayhead(4); + if (FlxG.keys.justPressed.DOWN) placeNoteAtPlayhead(5); + if (FlxG.keys.justPressed.UP) placeNoteAtPlayhead(6); + if (FlxG.keys.justPressed.RIGHT) placeNoteAtPlayhead(7); + case LiveInputStyle.NumberKeys: + if (FlxG.keys.justPressed.ONE) placeNoteAtPlayhead(0); + if (FlxG.keys.justPressed.TWO) placeNoteAtPlayhead(1); + if (FlxG.keys.justPressed.THREE) placeNoteAtPlayhead(2); + if (FlxG.keys.justPressed.FOUR) placeNoteAtPlayhead(3); + + if (FlxG.keys.justPressed.FIVE) placeNoteAtPlayhead(4); + if (FlxG.keys.justPressed.SIX) placeNoteAtPlayhead(5); + if (FlxG.keys.justPressed.SEVEN) placeNoteAtPlayhead(6); + if (FlxG.keys.justPressed.EIGHT) placeNoteAtPlayhead(7); + case LiveInputStyle.None: + // Do nothing. } } function placeNoteAtPlayhead(column:Int):Void { - var gridSnappedPlayheadPos = scrollPositionInPixels - (scrollPositionInPixels % GRID_SIZE); + var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels; + var playheadPosFractionalStep:Float = playheadPos / GRID_SIZE / (16 / noteSnapQuant); + var playheadPosStep:Int = Std.int(Math.floor(playheadPosFractionalStep)); + var playheadPosMs:Float = playheadPosStep * Conductor.stepLengthMs * (16 / noteSnapQuant); + + var newNoteData:SongNoteData = new SongNoteData(playheadPosMs, column, 0, selectedNoteKind); + performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL)); } function set_scrollPositionInPixels(value:Float):Float @@ -2888,7 +3129,7 @@ class ChartEditorState extends HaxeUIState // but the playhead is in the middle, move the playhead up. if (playheadPositionInPixels > 0) { - var amount = scrollPositionInPixels - value; + var amount:Float = scrollPositionInPixels - value; playheadPositionInPixels -= amount; } @@ -2899,6 +3140,9 @@ class ChartEditorState extends HaxeUIState if (value == scrollPositionInPixels) return value; + // Difference in pixels. + var diff:Float = value - scrollPositionInPixels; + this.scrollPositionInPixels = value; // Move the grid sprite to the correct position. @@ -2914,12 +3158,9 @@ class ChartEditorState extends HaxeUIState renderedNotes.setPosition(gridTiledSprite.x, gridTiledSprite.y); renderedEvents.setPosition(gridTiledSprite.x, gridTiledSprite.y); renderedSelectionSquares.setPosition(gridTiledSprite.x, gridTiledSprite.y); - if (gridSpectrogram != null) - { - // Move the spectrogram to the correct position. - gridSpectrogram.y = gridTiledSprite.y; - gridSpectrogram.setPosition(0, 0); - } + + // Offset the selection box start position, if we are dragging. + if (selectionBoxStartPos != null) selectionBoxStartPos.y -= diff; return this.scrollPositionInPixels; } @@ -2945,7 +3186,7 @@ class ChartEditorState extends HaxeUIState /** * Loads an instrumental from an absolute file path, replacing the current instrumental. - * + * * @param path The absolute path to the audio file. * @return Success or failure. */ @@ -2974,12 +3215,19 @@ class ChartEditorState extends HaxeUIState */ public function loadInstrumentalFromBytes(bytes:haxe.io.Bytes, fileName:String = null):Bool { + if (bytes == null) + { + return false; + } + var openflSound:openfl.media.Sound = new openfl.media.Sound(); openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(bytes), bytes.length); audioInstTrack = FlxG.sound.load(openflSound, 1.0, false); audioInstTrack.autoDestroy = false; audioInstTrack.pause(); + audioInstTrackData = bytes; + postLoadInstrumental(); return true; @@ -2997,6 +3245,8 @@ class ChartEditorState extends HaxeUIState { audioInstTrack = instTrack; + audioInstTrackData = Assets.getBytes(path); + postLoadInstrumental(); return true; } @@ -3004,7 +3254,7 @@ class ChartEditorState extends HaxeUIState return false; } - function postLoadInstrumental():Void + public function postLoadInstrumental():Void { // Prevent the time from skipping back to 0 when the song ends. audioInstTrack.onComplete = function() { @@ -3015,11 +3265,8 @@ class ChartEditorState extends HaxeUIState songLengthInMs = audioInstTrack.length; gridTiledSprite.height = songLengthInPixels; - if (gridSpectrogram != null) - { - gridSpectrogram.setSound(audioInstTrack); - gridSpectrogram.generateSection(0, songLengthInMs / 1000); - } + + buildSpectrogram(audioInstTrack); scrollPositionInPixels = 0; playheadPositionInPixels = 0; @@ -3030,8 +3277,9 @@ class ChartEditorState extends HaxeUIState * Loads a vocal track from an absolute file path. * @param path The absolute path to the audio file. * @param charKey The character to load the vocal track for. + * @return Success or failure. */ - public function loadVocalsFromPath(path:Path, charKey:String = null):Bool + public function loadVocalsFromPath(path:Path, charKey:String = 'default'):Bool { #if sys var fileBytes:haxe.io.Bytes = sys.io.File.getBytes(path.toString()); @@ -3042,12 +3290,21 @@ class ChartEditorState extends HaxeUIState #end } - public function loadVocalsFromAsset(path:String, charKey:String = null):Bool + /** + * Loads a vocal track from an OpenFL asset. + * @param path ID of the asset. + * @param charKey Character to load the vocal track for. + * @return Success or failure. + */ + public function loadVocalsFromAsset(path:String, charKey:String = 'default'):Bool { var vocalTrack:FlxSound = FlxG.sound.load(path, 1.0, false); if (vocalTrack != null) { audioVocalTrackGroup.add(vocalTrack); + + audioVocalTrackData.set(charKey, Assets.getBytes(path)); + return true; } return false; @@ -3058,10 +3315,11 @@ class ChartEditorState extends HaxeUIState */ public function loadVocalsFromBytes(bytes:haxe.io.Bytes, charKey:String = null):Bool { - var openflSound = new openfl.media.Sound(); + var openflSound:openfl.media.Sound = new openfl.media.Sound(); openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(bytes), bytes.length); var vocalTrack:FlxSound = FlxG.sound.load(openflSound, 1.0, false); audioVocalTrackGroup.add(vocalTrack); + audioVocalTrackData.set(charKey, bytes); return true; } @@ -3074,45 +3332,84 @@ class ChartEditorState extends HaxeUIState if (song == null) { - // showNotification('Failed to load song.'); return; } // Load the song metadata. var rawSongMetadata:Array = song.getRawMetadata(); - var songName:String = rawSongMetadata[0].songName; - - this.songMetadata = new Map(); + var songMetadata:Map = []; + var songChartData:Map = []; for (metadata in rawSongMetadata) { - var variation = (metadata.variation == null || metadata.variation == '') ? 'default' : metadata.variation; - this.songMetadata.set(variation, metadata); + var variation:String = (metadata.variation == null || metadata.variation == '') ? 'default' : metadata.variation; + + songMetadata.set(variation, metadata); + songChartData.set(variation, SongDataParser.parseSongChartData(songId, metadata.variation)); } - this.songChartData = new Map(); + loadSong(songMetadata, songChartData); - for (metadata in rawSongMetadata) + if (audioInstTrack != null) { - var variation = (metadata.variation == null || metadata.variation == '') ? 'default' : metadata.variation; - this.songChartData.set(variation, SongDataParser.parseSongChartData(songId, metadata.variation)); + audioInstTrack.stop(); + audioInstTrack = null; } - - Conductor.forceBPM(null); // Disable the forced BPM. - Conductor.mapTimeChanges(currentSongMetadata.timeChanges); - loadInstrumentalFromAsset(Paths.inst(songId)); - loadVocalsFromAsset(Paths.voices(songId)); + + if (audioVocalTrackGroup != null) + { + audioVocalTrackGroup.stop(); + audioVocalTrackGroup.clear(); + } + // Add player vocals. + if (currentSongCharacterPlayer != null) audioVocalTrackGroup.setPlayerVocals(new FlxSound().loadEmbedded(Assets.getSound(Paths.voices(songId, + '-$currentSongCharacterPlayer')))); + // Add opponent vocals. + if (currentSongCharacterOpponent != null) audioVocalTrackGroup.setOpponentVocals(new FlxSound().loadEmbedded(Assets.getSound(Paths.voices(songId, + '-$currentSongCharacterOpponent')))); + + postLoadInstrumental(); NotificationManager.instance.addNotification( { title: 'Success', - body: 'Loaded song ($songName)', + body: 'Loaded song (${rawSongMetadata[0].songName})', type: NotificationType.Success, expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME }); } + /** + * Loads song metadata and chart data into the editor. + * @param newSongMetadata The song metadata to load. + * @param newSongChartData The song chart data to load. + */ + public function loadSong(newSongMetadata:Map, newSongChartData:Map):Void + { + this.songMetadata = newSongMetadata; + this.songChartData = newSongChartData; + + Conductor.forceBPM(null); // Disable the forced BPM. + Conductor.mapTimeChanges(currentSongMetadata.timeChanges); + + difficultySelectDirty = true; + opponentPreviewDirty = true; + playerPreviewDirty = true; + + // Remove instrumental and vocal tracks, they will be loaded next. + if (audioInstTrack != null) + { + audioInstTrack.stop(); + audioInstTrack = null; + } + if (audioVocalTrackGroup != null) + { + audioVocalTrackGroup.stop(); + audioVocalTrackGroup.clear(); + } + } + /** * When setting the scroll position, except when automatically scrolling during song playback, * we need to update the conductor's current step time and the timestamp of the audio tracks. @@ -3130,9 +3427,42 @@ class ChartEditorState extends HaxeUIState noteDisplayDirty = true; } + var currentScrollEase:VarTween; + + function easeSongToScrollPosition(targetScrollPosition:Float):Void + { + if (currentScrollEase != null) cancelScrollEase(currentScrollEase); + + currentScrollEase = FlxTween.tween(this, {scrollPositionInPixels: targetScrollPosition}, SCROLL_EASE_DURATION, + { + ease: FlxEase.quintInOut, + onUpdate: this.onScrollEaseUpdate, + onComplete: this.cancelScrollEase, + type: ONESHOT + }); + } + + function onScrollEaseUpdate(_:FlxTween):Void + { + moveSongToScrollPosition(); + } + + function cancelScrollEase(_:FlxTween):Void + { + if (currentScrollEase != null) + { + @:privateAccess + var targetScrollPosition:Float = currentScrollEase._properties.scrollPositionInPixels; + + currentScrollEase.cancel(); + currentScrollEase = null; + this.scrollPositionInPixels = targetScrollPosition; + } + } + /** * Perform (or redo) a command, then add it to the undo stack. - * + * * @param command The command to perform. * @param purgeRedoStack If true, the redo stack will be cleared. */ @@ -3248,11 +3578,11 @@ class ChartEditorState extends HaxeUIState */ public function exportAllSongData(?force:Bool = false, ?tmp:Bool = false):Void { - var zipEntries = []; + var zipEntries:Array = []; for (variation in availableVariations) { - var variationId = variation; + var variationId:String = variation; if (variation == '' || variation == 'default' || variation == 'normal') { variationId = ''; @@ -3260,21 +3590,25 @@ class ChartEditorState extends HaxeUIState if (variationId == '') { - var variationMetadata = songMetadata.get(variation); + var variationMetadata:SongMetadata = songMetadata.get(variation); zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-metadata.json', SerializerUtil.toJSON(variationMetadata))); - var variationChart = songChartData.get(variation); + var variationChart:SongChartData = songChartData.get(variation); zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-chart.json', SerializerUtil.toJSON(variationChart))); } else { - var variationMetadata = songMetadata.get(variation); + var variationMetadata:SongMetadata = songMetadata.get(variation); zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-metadata-$variationId.json', SerializerUtil.toJSON(variationMetadata))); - var variationChart = songChartData.get(variation); + var variationChart:SongChartData = songChartData.get(variation); zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-chart-$variationId.json', SerializerUtil.toJSON(variationChart))); } } - // TODO: Add audio files to the ZIP. + zipEntries.push(FileUtil.makeZIPEntryFromBytes('Inst.ogg', audioInstTrackData)); + for (charId in audioVocalTrackData.keys()) + { + zipEntries.push(FileUtil.makeZIPEntryFromBytes('Vocals-$charId.ogg', audioVocalTrackData.get(charId))); + } trace('Exporting ${zipEntries.length} files to ZIP...'); @@ -3307,3 +3641,10 @@ class ChartEditorState extends HaxeUIState FileUtil.saveMultipleFiles(zipEntries, onSave, onCancel, '$currentSongId-chart.zip'); } } + +enum LiveInputStyle +{ + None; + NumberKeys; + WASD; +} From 26998c916435b521e1a48b3cdcc3731323def4e3 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 8 Jun 2023 17:07:35 -0400 Subject: [PATCH 08/14] More chart editor changes --- source/funkin/play/song/Song.hx | 2 +- source/funkin/play/song/SongData.hx | 163 +++++++++------ source/funkin/play/song/SongDataUtils.hx | 57 +++-- source/funkin/play/song/SongMigrator.hx | 195 +++++++++++++++++- source/funkin/play/song/SongSerializer.hx | 27 +-- source/funkin/play/song/SongValidator.hx | 8 +- .../ui/debug/charting/ChartEditorState.hx | 38 ++-- 7 files changed, 350 insertions(+), 140 deletions(-) diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index a712de1cc..7de005cb0 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -324,7 +324,7 @@ class SongDifficulty /** * Build a list of vocal files for the given character. * Automatically resolves suffixed character IDs (so bf-car will resolve to bf if needed). - * + * * @param id The character we are about to play. */ public function buildVoiceList(?id:String = 'bf'):Array diff --git a/source/funkin/play/song/SongData.hx b/source/funkin/play/song/SongData.hx index 982ccb402..2ae38156a 100644 --- a/source/funkin/play/song/SongData.hx +++ b/source/funkin/play/song/SongData.hx @@ -1,5 +1,7 @@ package funkin.play.song; +import funkin.modding.events.ScriptEventDispatcher; +import funkin.modding.events.ScriptEvent; import flixel.util.typeLimit.OneOfTwo; import funkin.play.song.ScriptedSong; import funkin.util.assets.DataAssets; @@ -24,7 +26,7 @@ class SongDataParser /** * Parses and preloads the game's song metadata and scripts when the game starts. - * + * * If you want to force song metadata to be reloaded, you can just call this function again. */ public static function loadSongCache():Void @@ -95,6 +97,9 @@ class SongDataParser { var song:Song = songCache.get(songId); trace('Successfully fetch song: ${songId}'); + + var event:ScriptEvent = new ScriptEvent(ScriptEvent.CREATE, false); + ScriptEventDispatcher.callEvent(song, event); return song; } else @@ -112,12 +117,21 @@ class SongDataParser } } + /** + * A list of all the song IDs available to the game. + * @return The list of song IDs. + */ public static function listSongIds():Array { return songCache.keys().array(); } - public static function parseSongMetadata(songId:String):Array + /** + * Loads the song metadata for a particular song. + * @param songId The ID of the song to load. + * @return The song metadata for each variation, or an empty array if the song was not found. + */ + public static function loadSongMetadata(songId:String):Array { var result:Array = []; @@ -139,19 +153,13 @@ class SongDataParser result.push(songMetadata); - var variations = songMetadata.playData.songVariations; + var variations:Array = songMetadata.playData.songVariations; for (variation in variations) { - var variationJsonStr:String = loadSongMetadataFile(songId, variation); - var variationJsonData:Dynamic = null; - try - { - variationJsonData = Json.parse(variationJsonStr); - } - catch (e) {} - var variationSongMetadata:SongMetadata = SongMigrator.migrateSongMetadata(variationJsonData, '${songId}-${variation}'); - variationSongMetadata = SongValidator.validateSongMetadata(variationSongMetadata, '${songId}-${variation}'); + var variationRawJson:String = loadSongMetadataFile(songId, variation); + var variationSongMetadata:SongMetadata = SongMigrator.migrateSongMetadata(variationRawJson, '${songId}_${variation}'); + variationSongMetadata = SongValidator.validateSongMetadata(variationSongMetadata, '${songId}_${variation}'); if (variationSongMetadata != null) { variationSongMetadata.variation = variation; @@ -168,7 +176,7 @@ class SongDataParser var rawJson:String = Assets.getText(songMetadataFilePath).trim(); - while (!rawJson.endsWith("}")) + while (!rawJson.endsWith('}') && rawJson.length > 0) { rawJson = rawJson.substr(0, rawJson.length - 1); } @@ -176,7 +184,7 @@ class SongDataParser return rawJson; } - public static function parseSongChartData(songId:String, variation:String = ""):SongChartData + public static function parseSongChartData(songId:String, variation:String = ''):SongChartData { var rawJson:String = loadSongChartDataFile(songId, variation); var jsonData:Dynamic = null; @@ -184,7 +192,11 @@ class SongDataParser { jsonData = Json.parse(rawJson); } - catch (e) {} + catch (e) + { + trace('Failed to parse song chart data: ${songId} (${variation})'); + trace(e); + } var songChartData:SongChartData = SongMigrator.migrateSongChartData(jsonData, songId); songChartData = SongValidator.validateSongChartData(songChartData, songId); @@ -204,7 +216,7 @@ class SongDataParser var rawJson:String = Assets.getText(songChartDataFilePath).trim(); - while (!rawJson.endsWith("}")) + while (!rawJson.endsWith('}') && rawJson.length > 0) { rawJson = rawJson.substr(0, rawJson.length - 1); } @@ -217,7 +229,7 @@ typedef RawSongMetadata = { /** * A semantic versioning string for the song data format. - * + * */ var version:Version; @@ -272,7 +284,7 @@ abstract SongMetadata(RawSongMetadata) public function clone(?newVariation:String = null):SongMetadata { - var result = new SongMetadata(this.songName, this.artist, newVariation == null ? this.variation : newVariation); + var result:SongMetadata = new SongMetadata(this.songName, this.artist, newVariation == null ? this.variation : newVariation); result.version = this.version; result.timeFormat = this.timeFormat; result.divisions = this.divisions; @@ -350,22 +362,22 @@ abstract SongNoteData(RawSongNoteData) public var time(get, set):Float; - public function get_time():Float + function get_time():Float { return this.t; } - public function set_time(value:Float):Float + function set_time(value:Float):Float { return this.t = value; } public var stepTime(get, never):Float; - public function get_stepTime():Float + function get_stepTime():Float { // TODO: Account for changes in BPM. - return this.t / Conductor.stepCrochet; + return this.t / Conductor.stepLengthMs; } /** @@ -373,12 +385,12 @@ abstract SongNoteData(RawSongNoteData) */ public var data(get, set):Int; - public function get_data():Int + function get_data():Int { return this.d; } - public function set_data(value:Int):Int + function set_data(value:Int):Int { return this.d = value; } @@ -414,7 +426,7 @@ abstract SongNoteData(RawSongNoteData) /** * The strumline index of the note, if applicable. * Strips the direction from the data. - * + * * 0 = player, 1 = opponent, etc. */ public inline function getStrumlineIndex(strumlineSize:Int = 4):Int @@ -429,26 +441,26 @@ abstract SongNoteData(RawSongNoteData) public var length(get, set):Float; - public function get_length():Float + function get_length():Float { return this.l; } - public function set_length(value:Float):Float + function set_length(value:Float):Float { return this.l = value; } public var kind(get, set):String; - public function get_kind():String + function get_kind():String { if (this.k == null || this.k == '') return 'normal'; return this.k; } - public function set_kind(value:String):String + function set_kind(value:String):String { if (value == 'normal' || value == '') value = null; return this.k = value; @@ -536,56 +548,56 @@ abstract SongEventData(RawSongEventData) public var time(get, set):Float; - public function get_time():Float + function get_time():Float { return this.t; } - public function set_time(value:Float):Float + function set_time(value:Float):Float { return this.t = value; } public var stepTime(get, never):Float; - public function get_stepTime():Float + function get_stepTime():Float { // TODO: Account for changes in BPM. - return this.t / Conductor.stepCrochet; + return this.t / Conductor.stepLengthMs; } public var event(get, set):String; - public function get_event():String + function get_event():String { return this.e; } - public function set_event(value:String):String + function set_event(value:String):String { return this.e = value; } public var value(get, set):Dynamic; - public function get_value():Dynamic + function get_value():Dynamic { return this.v; } - public function set_value(value:Dynamic):Dynamic + function set_value(value:Dynamic):Dynamic { return this.v = value; } public var activated(get, set):Bool; - public function get_activated():Bool + function get_activated():Bool { return this.a; } - public function set_activated(value:Bool):Bool + function set_activated(value:Bool):Bool { return this.a = value; } @@ -664,7 +676,7 @@ abstract SongEventData(RawSongEventData) abstract SongPlayableChar(RawSongPlayableChar) { - public function new(girlfriend:String, opponent:String, inst:String = "") + public function new(girlfriend:String, opponent:String, inst:String = '') { this = { @@ -676,36 +688,36 @@ abstract SongPlayableChar(RawSongPlayableChar) public var girlfriend(get, set):String; - public function get_girlfriend():String + function get_girlfriend():String { return this.g; } - public function set_girlfriend(value:String):String + function set_girlfriend(value:String):String { return this.g = value; } public var opponent(get, set):String; - public function get_opponent():String + function get_opponent():String { return this.o; } - public function set_opponent(value:String):String + function set_opponent(value:String):String { return this.o = value; } public var inst(get, set):String; - public function get_inst():String + function get_inst():String { return this.i; } - public function set_inst(value:String):String + function set_inst(value:String):String { return this.i = value; } @@ -751,6 +763,35 @@ abstract SongChartData(RawSongChartData) return (result == 0.0) ? 1.0 : result; } + + public function setScrollSpeed(value:Float, diff:String = 'default'):Float + { + return this.scrollSpeed.set(diff, value); + } + + public function getNotes(diff:String):Array + { + var result:Array = this.notes.get(diff); + + if (result == null && diff != 'normal') return getNotes('normal'); + + return (result == null) ? [] : result; + } + + public function setNotes(value:Array, diff:String):Array + { + return this.notes.set(diff, value); + } + + public function getEvents():Array + { + return this.events; + } + + public function setEvents(value:Array):Array + { + return this.events = value; + } } typedef RawSongTimeChange = @@ -811,67 +852,67 @@ abstract SongTimeChange(RawSongTimeChange) public var timeStamp(get, set):Float; - public function get_timeStamp():Float + function get_timeStamp():Float { return this.t; } - public function set_timeStamp(value:Float):Float + function set_timeStamp(value:Float):Float { return this.t = value; } public var beatTime(get, set):Int; - public function get_beatTime():Int + function get_beatTime():Int { return this.b; } - public function set_beatTime(value:Int):Int + function set_beatTime(value:Int):Int { return this.b = value; } public var bpm(get, set):Float; - public function get_bpm():Float + function get_bpm():Float { return this.bpm; } - public function set_bpm(value:Float):Float + function set_bpm(value:Float):Float { return this.bpm = value; } public var timeSignatureNum(get, set):Int; - public function get_timeSignatureNum():Int + function get_timeSignatureNum():Int { return this.n; } - public function set_timeSignatureNum(value:Int):Int + function set_timeSignatureNum(value:Int):Int { return this.n = value; } public var timeSignatureDen(get, set):Int; - public function get_timeSignatureDen():Int + function get_timeSignatureDen():Int { return this.d; } - public function set_timeSignatureDen(value:Int):Int + function set_timeSignatureDen(value:Int):Int { return this.d = value; } public var beatTuplets(get, set):Array; - public function get_beatTuplets():Array + function get_beatTuplets():Array { if (Std.isOfType(this.bt, Int)) { @@ -883,7 +924,7 @@ abstract SongTimeChange(RawSongTimeChange) } } - public function set_beatTuplets(value:Array):Array + function set_beatTuplets(value:Array):Array { return this.bt = value; } @@ -891,7 +932,7 @@ abstract SongTimeChange(RawSongTimeChange) enum abstract SongTimeFormat(String) from String to String { - var TICKS = "ticks"; - var FLOAT = "float"; - var MILLISECONDS = "ms"; + var TICKS = 'ticks'; + var FLOAT = 'float'; + var MILLISECONDS = 'ms'; } diff --git a/source/funkin/play/song/SongDataUtils.hx b/source/funkin/play/song/SongDataUtils.hx index 27625e846..750d5f54b 100644 --- a/source/funkin/play/song/SongDataUtils.hx +++ b/source/funkin/play/song/SongDataUtils.hx @@ -14,14 +14,13 @@ class SongDataUtils * Given an array of SongNoteData objects, return a new array of SongNoteData objects * whose timestamps are shifted by the given amount. * Does not mutate the original array. - * + * * @param notes The notes to modify. * @param offset The time difference to apply in milliseconds. */ public static function offsetSongNoteData(notes:Array, offset:Int):Array { - return notes.map(function(note:SongNoteData):SongNoteData - { + return notes.map(function(note:SongNoteData):SongNoteData { return new SongNoteData(note.time + offset, note.data, note.length, note.kind); }); } @@ -30,14 +29,13 @@ class SongDataUtils * Given an array of SongEventData objects, return a new array of SongEventData objects * whose timestamps are shifted by the given amount. * Does not mutate the original array. - * + * * @param events The events to modify. * @param offset The time difference to apply in milliseconds. */ public static function offsetSongEventData(events:Array, offset:Int):Array { - return events.map(function(event:SongEventData):SongEventData - { + return events.map(function(event:SongEventData):SongEventData { return new SongEventData(event.time + offset, event.event, event.value); }); } @@ -45,7 +43,7 @@ class SongDataUtils /** * Return a new array without a certain subset of notes from an array of SongNoteData objects. * Does not mutate the original array. - * + * * @param notes The array of notes to be subtracted from. * @param subtrahend The notes to remove from the `notes` array. Yes, subtrahend is a real word. */ @@ -53,8 +51,7 @@ class SongDataUtils { if (notes.length == 0 || subtrahend.length == 0) return notes; - var result = notes.filter(function(note:SongNoteData):Bool - { + var result = notes.filter(function(note:SongNoteData):Bool { for (x in subtrahend) // SongNoteData's == operation has been overridden so that this will work. if (x == note) return false; @@ -68,7 +65,7 @@ class SongDataUtils /** * Return a new array without a certain subset of events from an array of SongEventData objects. * Does not mutate the original array. - * + * * @param events The array of events to be subtracted from. * @param subtrahend The events to remove from the `events` array. Yes, subtrahend is a real word. */ @@ -76,8 +73,7 @@ class SongDataUtils { if (events.length == 0 || subtrahend.length == 0) return events; - return events.filter(function(event:SongEventData):Bool - { + return events.filter(function(event:SongEventData):Bool { // SongEventData's == operation has been overridden so that this will work. return !subtrahend.has(event); }); @@ -89,8 +85,7 @@ class SongDataUtils */ public static function flipNotes(notes:Array, ?strumlineSize:Int = 4):Array { - return notes.map(function(note:SongNoteData):SongNoteData - { + return notes.map(function(note:SongNoteData):SongNoteData { var newData = note.data; if (newData < strumlineSize) newData += strumlineSize; @@ -103,22 +98,26 @@ class SongDataUtils /** * Prepare an array of notes to be used as the clipboard data. - * + * * Offset the provided array of notes such that the first note is at 0 milliseconds. */ - public static function buildNoteClipboard(notes:Array):Array + public static function buildNoteClipboard(notes:Array, ?timeOffset:Int = null):Array { - return offsetSongNoteData(sortNotes(notes), -Std.int(notes[0].time)); + if (notes.length == 0) return notes; + if (timeOffset == null) timeOffset = -Std.int(notes[0].time); + return offsetSongNoteData(sortNotes(notes), timeOffset); } /** * Prepare an array of events to be used as the clipboard data. - * + * * Offset the provided array of events such that the first event is at 0 milliseconds. */ - public static function buildEventClipboard(events:Array):Array + public static function buildEventClipboard(events:Array, ?timeOffset:Int = null):Array { - return offsetSongEventData(sortEvents(events), -Std.int(events[0].time)); + if (events.length == 0) return events; + if (timeOffset == null) timeOffset = -Std.int(events[0].time); + return offsetSongEventData(sortEvents(events), timeOffset); } /** @@ -127,8 +126,7 @@ class SongDataUtils public static function sortNotes(notes:Array, ?desc:Bool = false):Array { // TODO: Modifies the array in place. Is this okay? - notes.sort(function(a:SongNoteData, b:SongNoteData):Int - { + notes.sort(function(a:SongNoteData, b:SongNoteData):Int { return FlxSort.byValues(desc ? FlxSort.DESCENDING : FlxSort.ASCENDING, a.time, b.time); }); return notes; @@ -140,8 +138,7 @@ class SongDataUtils public static function sortEvents(events:Array, ?desc:Bool = false):Array { // TODO: Modifies the array in place. Is this okay? - events.sort(function(a:SongEventData, b:SongEventData):Int - { + events.sort(function(a:SongEventData, b:SongEventData):Int { return FlxSort.byValues(desc ? FlxSort.DESCENDING : FlxSort.ASCENDING, a.time, b.time); }); return events; @@ -192,8 +189,7 @@ class SongDataUtils */ public static function getNotesInTimeRange(notes:Array, start:Float, end:Float):Array { - return notes.filter(function(note:SongNoteData):Bool - { + return notes.filter(function(note:SongNoteData):Bool { return note.time >= start && note.time <= end; }); } @@ -203,8 +199,7 @@ class SongDataUtils */ public static function getEventsInTimeRange(events:Array, start:Float, end:Float):Array { - return events.filter(function(event:SongEventData):Bool - { + return events.filter(function(event:SongEventData):Bool { return event.time >= start && event.time <= end; }); } @@ -214,8 +209,7 @@ class SongDataUtils */ public static function getNotesInDataRange(notes:Array, start:Int, end:Int):Array { - return notes.filter(function(note:SongNoteData):Bool - { + return notes.filter(function(note:SongNoteData):Bool { return note.data >= start && note.data <= end; }); } @@ -225,8 +219,7 @@ class SongDataUtils */ public static function getNotesWithData(notes:Array, data:Array):Array { - return notes.filter(function(note:SongNoteData):Bool - { + return notes.filter(function(note:SongNoteData):Bool { return data.indexOf(note.data) != -1; }); } diff --git a/source/funkin/play/song/SongMigrator.hx b/source/funkin/play/song/SongMigrator.hx index 1872585d0..05adf2457 100644 --- a/source/funkin/play/song/SongMigrator.hx +++ b/source/funkin/play/song/SongMigrator.hx @@ -1,7 +1,11 @@ package funkin.play.song; +import funkin.play.song.formats.FNFLegacy; import funkin.play.song.SongData.SongChartData; +import funkin.play.song.SongData.SongEventData; import funkin.play.song.SongData.SongMetadata; +import funkin.play.song.SongData.SongNoteData; +import funkin.play.song.SongData.SongPlayableChar; import funkin.util.VersionUtil; class SongMigrator @@ -11,13 +15,22 @@ class SongMigrator * Handle breaking changes by incrementing this value * and adding migration to the SongMigrator class. */ - public static final CHART_VERSION:String = "2.0.0"; + public static final CHART_VERSION:String = '2.0.0'; - public static final CHART_VERSION_RULE:String = "2.0.x"; + /** + * Version rule for which chart versions are compatible with the current version. + */ + public static final CHART_VERSION_RULE:String = '2.0.x'; + /** + * Migrate song data from an older chart version to the current version. + * @param jsonData The song metadata to migrate. + * @param songId The ID of the song (only used for error reporting). + * @return The migrated song metadata, or null if the migration failed. + */ public static function migrateSongMetadata(jsonData:Dynamic, songId:String):SongMetadata { - if (jsonData.version) + if (jsonData.version != null) { if (VersionUtil.validateVersion(jsonData.version, CHART_VERSION_RULE)) { @@ -32,10 +45,11 @@ class SongMigrator trace('Song (${songId}) metadata version (${jsonData.version}) is outdated.'); switch (jsonData.version) { - // TODO: Add migration functions as cases here. + case '1.0.0': + return migrateSongMetadataFromLegacy(jsonData); default: - // Unknown version. - trace('Song (${songId}) unknown metadata version: ${jsonData.version}'); + trace('Song (${songId}) has unknown metadata version (${jsonData.version}), assuming FNF Legacy.'); + return migrateSongMetadataFromLegacy(jsonData); } } } @@ -46,6 +60,12 @@ class SongMigrator return null; } + /** + * Migrate song chart data from an older chart version to the current version. + * @param jsonData The song chart data to migrate. + * @param songId The ID of the song (only used for error reporting). + * @return The migrated song chart data, or null if the migration failed. + */ public static function migrateSongChartData(jsonData:Dynamic, songId:String):SongChartData { if (jsonData.version) @@ -76,4 +96,167 @@ class SongMigrator } return null; } + + /** + * Migrate song metadata from FNF Legacy chart version to the current version. + * @param jsonData The song metadata to migrate. + * @param songId The ID of the song (only used for error reporting). + * @return The migrated song metadata, or null if the migration failed. + */ + public static function migrateSongMetadataFromLegacy(jsonData:Dynamic):SongMetadata + { + trace('Migrating song metadata from FNF Legacy.'); + + var songData:FNFLegacy = cast jsonData; + + var songMetadata:SongMetadata = new SongMetadata('Import', 'Kawai Sprite', 'default'); + + var hadError:Bool = false; + + // Set generatedBy string for debugging. + songMetadata.generatedBy = 'Chart Editor Import (FNF Legacy)'; + + try + { + // Set the song's BPM. + songMetadata.timeChanges[0].bpm = songData.song.bpm; + } + catch (e) + { + trace("Couldn't parse BPM!"); + hadError = true; + } + + try + { + // Set the song's stage. + songMetadata.playData.stage = songData.song.stageDefault; + } + catch (e) + { + trace("Couldn't parse stage!"); + hadError = true; + } + + try + { + // Set's the song's name. + songMetadata.songName = songData.song.song; + } + catch (e) + { + trace("Couldn't parse song name!"); + hadError = true; + } + + songMetadata.playData.difficulties = []; + if (songData.song != null && songData.song.notes != null) + { + if (songData.song.notes.easy != null) songMetadata.playData.difficulties.push('easy'); + if (songData.song.notes.normal != null) songMetadata.playData.difficulties.push('normal'); + if (songData.song.notes.hard != null) songMetadata.playData.difficulties.push('hard'); + } + else + { + trace("Couldn't parse difficulties!"); + hadError = true; + } + + songMetadata.playData.songVariations = []; + + // Set the song's song variations. + songMetadata.playData.playableChars = {}; + try + { + Reflect.setField(songMetadata.playData.playableChars, songData.song.player1, new SongPlayableChar('', songData.song.player2)); + } + catch (e) + { + trace("Couldn't parse characters!"); + hadError = true; + } + + return songMetadata; + } + + /** + * Migrate song chart data from FNF Legacy chart version to the current version. + * @param jsonData The song data to migrate. + * @param songId The ID of the song (only used for error reporting). + * @param difficulty The difficulty to migrate. + * @return The migrated song chart data, or null if the migration failed. + */ + public static function migrateSongChartDataFromLegacy(jsonData:Dynamic):SongChartData + { + trace('Migrating song chart data from FNF Legacy.'); + + var songData:FNFLegacy = cast jsonData; + + var songChartData:SongChartData = new SongChartData(1.0, [], []); + + if (songData.song.notes.normal != null) + { + var songEventsEmpty:Bool = songChartData.getEvents() == null || songChartData.getEvents().length == 0; + if (songEventsEmpty) songChartData.setEvents(migrateSongEventDataFromLegacy(songData.song.notes.normal)); + songChartData.setNotes(migrateSongNoteDataFromLegacy(songData.song.notes.normal), 'normal'); + songChartData.setScrollSpeed(songData.song.speed.normal, 'normal'); + } + if (songData.song.notes.easy != null) + { + var songEventsEmpty:Bool = songChartData.getEvents() == null || songChartData.getEvents().length == 0; + if (songEventsEmpty) songChartData.setEvents(migrateSongEventDataFromLegacy(songData.song.notes.easy)); + songChartData.setNotes(migrateSongNoteDataFromLegacy(songData.song.notes.easy), 'easy'); + songChartData.setScrollSpeed(songData.song.speed.easy, 'easy'); + } + if (songData.song.notes.hard != null) + { + var songEventsEmpty:Bool = songChartData.getEvents() == null || songChartData.getEvents().length == 0; + if (songEventsEmpty) songChartData.setEvents(migrateSongEventDataFromLegacy(songData.song.notes.hard)); + songChartData.setNotes(migrateSongNoteDataFromLegacy(songData.song.notes.hard), 'hard'); + songChartData.setScrollSpeed(songData.song.speed.hard, 'hard'); + } + + return songChartData; + } + + static function migrateSongNoteDataFromLegacy(sections:Array):Array + { + var songNotes:Array = []; + + for (section in sections) + { + // Skip empty sections. + if (section.sectionNotes.length == 0) continue; + + for (note in section.sectionNotes) + { + songNotes.push(new SongNoteData(note.time, note.getData(section.mustHitSection), note.length, note.kind)); + } + } + + return songNotes; + } + + static function migrateSongEventDataFromLegacy(sections:Array):Array + { + var songEvents:Array = []; + + var lastSectionWasMustHit:Null = null; + for (section in sections) + { + // Skip empty sections. + if (section.sectionNotes.length == 0) continue; + + if (section.mustHitSection != lastSectionWasMustHit) + { + lastSectionWasMustHit = section.mustHitSection; + + var firstNote:LegacyNote = section.sectionNotes[0]; + + songEvents.push(new SongEventData(firstNote.time, 'FocusCamera', {char: section.mustHitSection ? 0 : 1})); + } + } + + return songEvents; + } } diff --git a/source/funkin/play/song/SongSerializer.hx b/source/funkin/play/song/SongSerializer.hx index c7933665a..968a7a1f5 100644 --- a/source/funkin/play/song/SongSerializer.hx +++ b/source/funkin/play/song/SongSerializer.hx @@ -50,8 +50,7 @@ class SongSerializer */ public static function importSongChartDataAsync(callback:SongChartData->Void):Void { - browseFileReference(function(fileReference:FileReference) - { + browseFileReference(function(fileReference:FileReference) { var data = fileReference.data.toString(); if (data == null) return; @@ -68,8 +67,7 @@ class SongSerializer */ public static function importSongMetadataAsync(callback:SongMetadata->Void):Void { - browseFileReference(function(fileReference:FileReference) - { + browseFileReference(function(fileReference:FileReference) { var data = fileReference.data.toString(); if (data == null) return; @@ -103,7 +101,7 @@ class SongSerializer /** * Save a SongChartData object as a JSON file to a specified path. * Works great on HTML5 and desktop. - * + * * @param path The file path to save to. */ public static function exportSongChartDataAs(path:String, data:SongChartData) @@ -116,7 +114,7 @@ class SongSerializer /** * Save a SongMetadata object as a JSON file to a specified path. * Works great on HTML5 and desktop. - * + * * @param path The file path to save to. */ public static function exportSongMetadataAs(path:String, data:SongMetadata) @@ -163,19 +161,17 @@ class SongSerializer /** * Browse for a file to read and execute a callback once we have a file reference. * Works great on HTML5 or desktop. - * + * * @param callback The function to call when the file is loaded. */ static function browseFileReference(callback:FileReference->Void) { var file = new FileReference(); - file.addEventListener(Event.SELECT, function(e) - { + file.addEventListener(Event.SELECT, function(e) { var selectedFileRef:FileReference = e.target; trace('Selected file: ' + selectedFileRef.name); - selectedFileRef.addEventListener(Event.COMPLETE, function(e) - { + selectedFileRef.addEventListener(Event.COMPLETE, function(e) { var loadedFileRef:FileReference = e.target; trace('Loaded file: ' + loadedFileRef.name); callback(loadedFileRef); @@ -192,16 +188,13 @@ class SongSerializer static function writeFileReference(path:String, data:String) { var file = new FileReference(); - file.addEventListener(Event.COMPLETE, function(e:Event) - { + file.addEventListener(Event.COMPLETE, function(e:Event) { trace('Successfully wrote file.'); }); - file.addEventListener(Event.CANCEL, function(e:Event) - { + file.addEventListener(Event.CANCEL, function(e:Event) { trace('Cancelled writing file.'); }); - file.addEventListener(IOErrorEvent.IO_ERROR, function(e:IOErrorEvent) - { + file.addEventListener(IOErrorEvent.IO_ERROR, function(e:IOErrorEvent) { trace('IO error writing file.'); }); file.save(data, path); diff --git a/source/funkin/play/song/SongValidator.hx b/source/funkin/play/song/SongValidator.hx index 950113bcf..d393c11eb 100644 --- a/source/funkin/play/song/SongValidator.hx +++ b/source/funkin/play/song/SongValidator.hx @@ -30,7 +30,7 @@ class SongValidator /** * Validates the fields of a SongMetadata object (excluding the version field). - * + * * @param input The SongMetadata object to validate. * @param songId The ID of the song being validated. Only used for error messages. * @return The validated SongMetadata object. @@ -73,7 +73,7 @@ class SongValidator /** * Validates the fields of a SongPlayData object. - * + * * @param input The SongPlayData object to validate. * @param songId The ID of the song being validated. Only used for error messages. * @return The validated SongPlayData object. @@ -85,7 +85,7 @@ class SongValidator /** * Validates the fields of a TimeChange object. - * + * * @param input The TimeChange object to validate. * @param songId The ID of the song being validated. Only used for error messages. * @return The validated TimeChange object. @@ -113,7 +113,7 @@ class SongValidator /** * Validates the fields of a SongChartData object (excluding the version field). - * + * * @param input The SongChartData object to validate. * @param songId The ID of the song being validated. Only used for error messages. * @return The validated SongChartData object. diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index c24c2db1b..43fa8b26a 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -20,7 +20,7 @@ import flixel.util.FlxColor; import flixel.util.FlxSort; import flixel.util.FlxTimer; import funkin.audio.visualize.PolygonSpectogram; -import funkin.audio.VocalGroup; +import funkin.audio.VoicesGroup; import funkin.input.Cursor; import funkin.input.TurboKeyHandler; import funkin.modding.events.ScriptEvent; @@ -197,12 +197,12 @@ class ChartEditorState extends HaxeUIState function get_scrollPositionInMs():Float { - return scrollPositionInSteps * Conductor.stepLengthMs; + return scrollPositionInSteps * Conductor.stepCrochet; } function set_scrollPositionInMs(value:Float):Float { - scrollPositionInPixels = value / Conductor.stepLengthMs; + scrollPositionInPixels = value / Conductor.stepCrochet; return value; } @@ -231,7 +231,7 @@ class ChartEditorState extends HaxeUIState function get_playheadPositionInMs():Float { - return playheadPositionInSteps * Conductor.stepLengthMs; + return playheadPositionInSteps * Conductor.stepCrochet; } /** @@ -271,7 +271,7 @@ class ChartEditorState extends HaxeUIState function get_songLengthInMs():Float { - return songLengthInSteps * Conductor.stepLengthMs; + return songLengthInSteps * Conductor.stepCrochet; } function set_songLengthInMs(value:Float):Float @@ -642,7 +642,7 @@ class ChartEditorState extends HaxeUIState /** * The audio track for the vocals. */ - var audioVocalTrackGroup:VocalGroup; + var audioVocalTrackGroup:VoicesGroup; /** * The raw byte data for the vocal audio tracks. @@ -1053,7 +1053,7 @@ class ChartEditorState extends HaxeUIState // Initialize the song chart data. songChartData = new Map(); - audioVocalTrackGroup = new VocalGroup(); + audioVocalTrackGroup = new VoicesGroup(); } /** @@ -1811,7 +1811,7 @@ class ChartEditorState extends HaxeUIState // The song position of the cursor, in steps. var cursorFractionalStep:Float = cursorY / GRID_SIZE / (16 / noteSnapQuant); var cursorStep:Int = Std.int(Math.floor(cursorFractionalStep)); - var cursorMs:Float = cursorStep * Conductor.stepLengthMs * (16 / noteSnapQuant); + var cursorMs:Float = cursorStep * Conductor.stepCrochet * (16 / noteSnapQuant); // The direction value for the column at the cursor. var cursorColumn:Int = Math.floor(cursorX / GRID_SIZE); if (cursorColumn < 0) cursorColumn = 0; @@ -1849,7 +1849,7 @@ class ChartEditorState extends HaxeUIState // We released the mouse. Select the notes in the box. var cursorFractionalStepStart:Float = cursorYStart / GRID_SIZE; var cursorStepStart:Int = Math.floor(cursorFractionalStepStart); - var cursorMsStart:Float = cursorStepStart * Conductor.stepLengthMs; + var cursorMsStart:Float = cursorStepStart * Conductor.stepCrochet; var cursorColumnBase:Int = Math.floor(cursorX / GRID_SIZE); var cursorColumnBaseStart:Int = Math.floor(cursorXStart / GRID_SIZE); @@ -2053,12 +2053,12 @@ class ChartEditorState extends HaxeUIState { // Handle extending the note as you drag. - // Since use Math.floor and stepLengthMs here, the hold notes will be beat snapped. - var dragLengthSteps:Float = Math.floor((cursorMs - currentPlaceNoteData.time) / Conductor.stepLengthMs); + // Since use Math.floor and stepCrochet here, the hold notes will be beat snapped. + var dragLengthSteps:Float = Math.floor((cursorMs - currentPlaceNoteData.time) / Conductor.stepCrochet); // Without this, the newly placed note feels too short compared to the user's input. var INCREMENT:Float = 1.0; - var dragLengthMs:Float = (dragLengthSteps + INCREMENT) * Conductor.stepLengthMs; + var dragLengthMs:Float = (dragLengthSteps + INCREMENT) * Conductor.stepCrochet; // TODO: Add and update some sort of preview? @@ -2363,7 +2363,7 @@ class ChartEditorState extends HaxeUIState } // Get the position the note should be at. - var noteTimePixels:Float = noteData.time / Conductor.stepLengthMs * GRID_SIZE; + var noteTimePixels:Float = noteData.time / Conductor.stepCrochet * GRID_SIZE; // Make sure the note appears when scrolling up. var modifiedViewAreaTop:Float = viewAreaTop - GRID_SIZE; @@ -2389,7 +2389,7 @@ class ChartEditorState extends HaxeUIState { // If the note is a hold, we need to make sure it's long enough. var noteLengthMs:Float = noteSprite.noteData.length; - var noteLengthSteps:Float = (noteLengthMs / Conductor.stepLengthMs); + var noteLengthSteps:Float = (noteLengthMs / Conductor.stepCrochet); var lastNoteSprite:ChartEditorNoteSprite = noteSprite; while (noteLengthSteps > 0) @@ -2413,7 +2413,7 @@ class ChartEditorState extends HaxeUIState // Make sure the last note sprite shows the end cap properly. lastNoteSprite.childNoteSprite = null; - // var noteLengthPixels:Float = (noteLengthMs / Conductor.stepLengthMs + 1) * GRID_SIZE; + // var noteLengthPixels:Float = (noteLengthMs / Conductor.stepCrochet + 1) * GRID_SIZE; // add(new FlxSprite(noteSprite.x, noteSprite.y - renderedNotes.y + noteLengthPixels).makeGraphic(40, 2, 0xFFFF0000)); } } @@ -2428,7 +2428,7 @@ class ChartEditorState extends HaxeUIState } // Get the position the event should be at. - var eventTimePixels:Float = eventData.time / Conductor.stepLengthMs * GRID_SIZE; + var eventTimePixels:Float = eventData.time / Conductor.stepCrochet * GRID_SIZE; // Make sure the event appears when scrolling up. var modifiedViewAreaTop:Float = viewAreaTop - GRID_SIZE; @@ -3115,7 +3115,7 @@ class ChartEditorState extends HaxeUIState var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels; var playheadPosFractionalStep:Float = playheadPos / GRID_SIZE / (16 / noteSnapQuant); var playheadPosStep:Int = Std.int(Math.floor(playheadPosFractionalStep)); - var playheadPosMs:Float = playheadPosStep * Conductor.stepLengthMs * (16 / noteSnapQuant); + var playheadPosMs:Float = playheadPosStep * Conductor.stepCrochet * (16 / noteSnapQuant); var newNoteData:SongNoteData = new SongNoteData(playheadPosMs, column, 0, selectedNoteKind); performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL)); @@ -3363,10 +3363,10 @@ class ChartEditorState extends HaxeUIState audioVocalTrackGroup.clear(); } // Add player vocals. - if (currentSongCharacterPlayer != null) audioVocalTrackGroup.setPlayerVocals(new FlxSound().loadEmbedded(Assets.getSound(Paths.voices(songId, + if (currentSongCharacterPlayer != null) audioVocalTrackGroup.addPlayerVocals(new FlxSound().loadEmbedded(Assets.getSound(Paths.voices(songId, '-$currentSongCharacterPlayer')))); // Add opponent vocals. - if (currentSongCharacterOpponent != null) audioVocalTrackGroup.setOpponentVocals(new FlxSound().loadEmbedded(Assets.getSound(Paths.voices(songId, + if (currentSongCharacterOpponent != null) audioVocalTrackGroup.addOpponentVocals(new FlxSound().loadEmbedded(Assets.getSound(Paths.voices(songId, '-$currentSongCharacterOpponent')))); postLoadInstrumental(); From 2200b6a24a88fa6525d6c9ce21ab4f30bf2c5560 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Fri, 9 Jun 2023 15:27:23 -0400 Subject: [PATCH 09/14] File renames --- source/funkin/play/{GameOverSubstate.hx => GameOverSubState.hx} | 0 .../{StageOffsetSubstate.hx => StageOffsetSubState.hx} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename source/funkin/play/{GameOverSubstate.hx => GameOverSubState.hx} (100%) rename source/funkin/ui/stageBuildShit/{StageOffsetSubstate.hx => StageOffsetSubState.hx} (100%) diff --git a/source/funkin/play/GameOverSubstate.hx b/source/funkin/play/GameOverSubState.hx similarity index 100% rename from source/funkin/play/GameOverSubstate.hx rename to source/funkin/play/GameOverSubState.hx diff --git a/source/funkin/ui/stageBuildShit/StageOffsetSubstate.hx b/source/funkin/ui/stageBuildShit/StageOffsetSubState.hx similarity index 100% rename from source/funkin/ui/stageBuildShit/StageOffsetSubstate.hx rename to source/funkin/ui/stageBuildShit/StageOffsetSubState.hx From 038d2582330b0edcbc82cc0be61da3d896a6edab Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Fri, 9 Jun 2023 15:28:14 -0400 Subject: [PATCH 10/14] Chart editor changes (support for legacy import) --- source/funkin/PauseSubState.hx | 2 +- source/funkin/play/song/Song.hx | 2 +- source/funkin/play/song/SongData.hx | 4 +- source/funkin/play/song/SongMigrator.hx | 63 +++++++++++-------- source/funkin/play/song/SongSerializer.hx | 8 +-- source/funkin/play/song/formats/FNFLegacy.hx | 4 +- .../charting/ChartEditorDialogHandler.hx | 2 +- .../ui/debug/charting/ChartEditorState.hx | 4 +- .../charting/ChartEditorToolboxHandler.hx | 4 +- 9 files changed, 52 insertions(+), 41 deletions(-) diff --git a/source/funkin/PauseSubState.hx b/source/funkin/PauseSubState.hx index 7349052aa..77fdfabf1 100644 --- a/source/funkin/PauseSubState.hx +++ b/source/funkin/PauseSubState.hx @@ -225,7 +225,7 @@ class PauseSubState extends MusicBeatSubState if (PlayStatePlaylist.isStoryMode) openSubState(new funkin.ui.StickerSubState(null, STORY)); else - openSubState(new funkin.ui.StickerSubState()); + openSubState(new funkin.ui.StickerSubState(null, FREEPLAY)); } } diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index 7de005cb0..22b5e8f07 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -47,7 +47,7 @@ class Song implements IPlayStateScriptedClass difficultyIds = []; difficulties = new Map(); - _metadata = SongDataParser.parseSongMetadata(songId); + _metadata = SongDataParser.loadSongMetadata(songId); if (_metadata == null || _metadata.length == 0) { throw 'Could not find song data for songId: $songId'; diff --git a/source/funkin/play/song/SongData.hx b/source/funkin/play/song/SongData.hx index 2ae38156a..a6d19454f 100644 --- a/source/funkin/play/song/SongData.hx +++ b/source/funkin/play/song/SongData.hx @@ -377,7 +377,7 @@ abstract SongNoteData(RawSongNoteData) function get_stepTime():Float { // TODO: Account for changes in BPM. - return this.t / Conductor.stepLengthMs; + return this.t / Conductor.stepCrochet; } /** @@ -563,7 +563,7 @@ abstract SongEventData(RawSongEventData) function get_stepTime():Float { // TODO: Account for changes in BPM. - return this.t / Conductor.stepLengthMs; + return this.t / Conductor.stepCrochet; } public var event(get, set):String; diff --git a/source/funkin/play/song/SongMigrator.hx b/source/funkin/play/song/SongMigrator.hx index 05adf2457..81500406d 100644 --- a/source/funkin/play/song/SongMigrator.hx +++ b/source/funkin/play/song/SongMigrator.hx @@ -103,11 +103,28 @@ class SongMigrator * @param songId The ID of the song (only used for error reporting). * @return The migrated song metadata, or null if the migration failed. */ - public static function migrateSongMetadataFromLegacy(jsonData:Dynamic):SongMetadata + public static function migrateSongMetadataFromLegacy(jsonData:Dynamic, difficulty:String = 'normal'):SongMetadata { trace('Migrating song metadata from FNF Legacy.'); var songData:FNFLegacy = cast jsonData; + // Some cleanup + if (Std.isOfType(jsonData.song.notes, Array)) + { + jsonData.song.notes = haxe.ds.Either.Left(jsonData.song.notes); + } + else + { + jsonData.song.notes = haxe.ds.Either.Right(jsonData.song.notes); + } + if (Std.isOfType(jsonData.song.speed, Float)) + { + jsonData.song.speed = haxe.ds.Either.Left(jsonData.song.speed); + } + else + { + jsonData.song.speed = haxe.ds.Either.Right(jsonData.song.speed); + } var songMetadata:SongMetadata = new SongMetadata('Import', 'Kawai Sprite', 'default'); @@ -152,9 +169,20 @@ class SongMigrator songMetadata.playData.difficulties = []; if (songData.song != null && songData.song.notes != null) { - if (songData.song.notes.easy != null) songMetadata.playData.difficulties.push('easy'); - if (songData.song.notes.normal != null) songMetadata.playData.difficulties.push('normal'); - if (songData.song.notes.hard != null) songMetadata.playData.difficulties.push('hard'); + if (Std.isOfType(songData.song.notes, Array)) + { + // One difficulty of notes. + songMetadata.playData.difficulties.push(difficulty); + } + else + { + // Multiple difficulties of notes. + var songNoteDataDynamic:haxe.DynamicAccess = cast songData.song.notes; + for (difficultyKey in songNoteDataDynamic.keys()) + { + songMetadata.playData.difficulties.push(difficultyKey); + } + } } else { @@ -186,7 +214,7 @@ class SongMigrator * @param difficulty The difficulty to migrate. * @return The migrated song chart data, or null if the migration failed. */ - public static function migrateSongChartDataFromLegacy(jsonData:Dynamic):SongChartData + public static function migrateSongChartDataFromLegacy(jsonData:Dynamic, difficulty:String = 'normal'):SongChartData { trace('Migrating song chart data from FNF Legacy.'); @@ -194,27 +222,10 @@ class SongMigrator var songChartData:SongChartData = new SongChartData(1.0, [], []); - if (songData.song.notes.normal != null) - { - var songEventsEmpty:Bool = songChartData.getEvents() == null || songChartData.getEvents().length == 0; - if (songEventsEmpty) songChartData.setEvents(migrateSongEventDataFromLegacy(songData.song.notes.normal)); - songChartData.setNotes(migrateSongNoteDataFromLegacy(songData.song.notes.normal), 'normal'); - songChartData.setScrollSpeed(songData.song.speed.normal, 'normal'); - } - if (songData.song.notes.easy != null) - { - var songEventsEmpty:Bool = songChartData.getEvents() == null || songChartData.getEvents().length == 0; - if (songEventsEmpty) songChartData.setEvents(migrateSongEventDataFromLegacy(songData.song.notes.easy)); - songChartData.setNotes(migrateSongNoteDataFromLegacy(songData.song.notes.easy), 'easy'); - songChartData.setScrollSpeed(songData.song.speed.easy, 'easy'); - } - if (songData.song.notes.hard != null) - { - var songEventsEmpty:Bool = songChartData.getEvents() == null || songChartData.getEvents().length == 0; - if (songEventsEmpty) songChartData.setEvents(migrateSongEventDataFromLegacy(songData.song.notes.hard)); - songChartData.setNotes(migrateSongNoteDataFromLegacy(songData.song.notes.hard), 'hard'); - songChartData.setScrollSpeed(songData.song.speed.hard, 'hard'); - } + var songEventsEmpty:Bool = songChartData.getEvents() == null || songChartData.getEvents().length == 0; + if (songEventsEmpty) songChartData.setEvents(migrateSongEventDataFromLegacy(songData.song.notes)); + songChartData.setNotes(migrateSongNoteDataFromLegacy(songData.song.notes), difficulty); + songChartData.setScrollSpeed(songData.song.speed, difficulty); return songChartData; } diff --git a/source/funkin/play/song/SongSerializer.hx b/source/funkin/play/song/SongSerializer.hx index 968a7a1f5..a08b722da 100644 --- a/source/funkin/play/song/SongSerializer.hx +++ b/source/funkin/play/song/SongSerializer.hx @@ -82,9 +82,9 @@ class SongSerializer * Save a SongChartData object as a JSON file to an automatically generated path. * Works great on HTML5 and desktop. */ - public static function exportSongChartData(data:SongChartData) + public static function exportSongChartData(data:SongChartData, songId:String) { - var path = 'chart.json'; + var path = '${songId}-chart.json'; exportSongChartDataAs(path, data); } @@ -92,9 +92,9 @@ class SongSerializer * Save a SongMetadata object as a JSON file to an automatically generated path. * Works great on HTML5 and desktop. */ - public static function exportSongMetadata(data:SongMetadata) + public static function exportSongMetadata(data:SongMetadata, songId:String) { - var path = 'metadata.json'; + var path = '${songId}-metadata.json'; exportSongMetadataAs(path, data); } diff --git a/source/funkin/play/song/formats/FNFLegacy.hx b/source/funkin/play/song/formats/FNFLegacy.hx index 51dc602f3..a64e461bd 100644 --- a/source/funkin/play/song/formats/FNFLegacy.hx +++ b/source/funkin/play/song/formats/FNFLegacy.hx @@ -10,10 +10,10 @@ typedef LegacySongData = var player1:String; // Boyfriend var player2:String; // Opponent - var speed:LegacyScrollSpeeds; + var speed:Float; var stageDefault:String; var bpm:Float; - var notes:LegacyNoteData; + var notes:Array; var song:String; // Song name }; diff --git a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx index 4044df5d8..ec1b0754d 100644 --- a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx +++ b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx @@ -601,7 +601,7 @@ class ChartEditorDialogHandler { trace('Adding vocal upload for character ${charKey}'); var charMetadata:CharacterData = CharacterDataParser.fetchCharacterData(charKey); - var charName:String = charMetadata.name; + var charName:String = charMetadata != null ? charMetadata.name : charKey; var vocalsEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_UPLOAD_VOCALS_ENTRY_LAYOUT); diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 173e64d24..0769ee800 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -3367,10 +3367,10 @@ class ChartEditorState extends HaxeUIState audioVocalTrackGroup.clear(); } // Add player vocals. - if (currentSongCharacterPlayer != null) audioVocalTrackGroup.addPlayerVocals(new FlxSound().loadEmbedded(Assets.getSound(Paths.voices(songId, + if (currentSongCharacterPlayer != null) audioVocalTrackGroup.addPlayerVoice(new FlxSound().loadEmbedded(Assets.getSound(Paths.voices(songId, '-$currentSongCharacterPlayer')))); // Add opponent vocals. - if (currentSongCharacterOpponent != null) audioVocalTrackGroup.addOpponentVocals(new FlxSound().loadEmbedded(Assets.getSound(Paths.voices(songId, + if (currentSongCharacterOpponent != null) audioVocalTrackGroup.addOpponentVoice(new FlxSound().loadEmbedded(Assets.getSound(Paths.voices(songId, '-$currentSongCharacterOpponent')))); postLoadInstrumental(); diff --git a/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx b/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx index 9f976df6b..6784b9cfc 100644 --- a/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx +++ b/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx @@ -374,11 +374,11 @@ class ChartEditorToolboxHandler var difficultyToolboxLoadChart:Button = toolbox.findComponent('difficultyToolboxLoadChart', Button); difficultyToolboxSaveMetadata.onClick = function(event:UIEvent) { - SongSerializer.exportSongMetadata(state.currentSongMetadata); + SongSerializer.exportSongMetadata(state.currentSongMetadata, state); }; difficultyToolboxSaveChart.onClick = function(event:UIEvent) { - SongSerializer.exportSongChartData(state.currentSongChartData); + SongSerializer.exportSongChartData(state.currentSongChartData, state); }; difficultyToolboxSaveAll.onClick = function(event:UIEvent) { From 6f846751c7f22f98ddc9a0cc813f8e9c7bd7b929 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Fri, 9 Jun 2023 15:32:44 -0400 Subject: [PATCH 11/14] Typo fix --- source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx b/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx index 6784b9cfc..5cace2ff6 100644 --- a/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx +++ b/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx @@ -374,11 +374,11 @@ class ChartEditorToolboxHandler var difficultyToolboxLoadChart:Button = toolbox.findComponent('difficultyToolboxLoadChart', Button); difficultyToolboxSaveMetadata.onClick = function(event:UIEvent) { - SongSerializer.exportSongMetadata(state.currentSongMetadata, state); + SongSerializer.exportSongMetadata(state.currentSongMetadata, state.currentSongId); }; difficultyToolboxSaveChart.onClick = function(event:UIEvent) { - SongSerializer.exportSongChartData(state.currentSongChartData, state); + SongSerializer.exportSongChartData(state.currentSongChartData, state.currentSongId); }; difficultyToolboxSaveAll.onClick = function(event:UIEvent) { From b847212375ea4740a8b175a25ac95886fd988cfb Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Sat, 10 Jun 2023 03:59:25 -0400 Subject: [PATCH 12/14] Migrator fixes --- source/funkin/play/song/SongMigrator.hx | 17 ----------------- source/funkin/util/SerializerUtil.hx | 11 ++++++++++- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/source/funkin/play/song/SongMigrator.hx b/source/funkin/play/song/SongMigrator.hx index 81500406d..f561c4d3e 100644 --- a/source/funkin/play/song/SongMigrator.hx +++ b/source/funkin/play/song/SongMigrator.hx @@ -108,23 +108,6 @@ class SongMigrator trace('Migrating song metadata from FNF Legacy.'); var songData:FNFLegacy = cast jsonData; - // Some cleanup - if (Std.isOfType(jsonData.song.notes, Array)) - { - jsonData.song.notes = haxe.ds.Either.Left(jsonData.song.notes); - } - else - { - jsonData.song.notes = haxe.ds.Either.Right(jsonData.song.notes); - } - if (Std.isOfType(jsonData.song.speed, Float)) - { - jsonData.song.speed = haxe.ds.Either.Left(jsonData.song.speed); - } - else - { - jsonData.song.speed = haxe.ds.Either.Right(jsonData.song.speed); - } var songMetadata:SongMetadata = new SongMetadata('Import', 'Kawai Sprite', 'default'); diff --git a/source/funkin/util/SerializerUtil.hx b/source/funkin/util/SerializerUtil.hx index 662c05250..9452b7785 100644 --- a/source/funkin/util/SerializerUtil.hx +++ b/source/funkin/util/SerializerUtil.hx @@ -44,7 +44,16 @@ class SerializerUtil */ public static function fromJSONBytes(input:Bytes):Dynamic { - return Json.parse(input.toString()); + try + { + return Json.parse(input.toString()); + } + catch (e:Dynamic) + { + trace('An error occurred while parsing JSON from byte data'); + trace(e); + return null; + } } /** From 3bf7955ffad4b32436f340eb18798b420f7ad461 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Mon, 3 Jul 2023 13:16:02 -0400 Subject: [PATCH 13/14] Conductor fix --- source/funkin/ui/debug/charting/ChartEditorState.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 0c0f31d06..f5a87ceb1 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -3119,7 +3119,7 @@ class ChartEditorState extends HaxeUIState var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels; var playheadPosFractionalStep:Float = playheadPos / GRID_SIZE / (16 / noteSnapQuant); var playheadPosStep:Int = Std.int(Math.floor(playheadPosFractionalStep)); - var playheadPosMs:Float = playheadPosStep * Conductor.stepCrochet * (16 / noteSnapQuant); + var playheadPosMs:Float = playheadPosStep * Conductor.stepLengthMs * (16 / noteSnapQuant); var newNoteData:SongNoteData = new SongNoteData(playheadPosMs, column, 0, selectedNoteKind); performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL)); From f634a099c36613c98cf57032b1e943ace4dc5695 Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Sat, 22 Jul 2023 14:43:28 -0400 Subject: [PATCH 14/14] actual songdata git merge proper lol --- source/funkin/play/song/SongData.hx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/funkin/play/song/SongData.hx b/source/funkin/play/song/SongData.hx index 4ec90d3ec..b0ab4ccf1 100644 --- a/source/funkin/play/song/SongData.hx +++ b/source/funkin/play/song/SongData.hx @@ -895,12 +895,12 @@ abstract SongTimeChange(RawSongTimeChange) from RawSongTimeChange public var beatTime(get, set):Null; - function get_beatTime():Int + public function get_beatTime():Null { return this.b; } - function set_beatTime(value:Int):Int + public function set_beatTime(value:Null):Null { return this.b = value; }