More chart editor changes

This commit is contained in:
EliteMasterEric 2023-06-08 17:07:35 -04:00
parent c3577b32ef
commit 26998c9164
7 changed files with 350 additions and 140 deletions

View file

@ -324,7 +324,7 @@ class SongDifficulty
/** /**
* Build a list of vocal files for the given character. * Build a list of vocal files for the given character.
* Automatically resolves suffixed character IDs (so bf-car will resolve to bf if needed). * Automatically resolves suffixed character IDs (so bf-car will resolve to bf if needed).
* *
* @param id The character we are about to play. * @param id The character we are about to play.
*/ */
public function buildVoiceList(?id:String = 'bf'):Array<String> public function buildVoiceList(?id:String = 'bf'):Array<String>

View file

@ -1,5 +1,7 @@
package funkin.play.song; package funkin.play.song;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.modding.events.ScriptEvent;
import flixel.util.typeLimit.OneOfTwo; import flixel.util.typeLimit.OneOfTwo;
import funkin.play.song.ScriptedSong; import funkin.play.song.ScriptedSong;
import funkin.util.assets.DataAssets; 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. * 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. * If you want to force song metadata to be reloaded, you can just call this function again.
*/ */
public static function loadSongCache():Void public static function loadSongCache():Void
@ -95,6 +97,9 @@ class SongDataParser
{ {
var song:Song = songCache.get(songId); var song:Song = songCache.get(songId);
trace('Successfully fetch song: ${songId}'); trace('Successfully fetch song: ${songId}');
var event:ScriptEvent = new ScriptEvent(ScriptEvent.CREATE, false);
ScriptEventDispatcher.callEvent(song, event);
return song; return song;
} }
else 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<String> public static function listSongIds():Array<String>
{ {
return songCache.keys().array(); return songCache.keys().array();
} }
public static function parseSongMetadata(songId:String):Array<SongMetadata> /**
* 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<SongMetadata>
{ {
var result:Array<SongMetadata> = []; var result:Array<SongMetadata> = [];
@ -139,19 +153,13 @@ class SongDataParser
result.push(songMetadata); result.push(songMetadata);
var variations = songMetadata.playData.songVariations; var variations:Array<String> = songMetadata.playData.songVariations;
for (variation in variations) for (variation in variations)
{ {
var variationJsonStr:String = loadSongMetadataFile(songId, variation); var variationRawJson:String = loadSongMetadataFile(songId, variation);
var variationJsonData:Dynamic = null; var variationSongMetadata:SongMetadata = SongMigrator.migrateSongMetadata(variationRawJson, '${songId}_${variation}');
try variationSongMetadata = SongValidator.validateSongMetadata(variationSongMetadata, '${songId}_${variation}');
{
variationJsonData = Json.parse(variationJsonStr);
}
catch (e) {}
var variationSongMetadata:SongMetadata = SongMigrator.migrateSongMetadata(variationJsonData, '${songId}-${variation}');
variationSongMetadata = SongValidator.validateSongMetadata(variationSongMetadata, '${songId}-${variation}');
if (variationSongMetadata != null) if (variationSongMetadata != null)
{ {
variationSongMetadata.variation = variation; variationSongMetadata.variation = variation;
@ -168,7 +176,7 @@ class SongDataParser
var rawJson:String = Assets.getText(songMetadataFilePath).trim(); var rawJson:String = Assets.getText(songMetadataFilePath).trim();
while (!rawJson.endsWith("}")) while (!rawJson.endsWith('}') && rawJson.length > 0)
{ {
rawJson = rawJson.substr(0, rawJson.length - 1); rawJson = rawJson.substr(0, rawJson.length - 1);
} }
@ -176,7 +184,7 @@ class SongDataParser
return rawJson; 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 rawJson:String = loadSongChartDataFile(songId, variation);
var jsonData:Dynamic = null; var jsonData:Dynamic = null;
@ -184,7 +192,11 @@ class SongDataParser
{ {
jsonData = Json.parse(rawJson); 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); var songChartData:SongChartData = SongMigrator.migrateSongChartData(jsonData, songId);
songChartData = SongValidator.validateSongChartData(songChartData, songId); songChartData = SongValidator.validateSongChartData(songChartData, songId);
@ -204,7 +216,7 @@ class SongDataParser
var rawJson:String = Assets.getText(songChartDataFilePath).trim(); var rawJson:String = Assets.getText(songChartDataFilePath).trim();
while (!rawJson.endsWith("}")) while (!rawJson.endsWith('}') && rawJson.length > 0)
{ {
rawJson = rawJson.substr(0, rawJson.length - 1); rawJson = rawJson.substr(0, rawJson.length - 1);
} }
@ -217,7 +229,7 @@ typedef RawSongMetadata =
{ {
/** /**
* A semantic versioning string for the song data format. * A semantic versioning string for the song data format.
* *
*/ */
var version:Version; var version:Version;
@ -272,7 +284,7 @@ abstract SongMetadata(RawSongMetadata)
public function clone(?newVariation:String = null):SongMetadata 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.version = this.version;
result.timeFormat = this.timeFormat; result.timeFormat = this.timeFormat;
result.divisions = this.divisions; result.divisions = this.divisions;
@ -350,22 +362,22 @@ abstract SongNoteData(RawSongNoteData)
public var time(get, set):Float; public var time(get, set):Float;
public function get_time():Float function get_time():Float
{ {
return this.t; return this.t;
} }
public function set_time(value:Float):Float function set_time(value:Float):Float
{ {
return this.t = value; return this.t = value;
} }
public var stepTime(get, never):Float; public var stepTime(get, never):Float;
public function get_stepTime():Float function get_stepTime():Float
{ {
// TODO: Account for changes in BPM. // 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 var data(get, set):Int;
public function get_data():Int function get_data():Int
{ {
return this.d; return this.d;
} }
public function set_data(value:Int):Int function set_data(value:Int):Int
{ {
return this.d = value; return this.d = value;
} }
@ -414,7 +426,7 @@ abstract SongNoteData(RawSongNoteData)
/** /**
* The strumline index of the note, if applicable. * The strumline index of the note, if applicable.
* Strips the direction from the data. * Strips the direction from the data.
* *
* 0 = player, 1 = opponent, etc. * 0 = player, 1 = opponent, etc.
*/ */
public inline function getStrumlineIndex(strumlineSize:Int = 4):Int public inline function getStrumlineIndex(strumlineSize:Int = 4):Int
@ -429,26 +441,26 @@ abstract SongNoteData(RawSongNoteData)
public var length(get, set):Float; public var length(get, set):Float;
public function get_length():Float function get_length():Float
{ {
return this.l; return this.l;
} }
public function set_length(value:Float):Float function set_length(value:Float):Float
{ {
return this.l = value; return this.l = value;
} }
public var kind(get, set):String; public var kind(get, set):String;
public function get_kind():String function get_kind():String
{ {
if (this.k == null || this.k == '') return 'normal'; if (this.k == null || this.k == '') return 'normal';
return this.k; return this.k;
} }
public function set_kind(value:String):String function set_kind(value:String):String
{ {
if (value == 'normal' || value == '') value = null; if (value == 'normal' || value == '') value = null;
return this.k = value; return this.k = value;
@ -536,56 +548,56 @@ abstract SongEventData(RawSongEventData)
public var time(get, set):Float; public var time(get, set):Float;
public function get_time():Float function get_time():Float
{ {
return this.t; return this.t;
} }
public function set_time(value:Float):Float function set_time(value:Float):Float
{ {
return this.t = value; return this.t = value;
} }
public var stepTime(get, never):Float; public var stepTime(get, never):Float;
public function get_stepTime():Float function get_stepTime():Float
{ {
// TODO: Account for changes in BPM. // TODO: Account for changes in BPM.
return this.t / Conductor.stepCrochet; return this.t / Conductor.stepLengthMs;
} }
public var event(get, set):String; public var event(get, set):String;
public function get_event():String function get_event():String
{ {
return this.e; return this.e;
} }
public function set_event(value:String):String function set_event(value:String):String
{ {
return this.e = value; return this.e = value;
} }
public var value(get, set):Dynamic; public var value(get, set):Dynamic;
public function get_value():Dynamic function get_value():Dynamic
{ {
return this.v; return this.v;
} }
public function set_value(value:Dynamic):Dynamic function set_value(value:Dynamic):Dynamic
{ {
return this.v = value; return this.v = value;
} }
public var activated(get, set):Bool; public var activated(get, set):Bool;
public function get_activated():Bool function get_activated():Bool
{ {
return this.a; return this.a;
} }
public function set_activated(value:Bool):Bool function set_activated(value:Bool):Bool
{ {
return this.a = value; return this.a = value;
} }
@ -664,7 +676,7 @@ abstract SongEventData(RawSongEventData)
abstract SongPlayableChar(RawSongPlayableChar) abstract SongPlayableChar(RawSongPlayableChar)
{ {
public function new(girlfriend:String, opponent:String, inst:String = "") public function new(girlfriend:String, opponent:String, inst:String = '')
{ {
this = this =
{ {
@ -676,36 +688,36 @@ abstract SongPlayableChar(RawSongPlayableChar)
public var girlfriend(get, set):String; public var girlfriend(get, set):String;
public function get_girlfriend():String function get_girlfriend():String
{ {
return this.g; return this.g;
} }
public function set_girlfriend(value:String):String function set_girlfriend(value:String):String
{ {
return this.g = value; return this.g = value;
} }
public var opponent(get, set):String; public var opponent(get, set):String;
public function get_opponent():String function get_opponent():String
{ {
return this.o; return this.o;
} }
public function set_opponent(value:String):String function set_opponent(value:String):String
{ {
return this.o = value; return this.o = value;
} }
public var inst(get, set):String; public var inst(get, set):String;
public function get_inst():String function get_inst():String
{ {
return this.i; return this.i;
} }
public function set_inst(value:String):String function set_inst(value:String):String
{ {
return this.i = value; return this.i = value;
} }
@ -751,6 +763,35 @@ abstract SongChartData(RawSongChartData)
return (result == 0.0) ? 1.0 : result; 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<SongNoteData>
{
var result:Array<SongNoteData> = this.notes.get(diff);
if (result == null && diff != 'normal') return getNotes('normal');
return (result == null) ? [] : result;
}
public function setNotes(value:Array<SongNoteData>, diff:String):Array<SongNoteData>
{
return this.notes.set(diff, value);
}
public function getEvents():Array<SongEventData>
{
return this.events;
}
public function setEvents(value:Array<SongEventData>):Array<SongEventData>
{
return this.events = value;
}
} }
typedef RawSongTimeChange = typedef RawSongTimeChange =
@ -811,67 +852,67 @@ abstract SongTimeChange(RawSongTimeChange)
public var timeStamp(get, set):Float; public var timeStamp(get, set):Float;
public function get_timeStamp():Float function get_timeStamp():Float
{ {
return this.t; return this.t;
} }
public function set_timeStamp(value:Float):Float function set_timeStamp(value:Float):Float
{ {
return this.t = value; return this.t = value;
} }
public var beatTime(get, set):Int; public var beatTime(get, set):Int;
public function get_beatTime():Int function get_beatTime():Int
{ {
return this.b; return this.b;
} }
public function set_beatTime(value:Int):Int function set_beatTime(value:Int):Int
{ {
return this.b = value; return this.b = value;
} }
public var bpm(get, set):Float; public var bpm(get, set):Float;
public function get_bpm():Float function get_bpm():Float
{ {
return this.bpm; return this.bpm;
} }
public function set_bpm(value:Float):Float function set_bpm(value:Float):Float
{ {
return this.bpm = value; return this.bpm = value;
} }
public var timeSignatureNum(get, set):Int; public var timeSignatureNum(get, set):Int;
public function get_timeSignatureNum():Int function get_timeSignatureNum():Int
{ {
return this.n; return this.n;
} }
public function set_timeSignatureNum(value:Int):Int function set_timeSignatureNum(value:Int):Int
{ {
return this.n = value; return this.n = value;
} }
public var timeSignatureDen(get, set):Int; public var timeSignatureDen(get, set):Int;
public function get_timeSignatureDen():Int function get_timeSignatureDen():Int
{ {
return this.d; return this.d;
} }
public function set_timeSignatureDen(value:Int):Int function set_timeSignatureDen(value:Int):Int
{ {
return this.d = value; return this.d = value;
} }
public var beatTuplets(get, set):Array<Int>; public var beatTuplets(get, set):Array<Int>;
public function get_beatTuplets():Array<Int> function get_beatTuplets():Array<Int>
{ {
if (Std.isOfType(this.bt, Int)) if (Std.isOfType(this.bt, Int))
{ {
@ -883,7 +924,7 @@ abstract SongTimeChange(RawSongTimeChange)
} }
} }
public function set_beatTuplets(value:Array<Int>):Array<Int> function set_beatTuplets(value:Array<Int>):Array<Int>
{ {
return this.bt = value; return this.bt = value;
} }
@ -891,7 +932,7 @@ abstract SongTimeChange(RawSongTimeChange)
enum abstract SongTimeFormat(String) from String to String enum abstract SongTimeFormat(String) from String to String
{ {
var TICKS = "ticks"; var TICKS = 'ticks';
var FLOAT = "float"; var FLOAT = 'float';
var MILLISECONDS = "ms"; var MILLISECONDS = 'ms';
} }

View file

@ -14,14 +14,13 @@ class SongDataUtils
* Given an array of SongNoteData objects, return a new array of SongNoteData objects * Given an array of SongNoteData objects, return a new array of SongNoteData objects
* whose timestamps are shifted by the given amount. * whose timestamps are shifted by the given amount.
* Does not mutate the original array. * Does not mutate the original array.
* *
* @param notes The notes to modify. * @param notes The notes to modify.
* @param offset The time difference to apply in milliseconds. * @param offset The time difference to apply in milliseconds.
*/ */
public static function offsetSongNoteData(notes:Array<SongNoteData>, offset:Int):Array<SongNoteData> public static function offsetSongNoteData(notes:Array<SongNoteData>, offset:Int):Array<SongNoteData>
{ {
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); 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 * Given an array of SongEventData objects, return a new array of SongEventData objects
* whose timestamps are shifted by the given amount. * whose timestamps are shifted by the given amount.
* Does not mutate the original array. * Does not mutate the original array.
* *
* @param events The events to modify. * @param events The events to modify.
* @param offset The time difference to apply in milliseconds. * @param offset The time difference to apply in milliseconds.
*/ */
public static function offsetSongEventData(events:Array<SongEventData>, offset:Int):Array<SongEventData> public static function offsetSongEventData(events:Array<SongEventData>, offset:Int):Array<SongEventData>
{ {
return events.map(function(event:SongEventData):SongEventData return events.map(function(event:SongEventData):SongEventData {
{
return new SongEventData(event.time + offset, event.event, event.value); 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. * Return a new array without a certain subset of notes from an array of SongNoteData objects.
* Does not mutate the original array. * Does not mutate the original array.
* *
* @param notes The array of notes to be subtracted from. * @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. * @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; 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) for (x in subtrahend)
// SongNoteData's == operation has been overridden so that this will work. // SongNoteData's == operation has been overridden so that this will work.
if (x == note) return false; 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. * Return a new array without a certain subset of events from an array of SongEventData objects.
* Does not mutate the original array. * Does not mutate the original array.
* *
* @param events The array of events to be subtracted from. * @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. * @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; 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. // SongEventData's == operation has been overridden so that this will work.
return !subtrahend.has(event); return !subtrahend.has(event);
}); });
@ -89,8 +85,7 @@ class SongDataUtils
*/ */
public static function flipNotes(notes:Array<SongNoteData>, ?strumlineSize:Int = 4):Array<SongNoteData> public static function flipNotes(notes:Array<SongNoteData>, ?strumlineSize:Int = 4):Array<SongNoteData>
{ {
return notes.map(function(note:SongNoteData):SongNoteData return notes.map(function(note:SongNoteData):SongNoteData {
{
var newData = note.data; var newData = note.data;
if (newData < strumlineSize) newData += strumlineSize; if (newData < strumlineSize) newData += strumlineSize;
@ -103,22 +98,26 @@ class SongDataUtils
/** /**
* Prepare an array of notes to be used as the clipboard data. * 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. * Offset the provided array of notes such that the first note is at 0 milliseconds.
*/ */
public static function buildNoteClipboard(notes:Array<SongNoteData>):Array<SongNoteData> public static function buildNoteClipboard(notes:Array<SongNoteData>, ?timeOffset:Int = null):Array<SongNoteData>
{ {
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. * 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. * Offset the provided array of events such that the first event is at 0 milliseconds.
*/ */
public static function buildEventClipboard(events:Array<SongEventData>):Array<SongEventData> public static function buildEventClipboard(events:Array<SongEventData>, ?timeOffset:Int = null):Array<SongEventData>
{ {
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<SongNoteData>, ?desc:Bool = false):Array<SongNoteData> public static function sortNotes(notes:Array<SongNoteData>, ?desc:Bool = false):Array<SongNoteData>
{ {
// TODO: Modifies the array in place. Is this okay? // 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 FlxSort.byValues(desc ? FlxSort.DESCENDING : FlxSort.ASCENDING, a.time, b.time);
}); });
return notes; return notes;
@ -140,8 +138,7 @@ class SongDataUtils
public static function sortEvents(events:Array<SongEventData>, ?desc:Bool = false):Array<SongEventData> public static function sortEvents(events:Array<SongEventData>, ?desc:Bool = false):Array<SongEventData>
{ {
// TODO: Modifies the array in place. Is this okay? // 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 FlxSort.byValues(desc ? FlxSort.DESCENDING : FlxSort.ASCENDING, a.time, b.time);
}); });
return events; return events;
@ -192,8 +189,7 @@ class SongDataUtils
*/ */
public static function getNotesInTimeRange(notes:Array<SongNoteData>, start:Float, end:Float):Array<SongNoteData> public static function getNotesInTimeRange(notes:Array<SongNoteData>, start:Float, end:Float):Array<SongNoteData>
{ {
return notes.filter(function(note:SongNoteData):Bool return notes.filter(function(note:SongNoteData):Bool {
{
return note.time >= start && note.time <= end; return note.time >= start && note.time <= end;
}); });
} }
@ -203,8 +199,7 @@ class SongDataUtils
*/ */
public static function getEventsInTimeRange(events:Array<SongEventData>, start:Float, end:Float):Array<SongEventData> public static function getEventsInTimeRange(events:Array<SongEventData>, start:Float, end:Float):Array<SongEventData>
{ {
return events.filter(function(event:SongEventData):Bool return events.filter(function(event:SongEventData):Bool {
{
return event.time >= start && event.time <= end; return event.time >= start && event.time <= end;
}); });
} }
@ -214,8 +209,7 @@ class SongDataUtils
*/ */
public static function getNotesInDataRange(notes:Array<SongNoteData>, start:Int, end:Int):Array<SongNoteData> public static function getNotesInDataRange(notes:Array<SongNoteData>, start:Int, end:Int):Array<SongNoteData>
{ {
return notes.filter(function(note:SongNoteData):Bool return notes.filter(function(note:SongNoteData):Bool {
{
return note.data >= start && note.data <= end; return note.data >= start && note.data <= end;
}); });
} }
@ -225,8 +219,7 @@ class SongDataUtils
*/ */
public static function getNotesWithData(notes:Array<SongNoteData>, data:Array<Int>):Array<SongNoteData> public static function getNotesWithData(notes:Array<SongNoteData>, data:Array<Int>):Array<SongNoteData>
{ {
return notes.filter(function(note:SongNoteData):Bool return notes.filter(function(note:SongNoteData):Bool {
{
return data.indexOf(note.data) != -1; return data.indexOf(note.data) != -1;
}); });
} }

View file

@ -1,7 +1,11 @@
package funkin.play.song; package funkin.play.song;
import funkin.play.song.formats.FNFLegacy;
import funkin.play.song.SongData.SongChartData; import funkin.play.song.SongData.SongChartData;
import funkin.play.song.SongData.SongEventData;
import funkin.play.song.SongData.SongMetadata; import funkin.play.song.SongData.SongMetadata;
import funkin.play.song.SongData.SongNoteData;
import funkin.play.song.SongData.SongPlayableChar;
import funkin.util.VersionUtil; import funkin.util.VersionUtil;
class SongMigrator class SongMigrator
@ -11,13 +15,22 @@ class SongMigrator
* Handle breaking changes by incrementing this value * Handle breaking changes by incrementing this value
* and adding migration to the SongMigrator class. * 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 public static function migrateSongMetadata(jsonData:Dynamic, songId:String):SongMetadata
{ {
if (jsonData.version) if (jsonData.version != null)
{ {
if (VersionUtil.validateVersion(jsonData.version, CHART_VERSION_RULE)) if (VersionUtil.validateVersion(jsonData.version, CHART_VERSION_RULE))
{ {
@ -32,10 +45,11 @@ class SongMigrator
trace('Song (${songId}) metadata version (${jsonData.version}) is outdated.'); trace('Song (${songId}) metadata version (${jsonData.version}) is outdated.');
switch (jsonData.version) switch (jsonData.version)
{ {
// TODO: Add migration functions as cases here. case '1.0.0':
return migrateSongMetadataFromLegacy(jsonData);
default: default:
// Unknown version. trace('Song (${songId}) has unknown metadata version (${jsonData.version}), assuming FNF Legacy.');
trace('Song (${songId}) unknown metadata version: ${jsonData.version}'); return migrateSongMetadataFromLegacy(jsonData);
} }
} }
} }
@ -46,6 +60,12 @@ class SongMigrator
return null; 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 public static function migrateSongChartData(jsonData:Dynamic, songId:String):SongChartData
{ {
if (jsonData.version) if (jsonData.version)
@ -76,4 +96,167 @@ class SongMigrator
} }
return null; 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<LegacyNoteSection>):Array<SongNoteData>
{
var songNotes:Array<SongNoteData> = [];
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<LegacyNoteSection>):Array<SongEventData>
{
var songEvents:Array<SongEventData> = [];
var lastSectionWasMustHit:Null<Bool> = 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;
}
} }

View file

@ -50,8 +50,7 @@ class SongSerializer
*/ */
public static function importSongChartDataAsync(callback:SongChartData->Void):Void public static function importSongChartDataAsync(callback:SongChartData->Void):Void
{ {
browseFileReference(function(fileReference:FileReference) browseFileReference(function(fileReference:FileReference) {
{
var data = fileReference.data.toString(); var data = fileReference.data.toString();
if (data == null) return; if (data == null) return;
@ -68,8 +67,7 @@ class SongSerializer
*/ */
public static function importSongMetadataAsync(callback:SongMetadata->Void):Void public static function importSongMetadataAsync(callback:SongMetadata->Void):Void
{ {
browseFileReference(function(fileReference:FileReference) browseFileReference(function(fileReference:FileReference) {
{
var data = fileReference.data.toString(); var data = fileReference.data.toString();
if (data == null) return; if (data == null) return;
@ -103,7 +101,7 @@ class SongSerializer
/** /**
* Save a SongChartData object as a JSON file to a specified path. * Save a SongChartData object as a JSON file to a specified path.
* Works great on HTML5 and desktop. * Works great on HTML5 and desktop.
* *
* @param path The file path to save to. * @param path The file path to save to.
*/ */
public static function exportSongChartDataAs(path:String, data:SongChartData) 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. * Save a SongMetadata object as a JSON file to a specified path.
* Works great on HTML5 and desktop. * Works great on HTML5 and desktop.
* *
* @param path The file path to save to. * @param path The file path to save to.
*/ */
public static function exportSongMetadataAs(path:String, data:SongMetadata) 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. * Browse for a file to read and execute a callback once we have a file reference.
* Works great on HTML5 or desktop. * Works great on HTML5 or desktop.
* *
* @param callback The function to call when the file is loaded. * @param callback The function to call when the file is loaded.
*/ */
static function browseFileReference(callback:FileReference->Void) static function browseFileReference(callback:FileReference->Void)
{ {
var file = new FileReference(); var file = new FileReference();
file.addEventListener(Event.SELECT, function(e) file.addEventListener(Event.SELECT, function(e) {
{
var selectedFileRef:FileReference = e.target; var selectedFileRef:FileReference = e.target;
trace('Selected file: ' + selectedFileRef.name); trace('Selected file: ' + selectedFileRef.name);
selectedFileRef.addEventListener(Event.COMPLETE, function(e) selectedFileRef.addEventListener(Event.COMPLETE, function(e) {
{
var loadedFileRef:FileReference = e.target; var loadedFileRef:FileReference = e.target;
trace('Loaded file: ' + loadedFileRef.name); trace('Loaded file: ' + loadedFileRef.name);
callback(loadedFileRef); callback(loadedFileRef);
@ -192,16 +188,13 @@ class SongSerializer
static function writeFileReference(path:String, data:String) static function writeFileReference(path:String, data:String)
{ {
var file = new FileReference(); var file = new FileReference();
file.addEventListener(Event.COMPLETE, function(e:Event) file.addEventListener(Event.COMPLETE, function(e:Event) {
{
trace('Successfully wrote file.'); trace('Successfully wrote file.');
}); });
file.addEventListener(Event.CANCEL, function(e:Event) file.addEventListener(Event.CANCEL, function(e:Event) {
{
trace('Cancelled writing file.'); 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.'); trace('IO error writing file.');
}); });
file.save(data, path); file.save(data, path);

View file

@ -30,7 +30,7 @@ class SongValidator
/** /**
* Validates the fields of a SongMetadata object (excluding the version field). * Validates the fields of a SongMetadata object (excluding the version field).
* *
* @param input The SongMetadata object to validate. * @param input The SongMetadata object to validate.
* @param songId The ID of the song being validated. Only used for error messages. * @param songId The ID of the song being validated. Only used for error messages.
* @return The validated SongMetadata object. * @return The validated SongMetadata object.
@ -73,7 +73,7 @@ class SongValidator
/** /**
* Validates the fields of a SongPlayData object. * Validates the fields of a SongPlayData object.
* *
* @param input The SongPlayData object to validate. * @param input The SongPlayData object to validate.
* @param songId The ID of the song being validated. Only used for error messages. * @param songId The ID of the song being validated. Only used for error messages.
* @return The validated SongPlayData object. * @return The validated SongPlayData object.
@ -85,7 +85,7 @@ class SongValidator
/** /**
* Validates the fields of a TimeChange object. * Validates the fields of a TimeChange object.
* *
* @param input The TimeChange object to validate. * @param input The TimeChange object to validate.
* @param songId The ID of the song being validated. Only used for error messages. * @param songId The ID of the song being validated. Only used for error messages.
* @return The validated TimeChange object. * @return The validated TimeChange object.
@ -113,7 +113,7 @@ class SongValidator
/** /**
* Validates the fields of a SongChartData object (excluding the version field). * Validates the fields of a SongChartData object (excluding the version field).
* *
* @param input The SongChartData object to validate. * @param input The SongChartData object to validate.
* @param songId The ID of the song being validated. Only used for error messages. * @param songId The ID of the song being validated. Only used for error messages.
* @return The validated SongChartData object. * @return The validated SongChartData object.

View file

@ -20,7 +20,7 @@ import flixel.util.FlxColor;
import flixel.util.FlxSort; import flixel.util.FlxSort;
import flixel.util.FlxTimer; import flixel.util.FlxTimer;
import funkin.audio.visualize.PolygonSpectogram; import funkin.audio.visualize.PolygonSpectogram;
import funkin.audio.VocalGroup; import funkin.audio.VoicesGroup;
import funkin.input.Cursor; import funkin.input.Cursor;
import funkin.input.TurboKeyHandler; import funkin.input.TurboKeyHandler;
import funkin.modding.events.ScriptEvent; import funkin.modding.events.ScriptEvent;
@ -197,12 +197,12 @@ class ChartEditorState extends HaxeUIState
function get_scrollPositionInMs():Float function get_scrollPositionInMs():Float
{ {
return scrollPositionInSteps * Conductor.stepLengthMs; return scrollPositionInSteps * Conductor.stepCrochet;
} }
function set_scrollPositionInMs(value:Float):Float function set_scrollPositionInMs(value:Float):Float
{ {
scrollPositionInPixels = value / Conductor.stepLengthMs; scrollPositionInPixels = value / Conductor.stepCrochet;
return value; return value;
} }
@ -231,7 +231,7 @@ class ChartEditorState extends HaxeUIState
function get_playheadPositionInMs():Float function get_playheadPositionInMs():Float
{ {
return playheadPositionInSteps * Conductor.stepLengthMs; return playheadPositionInSteps * Conductor.stepCrochet;
} }
/** /**
@ -271,7 +271,7 @@ class ChartEditorState extends HaxeUIState
function get_songLengthInMs():Float function get_songLengthInMs():Float
{ {
return songLengthInSteps * Conductor.stepLengthMs; return songLengthInSteps * Conductor.stepCrochet;
} }
function set_songLengthInMs(value:Float):Float function set_songLengthInMs(value:Float):Float
@ -642,7 +642,7 @@ class ChartEditorState extends HaxeUIState
/** /**
* The audio track for the vocals. * The audio track for the vocals.
*/ */
var audioVocalTrackGroup:VocalGroup; var audioVocalTrackGroup:VoicesGroup;
/** /**
* The raw byte data for the vocal audio tracks. * The raw byte data for the vocal audio tracks.
@ -1053,7 +1053,7 @@ class ChartEditorState extends HaxeUIState
// Initialize the song chart data. // Initialize the song chart data.
songChartData = new Map<String, SongChartData>(); songChartData = new Map<String, SongChartData>();
audioVocalTrackGroup = new VocalGroup(); audioVocalTrackGroup = new VoicesGroup();
} }
/** /**
@ -1811,7 +1811,7 @@ class ChartEditorState extends HaxeUIState
// The song position of the cursor, in steps. // The song position of the cursor, in steps.
var cursorFractionalStep:Float = cursorY / GRID_SIZE / (16 / noteSnapQuant); var cursorFractionalStep:Float = cursorY / GRID_SIZE / (16 / noteSnapQuant);
var cursorStep:Int = Std.int(Math.floor(cursorFractionalStep)); 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. // The direction value for the column at the cursor.
var cursorColumn:Int = Math.floor(cursorX / GRID_SIZE); var cursorColumn:Int = Math.floor(cursorX / GRID_SIZE);
if (cursorColumn < 0) cursorColumn = 0; if (cursorColumn < 0) cursorColumn = 0;
@ -1849,7 +1849,7 @@ class ChartEditorState extends HaxeUIState
// We released the mouse. Select the notes in the box. // We released the mouse. Select the notes in the box.
var cursorFractionalStepStart:Float = cursorYStart / GRID_SIZE; var cursorFractionalStepStart:Float = cursorYStart / GRID_SIZE;
var cursorStepStart:Int = Math.floor(cursorFractionalStepStart); 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 cursorColumnBase:Int = Math.floor(cursorX / GRID_SIZE);
var cursorColumnBaseStart:Int = Math.floor(cursorXStart / 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. // Handle extending the note as you drag.
// Since use Math.floor and stepLengthMs here, the hold notes will be beat snapped. // Since use Math.floor and stepCrochet here, the hold notes will be beat snapped.
var dragLengthSteps:Float = Math.floor((cursorMs - currentPlaceNoteData.time) / Conductor.stepLengthMs); 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. // Without this, the newly placed note feels too short compared to the user's input.
var INCREMENT:Float = 1.0; 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? // TODO: Add and update some sort of preview?
@ -2363,7 +2363,7 @@ class ChartEditorState extends HaxeUIState
} }
// Get the position the note should be at. // 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. // Make sure the note appears when scrolling up.
var modifiedViewAreaTop:Float = viewAreaTop - GRID_SIZE; 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. // If the note is a hold, we need to make sure it's long enough.
var noteLengthMs:Float = noteSprite.noteData.length; var noteLengthMs:Float = noteSprite.noteData.length;
var noteLengthSteps:Float = (noteLengthMs / Conductor.stepLengthMs); var noteLengthSteps:Float = (noteLengthMs / Conductor.stepCrochet);
var lastNoteSprite:ChartEditorNoteSprite = noteSprite; var lastNoteSprite:ChartEditorNoteSprite = noteSprite;
while (noteLengthSteps > 0) while (noteLengthSteps > 0)
@ -2413,7 +2413,7 @@ class ChartEditorState extends HaxeUIState
// Make sure the last note sprite shows the end cap properly. // Make sure the last note sprite shows the end cap properly.
lastNoteSprite.childNoteSprite = null; 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)); // 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. // 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. // Make sure the event appears when scrolling up.
var modifiedViewAreaTop:Float = viewAreaTop - GRID_SIZE; var modifiedViewAreaTop:Float = viewAreaTop - GRID_SIZE;
@ -3115,7 +3115,7 @@ class ChartEditorState extends HaxeUIState
var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels; var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels;
var playheadPosFractionalStep:Float = playheadPos / GRID_SIZE / (16 / noteSnapQuant); var playheadPosFractionalStep:Float = playheadPos / GRID_SIZE / (16 / noteSnapQuant);
var playheadPosStep:Int = Std.int(Math.floor(playheadPosFractionalStep)); 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); var newNoteData:SongNoteData = new SongNoteData(playheadPosMs, column, 0, selectedNoteKind);
performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL)); performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL));
@ -3363,10 +3363,10 @@ class ChartEditorState extends HaxeUIState
audioVocalTrackGroup.clear(); audioVocalTrackGroup.clear();
} }
// Add player vocals. // 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')))); '-$currentSongCharacterPlayer'))));
// Add opponent vocals. // 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')))); '-$currentSongCharacterOpponent'))));
postLoadInstrumental(); postLoadInstrumental();