diff --git a/.vscode/launch.json b/.vscode/launch.json index d16f1ca4f..74f72b826 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -23,6 +23,12 @@ "name": "Haxe Eval", "type": "haxe-eval", "request": "launch" + }, + { + // Attaches the debugger to an already running game + "name": "HXCPP - Attach", + "type": "hxcpp", + "request": "attach" } ] } diff --git a/source/funkin/PauseSubState.hx b/source/funkin/PauseSubState.hx index a074410ea..2ce9abf65 100644 --- a/source/funkin/PauseSubState.hx +++ b/source/funkin/PauseSubState.hx @@ -240,7 +240,7 @@ class PauseSubState extends MusicBeatSubState case 'Exit to Chart Editor': this.close(); - if (FlxG.sound.music != null) FlxG.sound.music.stop(); + if (FlxG.sound.music != null) FlxG.sound.music.pause(); // Don't reset song position! PlayState.instance.close(); // This only works because PlayState is a substate! case 'BACK': diff --git a/source/funkin/data/DataParse.hx b/source/funkin/data/DataParse.hx index 4a422b368..64a53d2a4 100644 --- a/source/funkin/data/DataParse.hx +++ b/source/funkin/data/DataParse.hx @@ -1,13 +1,15 @@ package funkin.data; import funkin.data.song.importer.FNFLegacyData.LegacyNote; -import hxjsonast.Json; -import hxjsonast.Tools; -import hxjsonast.Json.JObjectField; -import haxe.ds.Either; -import funkin.data.song.importer.FNFLegacyData.LegacyNoteSection; import funkin.data.song.importer.FNFLegacyData.LegacyNoteData; +import funkin.data.song.importer.FNFLegacyData.LegacyNoteSection; import funkin.data.song.importer.FNFLegacyData.LegacyScrollSpeeds; +import haxe.ds.Either; +import hxjsonast.Json; +import hxjsonast.Json.JObjectField; +import hxjsonast.Tools; +import thx.semver.Version; +import thx.semver.VersionRule; /** * `json2object` has an annotation `@:jcustomparse` which allows for mutation of parsed values. @@ -23,7 +25,8 @@ class DataParse * `@:jcustomparse(funkin.data.DataParse.stringNotEmpty)` * @param json Contains the `pos` and `value` of the property. * @param name The name of the property. - * @throws If the property is not a string or is empty. + * @throws Error If the property is not a string or is empty. + * @return The string value. */ public static function stringNotEmpty(json:Json, name:String):String { @@ -37,6 +40,42 @@ class DataParse } } + /** + * `@:jcustomparse(funkin.data.DataParse.semverVersion)` + * @param json Contains the `pos` and `value` of the property. + * @param name The name of the property. + * @return The value of the property as a `thx.semver.Version`. + */ + public static function semverVersion(json:Json, name:String):Version + { + switch (json.value) + { + case JString(s): + if (s == "") throw 'Expected version property $name to be non-empty.'; + return s; + default: + throw 'Expected version property $name to be a string, but it was ${json.value}.'; + } + } + + /** + * `@:jcustomparse(funkin.data.DataParse.semverVersionRule)` + * @param json Contains the `pos` and `value` of the property. + * @param name The name of the property. + * @return The value of the property as a `thx.semver.VersionRule`. + */ + public static function semverVersionRule(json:Json, name:String):VersionRule + { + switch (json.value) + { + case JString(s): + if (s == "") throw 'Expected version rule property $name to be non-empty.'; + return s; + default: + throw 'Expected version rule property $name to be a string, but it was ${json.value}.'; + } + } + /** * Parser which outputs a Dynamic value, either a object or something else. * @param json diff --git a/source/funkin/data/DataWrite.hx b/source/funkin/data/DataWrite.hx index 41993107f..2f3a7632f 100644 --- a/source/funkin/data/DataWrite.hx +++ b/source/funkin/data/DataWrite.hx @@ -1,6 +1,8 @@ package funkin.data; import funkin.util.SerializerUtil; +import thx.semver.Version; +import thx.semver.VersionRule; /** * `json2object` has an annotation `@:jcustomwrite` which allows for custom serialization of values to be written to JSON. @@ -9,9 +11,30 @@ import funkin.util.SerializerUtil; */ class DataWrite { + /** + * `@:jcustomwrite(funkin.data.DataWrite.dynamicValue)` + * @param value + * @return String + */ public static function dynamicValue(value:Dynamic):String { // Is this cheating? Yes. Do I care? No. return SerializerUtil.toJSON(value); } + + /** + * `@:jcustomwrite(funkin.data.DataWrite.semverVersion)` + */ + public static function semverVersion(value:Version):String + { + return value.toString(); + } + + /** + * `@:jcustomwrite(funkin.data.DataWrite.semverVersionRule)` + */ + public static function semverVersionRule(value:VersionRule):String + { + return value.toString(); + } } diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx index 9340e46c9..88993e519 100644 --- a/source/funkin/data/song/SongData.hx +++ b/source/funkin/data/song/SongData.hx @@ -12,6 +12,8 @@ class SongMetadata * */ // @:default(funkin.data.song.SongRegistry.SONG_METADATA_VERSION) + @:jcustomparse(funkin.data.DataParse.semverVersion) + @:jcustomwrite(funkin.data.DataWrite.semverVersion) public var version:Version; @:default("Unknown") @@ -203,6 +205,8 @@ class SongMusicData * */ // @:default(funkin.data.song.SongRegistry.SONG_METADATA_VERSION) + @:jcustomparse(funkin.data.DataParse.semverVersion) + @:jcustomwrite(funkin.data.DataWrite.semverVersion) public var version:Version; @:default("Unknown") @@ -367,6 +371,8 @@ class SongCharacterData class SongChartData { @:default(funkin.data.song.SongRegistry.SONG_CHART_DATA_VERSION) + @:jcustomparse(funkin.data.DataParse.semverVersion) + @:jcustomwrite(funkin.data.DataWrite.semverVersion) public var version:Version; public var scrollSpeed:Map; diff --git a/source/funkin/data/song/SongDataUtils.hx b/source/funkin/data/song/SongDataUtils.hx index ee3dfe98c..3ff3943c6 100644 --- a/source/funkin/data/song/SongDataUtils.hx +++ b/source/funkin/data/song/SongDataUtils.hx @@ -246,7 +246,8 @@ class SongDataUtils typedef SongClipboardItems = { - ?valid:Bool, - notes:Array, - events:Array + @:optional + var valid:Bool; + var notes:Array; + var events:Array; } diff --git a/source/funkin/data/song/migrator/SongData_v2_0_0.hx b/source/funkin/data/song/migrator/SongData_v2_0_0.hx index 935e7349c..eeeed2f2b 100644 --- a/source/funkin/data/song/migrator/SongData_v2_0_0.hx +++ b/source/funkin/data/song/migrator/SongData_v2_0_0.hx @@ -24,6 +24,8 @@ class SongMetadata_v2_0_0 // ========== // UNMODIFIED VALUES // ========== + @:jcustomparse(funkin.data.DataParse.semverVersion) + @:jcustomwrite(funkin.data.DataWrite.semverVersion) public var version:Version; @:default("Unknown") diff --git a/source/funkin/play/HealthIcon.hx b/source/funkin/play/HealthIcon.hx index 5b9c8ec75..958933df8 100644 --- a/source/funkin/play/HealthIcon.hx +++ b/source/funkin/play/HealthIcon.hx @@ -129,7 +129,6 @@ class HealthIcon extends FlxSprite if (value == characterId) return value; characterId = value ?? Constants.DEFAULT_HEALTH_ICON; - loadCharacter(characterId); return characterId; } @@ -138,7 +137,6 @@ class HealthIcon extends FlxSprite if (value == isPixel) return value; isPixel = value; - loadCharacter(characterId); return isPixel; } @@ -165,8 +163,11 @@ class HealthIcon extends FlxSprite { if (data == null) { - this.isPixel = false; this.characterId = Constants.DEFAULT_HEALTH_ICON; + this.isPixel = false; + + loadCharacter(characterId); + this.size.set(1.0, 1.0); this.offset.x = 0.0; this.offset.y = 0.0; @@ -174,8 +175,11 @@ class HealthIcon extends FlxSprite } else { - this.isPixel = data.isPixel ?? false; this.characterId = data.id; + this.isPixel = data.isPixel ?? false; + + loadCharacter(characterId); + this.size.set(data.scale ?? 1.0, data.scale ?? 1.0); this.offset.x = (data.offsets != null) ? data.offsets[0] : 0.0; this.offset.y = (data.offsets != null) ? data.offsets[1] : 0.0; diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 8b47e6ebd..9ccc66f69 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -698,7 +698,15 @@ class PlayState extends MusicBeatSubState FlxG.sound.music.pause(); FlxG.sound.music.time = (startTimestamp); - vocals = currentChart.buildVocals(); + if (!overrideMusic) + { + vocals = currentChart.buildVocals(); + + if (vocals.members.length == 0) + { + trace('WARNING: No vocals found for this song.'); + } + } vocals.pause(); vocals.time = 0; diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index f996d75ef..60b8b9864 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -154,6 +154,12 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry = dialog.findComponent('inputSongName', TextField); if (inputSongName == null) throw 'Could not locate inputSongName TextField in Song Metadata dialog'; inputSongName.onChange = function(event:UIEvent) { diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 30d8fde7e..40f841fd8 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -554,6 +554,9 @@ class ChartEditorState extends HaxeUIState notePreviewDirty = true; notePreviewViewportBoundsDirty = true; + // Make sure the difficulty we selected is in the list of difficulties. + currentSongMetadata.playData.difficulties.pushUnique(selectedDifficulty); + return selectedDifficulty; } @@ -971,6 +974,7 @@ class ChartEditorState extends HaxeUIState result = []; trace('Initializing blank note data for difficulty ' + selectedDifficulty); currentSongChartData.notes.set(selectedDifficulty, result); + currentSongMetadata.playData.difficulties.pushUnique(selectedDifficulty); return result; } return result; @@ -979,6 +983,7 @@ class ChartEditorState extends HaxeUIState function set_currentSongChartNoteData(value:Array):Array { currentSongChartData.notes.set(selectedDifficulty, value); + currentSongMetadata.playData.difficulties.pushUnique(selectedDifficulty); return value; } @@ -4089,7 +4094,7 @@ class ChartEditorState extends HaxeUIState } subStateClosed.add(fixCamera); - subStateClosed.add(updateConductor); + subStateClosed.add(resetConductorAfterTest); FlxTransitionableState.skipNextTransIn = false; FlxTransitionableState.skipNextTransOut = false; @@ -4122,10 +4127,9 @@ class ChartEditorState extends HaxeUIState add(this.component); } - function updateConductor(_:FlxSubState = null):Void + function resetConductorAfterTest(_:FlxSubState = null):Void { - var targetPos = scrollPositionInMs; - Conductor.update(targetPos); + moveSongToScrollPosition(); } public function postLoadInstrumental():Void @@ -4179,12 +4183,14 @@ class ChartEditorState extends HaxeUIState function moveSongToScrollPosition():Void { // Update the songPosition in the audio tracks. - if (audioInstTrack != null) audioInstTrack.time = scrollPositionInMs + playheadPositionInMs; + if (audioInstTrack != null) + { + audioInstTrack.time = scrollPositionInMs + playheadPositionInMs; + // Update the songPosition in the Conductor. + Conductor.update(audioInstTrack.time); + } if (audioVocalTrackGroup != null) audioVocalTrackGroup.time = scrollPositionInMs + playheadPositionInMs; - // Update the songPosition in the Conductor. - Conductor.update(audioInstTrack.time); - // We need to update the note sprites because we changed the scroll position. noteDisplayDirty = true; } diff --git a/source/funkin/util/tools/ArrayTools.hx b/source/funkin/util/tools/ArrayTools.hx index 67cc1c041..1c9f38de5 100644 --- a/source/funkin/util/tools/ArrayTools.hx +++ b/source/funkin/util/tools/ArrayTools.hx @@ -38,6 +38,19 @@ class ArrayTools return null; } + /** + * Push an element to the array if it is not already present. + * @param input The array to push to + * @param element The element to push + * @return Whether the element was pushed + */ + public static function pushUnique(input:Array, element:T):Bool + { + if (input.contains(element)) return false; + input.push(element); + return true; + } + /** * Remove all elements from the array, without creating a new array. * @param array The array to clear.