mirror of
https://github.com/FunkinCrew/Funkin.git
synced 2024-11-14 19:25:16 -05:00
"Add Variation" button, SongMetadata format changes, bug fixes, resolve metadata loading issues.
This commit is contained in:
parent
62c62cb53e
commit
42bb50882d
36 changed files with 2343 additions and 1562 deletions
2
assets
2
assets
|
@ -1 +1 @@
|
|||
Subproject commit a62e7e50d59c14d256c75da651b79dea77e1620e
|
||||
Subproject commit 7e698e3dd51443f0bb8aa4105596f8e87eca4a9b
|
2
hmm.json
2
hmm.json
|
@ -98,7 +98,7 @@
|
|||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "f4df19cfa196f85eece55c3367021fc965f1fa9a",
|
||||
"url": "https://github.com/elnabo/json2object"
|
||||
"url": "https://github.com/EliteMasterEric/json2object"
|
||||
},
|
||||
{
|
||||
"name": "lime",
|
||||
|
|
|
@ -6,9 +6,6 @@ import openfl.utils.Assets as OpenFlAssets;
|
|||
|
||||
class Paths
|
||||
{
|
||||
public static var SOUND_EXT = #if web "mp3" #else "ogg" #end;
|
||||
public static var VIDEO_EXT = "mp4";
|
||||
|
||||
static var currentLevel:String;
|
||||
|
||||
static public function setCurrentLevel(name:String)
|
||||
|
@ -84,7 +81,7 @@ class Paths
|
|||
|
||||
static public function sound(key:String, ?library:String)
|
||||
{
|
||||
return getPath('sounds/$key.$SOUND_EXT', SOUND, library);
|
||||
return getPath('sounds/$key.${Constants.EXT_SOUND}', SOUND, library);
|
||||
}
|
||||
|
||||
inline static public function soundRandom(key:String, min:Int, max:Int, ?library:String)
|
||||
|
@ -94,24 +91,24 @@ class Paths
|
|||
|
||||
inline static public function music(key:String, ?library:String)
|
||||
{
|
||||
return getPath('music/$key.$SOUND_EXT', MUSIC, library);
|
||||
return getPath('music/$key.${Constants.EXT_SOUND}', MUSIC, library);
|
||||
}
|
||||
|
||||
inline static public function videos(key:String, ?library:String)
|
||||
{
|
||||
return getPath('videos/$key.$VIDEO_EXT', BINARY, library);
|
||||
return getPath('videos/$key.${Constants.EXT_VIDEO}', BINARY, library);
|
||||
}
|
||||
|
||||
inline static public function voices(song:String, ?suffix:String = '')
|
||||
{
|
||||
if (suffix == null) suffix = ""; // no suffix, for a sorta backwards compatibility with older-ish voice files
|
||||
|
||||
return 'songs:assets/songs/${song.toLowerCase()}/Voices$suffix.$SOUND_EXT';
|
||||
return 'songs:assets/songs/${song.toLowerCase()}/Voices$suffix.${Constants.EXT_SOUND}';
|
||||
}
|
||||
|
||||
inline static public function inst(song:String, ?suffix:String = '')
|
||||
{
|
||||
return 'songs:assets/songs/${song.toLowerCase()}/Inst$suffix.$SOUND_EXT';
|
||||
return 'songs:assets/songs/${song.toLowerCase()}/Inst$suffix.${Constants.EXT_SOUND}';
|
||||
}
|
||||
|
||||
inline static public function image(key:String, ?library:String)
|
||||
|
|
|
@ -4,9 +4,6 @@ import openfl.Assets;
|
|||
import funkin.util.assets.DataAssets;
|
||||
import funkin.util.VersionUtil;
|
||||
import haxe.Constraints.Constructible;
|
||||
import json2object.Position;
|
||||
import json2object.Position.Line;
|
||||
import json2object.Error;
|
||||
|
||||
/**
|
||||
* The entry's constructor function must take a single argument, the entry's ID.
|
||||
|
@ -179,6 +176,15 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
|
|||
*/
|
||||
public abstract function parseEntryData(id:String):Null<J>;
|
||||
|
||||
/**
|
||||
* Parse and validate the JSON data and produce the corresponding data object.
|
||||
*
|
||||
* NOTE: Must be implemented on the implementation class.
|
||||
* @param contents The JSON as a string.
|
||||
* @param fileName An optional file name for error reporting.
|
||||
*/
|
||||
public abstract function parseEntryDataRaw(contents:String, ?fileName:String):Null<J>;
|
||||
|
||||
/**
|
||||
* Read, parse, and validate the JSON data and produce the corresponding data object,
|
||||
* accounting for old versions of the data.
|
||||
|
@ -226,79 +232,12 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
|
|||
*/
|
||||
abstract function createScriptedEntry(clsName:String):Null<T>;
|
||||
|
||||
function printErrors(errors:Array<Error>, id:String = ''):Void
|
||||
function printErrors(errors:Array<json2object.Error>, id:String = ''):Void
|
||||
{
|
||||
trace('[${registryId}] Failed to parse entry data: ${id}');
|
||||
|
||||
for (error in errors)
|
||||
printError(error);
|
||||
}
|
||||
|
||||
function printError(error:Error):Void
|
||||
{
|
||||
switch (error)
|
||||
{
|
||||
case IncorrectType(vari, expected, pos):
|
||||
trace(' Expected field "$vari" to be of type "$expected".');
|
||||
printPos(pos);
|
||||
case IncorrectEnumValue(value, expected, pos):
|
||||
trace(' Invalid enum value (expected "$expected", got "$value")');
|
||||
printPos(pos);
|
||||
case InvalidEnumConstructor(value, expected, pos):
|
||||
trace(' Invalid enum constructor (epxected "$expected", got "$value")');
|
||||
printPos(pos);
|
||||
case UninitializedVariable(vari, pos):
|
||||
trace(' Uninitialized variable "$vari"');
|
||||
printPos(pos);
|
||||
case UnknownVariable(vari, pos):
|
||||
trace(' Unknown variable "$vari"');
|
||||
printPos(pos);
|
||||
case ParserError(message, pos):
|
||||
trace(' Parsing error: ${message}');
|
||||
printPos(pos);
|
||||
case CustomFunctionException(e, pos):
|
||||
if (Std.isOfType(e, String))
|
||||
{
|
||||
trace(' ${e}');
|
||||
}
|
||||
else
|
||||
{
|
||||
printUnknownError(e);
|
||||
}
|
||||
printPos(pos);
|
||||
default:
|
||||
printUnknownError(error);
|
||||
}
|
||||
}
|
||||
|
||||
function printUnknownError(e:Dynamic):Void
|
||||
{
|
||||
switch (Type.typeof(e))
|
||||
{
|
||||
case TClass(c):
|
||||
trace(' [${Type.getClassName(c)}] ${e.toString()}');
|
||||
case TEnum(c):
|
||||
trace(' [${Type.getEnumName(c)}] ${e.toString()}');
|
||||
default:
|
||||
trace(' [${Type.typeof(e)}] ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Figure out the nicest way to print this.
|
||||
* Maybe look up how other JSON parsers format their errors?
|
||||
* @see https://github.com/elnabo/json2object/blob/master/src/json2object/Position.hx
|
||||
*/
|
||||
function printPos(pos:Position):Void
|
||||
{
|
||||
if (pos.lines[0].number == pos.lines[pos.lines.length - 1].number)
|
||||
{
|
||||
trace(' at ${(pos.file == '') ? 'line ' : '${pos.file}:'}${pos.lines[0].number}');
|
||||
}
|
||||
else
|
||||
{
|
||||
trace(' at ${(pos.file == '') ? 'line ' : '${pos.file}:'}${pos.lines[0].number}-${pos.lines[pos.lines.length - 1].number}');
|
||||
}
|
||||
DataError.printError(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
75
source/funkin/data/DataError.hx
Normal file
75
source/funkin/data/DataError.hx
Normal file
|
@ -0,0 +1,75 @@
|
|||
package funkin.data;
|
||||
|
||||
import json2object.Position;
|
||||
import json2object.Position.Line;
|
||||
import json2object.Error;
|
||||
|
||||
class DataError
|
||||
{
|
||||
public static function printError(error:Error):Void
|
||||
{
|
||||
switch (error)
|
||||
{
|
||||
case IncorrectType(vari, expected, pos):
|
||||
trace(' Expected field "$vari" to be of type "$expected".');
|
||||
printPos(pos);
|
||||
case IncorrectEnumValue(value, expected, pos):
|
||||
trace(' Invalid enum value (expected "$expected", got "$value")');
|
||||
printPos(pos);
|
||||
case InvalidEnumConstructor(value, expected, pos):
|
||||
trace(' Invalid enum constructor (epxected "$expected", got "$value")');
|
||||
printPos(pos);
|
||||
case UninitializedVariable(vari, pos):
|
||||
trace(' Uninitialized variable "$vari"');
|
||||
printPos(pos);
|
||||
case UnknownVariable(vari, pos):
|
||||
trace(' Unknown variable "$vari"');
|
||||
printPos(pos);
|
||||
case ParserError(message, pos):
|
||||
trace(' Parsing error: ${message}');
|
||||
printPos(pos);
|
||||
case CustomFunctionException(e, pos):
|
||||
if (Std.isOfType(e, String))
|
||||
{
|
||||
trace(' ${e}');
|
||||
}
|
||||
else
|
||||
{
|
||||
printUnknownError(e);
|
||||
}
|
||||
printPos(pos);
|
||||
default:
|
||||
printUnknownError(error);
|
||||
}
|
||||
}
|
||||
|
||||
public static function printUnknownError(e:Dynamic):Void
|
||||
{
|
||||
switch (Type.typeof(e))
|
||||
{
|
||||
case TClass(c):
|
||||
trace(' [${Type.getClassName(c)}] ${e.toString()}');
|
||||
case TEnum(c):
|
||||
trace(' [${Type.getEnumName(c)}] ${e.toString()}');
|
||||
default:
|
||||
trace(' [${Type.typeof(e)}] ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Figure out the nicest way to print this.
|
||||
* Maybe look up how other JSON parsers format their errors?
|
||||
* @see https://github.com/elnabo/json2object/blob/master/src/json2object/Position.hx
|
||||
*/
|
||||
static function printPos(pos:Position):Void
|
||||
{
|
||||
if (pos.lines[0].number == pos.lines[pos.lines.length - 1].number)
|
||||
{
|
||||
trace(' at ${(pos.file == '') ? 'line ' : '${pos.file}:'}${pos.lines[0].number}');
|
||||
}
|
||||
else
|
||||
{
|
||||
trace(' at ${(pos.file == '') ? 'line ' : '${pos.file}:'}${pos.lines[0].number}-${pos.lines[pos.lines.length - 1].number}');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,13 @@
|
|||
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.LegacyScrollSpeeds;
|
||||
|
||||
/**
|
||||
* `json2object` has an annotation `@:jcustomparse` which allows for mutation of parsed values.
|
||||
|
@ -39,36 +45,40 @@ class DataParse
|
|||
*/
|
||||
public static function dynamicValue(json:Json, name:String):Dynamic
|
||||
{
|
||||
return jsonToDynamic(json);
|
||||
return Tools.getValue(json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parser which outputs a Dynamic value, which must be an object with properties.
|
||||
* @param json
|
||||
* @param name
|
||||
* @return Dynamic
|
||||
* Parser which outputs a `Either<Array<LegacyNoteSection>, LegacyNoteData>`.
|
||||
* Used by the FNF legacy JSON importer.
|
||||
*/
|
||||
public static function dynamicObject(json:Json, name:String):Dynamic
|
||||
public static function eitherLegacyNoteData(json:Json, name:String):Either<Array<LegacyNoteSection>, LegacyNoteData>
|
||||
{
|
||||
switch (json.value)
|
||||
{
|
||||
case JArray(values):
|
||||
return Either.Left(legacyNoteSectionArray(json, name));
|
||||
case JObject(fields):
|
||||
return jsonFieldsToDynamicObject(fields);
|
||||
return Either.Right(cast Tools.getValue(json));
|
||||
default:
|
||||
throw 'Expected property $name to be an object, but it was ${json.value}.';
|
||||
throw 'Expected property $name to be note data, but it was ${json.value}.';
|
||||
}
|
||||
}
|
||||
|
||||
static function jsonToDynamic(json:Json):Null<Dynamic>
|
||||
/**
|
||||
* Parser which outputs a `Either<Float, LegacyScrollSpeeds>`.
|
||||
* Used by the FNF legacy JSON importer.
|
||||
*/
|
||||
public static function eitherLegacyScrollSpeeds(json:Json, name:String):Either<Float, LegacyScrollSpeeds>
|
||||
{
|
||||
return switch (json.value)
|
||||
switch (json.value)
|
||||
{
|
||||
case JString(s): s;
|
||||
case JNumber(n): Std.parseInt(n);
|
||||
case JBool(b): b;
|
||||
case JNull: null;
|
||||
case JObject(fields): jsonFieldsToDynamicObject(fields);
|
||||
case JArray(values): jsonArrayToDynamicArray(values);
|
||||
case JNumber(f):
|
||||
return Either.Left(Std.parseFloat(f));
|
||||
case JObject(fields):
|
||||
return Either.Right(cast Tools.getValue(json));
|
||||
default:
|
||||
throw 'Expected property $name to be scroll speeds, but it was ${json.value}.';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -82,7 +92,7 @@ class DataParse
|
|||
var result:Dynamic = {};
|
||||
for (field in fields)
|
||||
{
|
||||
Reflect.setField(result, field.name, jsonToDynamic(field.value));
|
||||
Reflect.setField(result, field.name, Tools.getValue(field.value));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
@ -94,6 +104,67 @@ class DataParse
|
|||
*/
|
||||
static function jsonArrayToDynamicArray(jsons:Array<Json>):Array<Null<Dynamic>>
|
||||
{
|
||||
return [for (json in jsons) jsonToDynamic(json)];
|
||||
return [for (json in jsons) Tools.getValue(json)];
|
||||
}
|
||||
|
||||
static function legacyNoteSectionArray(json:Json, name:String):Array<LegacyNoteSection>
|
||||
{
|
||||
switch (json.value)
|
||||
{
|
||||
case JArray(values):
|
||||
return [for (value in values) legacyNoteSection(value, name)];
|
||||
default:
|
||||
throw 'Expected property to be an array, but it was ${json.value}.';
|
||||
}
|
||||
}
|
||||
|
||||
static function legacyNoteSection(json:Json, name:String):LegacyNoteSection
|
||||
{
|
||||
switch (json.value)
|
||||
{
|
||||
case JObject(fields):
|
||||
return cast Tools.getValue(json);
|
||||
default:
|
||||
throw 'Expected property $name to be an object, but it was ${json.value}.';
|
||||
}
|
||||
}
|
||||
|
||||
public static function legacyNoteData(json:Json, name:String):LegacyNoteData
|
||||
{
|
||||
switch (json.value)
|
||||
{
|
||||
case JObject(fields):
|
||||
return cast Tools.getValue(json);
|
||||
default:
|
||||
throw 'Expected property $name to be an object, but it was ${json.value}.';
|
||||
}
|
||||
}
|
||||
|
||||
public static function legacyNotes(json:Json, name:String):Array<LegacyNote>
|
||||
{
|
||||
switch (json.value)
|
||||
{
|
||||
case JArray(values):
|
||||
return [for (value in values) legacyNote(value, name)];
|
||||
default:
|
||||
throw 'Expected property $name to be an array of notes, but it was ${json.value}.';
|
||||
}
|
||||
}
|
||||
|
||||
public static function legacyNote(json:Json, name:String):LegacyNote
|
||||
{
|
||||
switch (json.value)
|
||||
{
|
||||
case JArray(values):
|
||||
// var time:Null<Float> = values[0] == null ? null : Tools.getValue(values[0]);
|
||||
// var data:Null<Int> = values[1] == null ? null : Tools.getValue(values[1]);
|
||||
// var length:Null<Float> = values[2] == null ? null : Tools.getValue(values[2]);
|
||||
// var alt:Null<Bool> = values[3] == null ? null : Tools.getValue(values[3]);
|
||||
|
||||
// return new LegacyNote(time, data, length, alt);
|
||||
return null;
|
||||
default:
|
||||
throw 'Expected property $name to be a note, but it was ${json.value}.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,17 @@
|
|||
package funkin.data;
|
||||
|
||||
import funkin.util.SerializerUtil;
|
||||
|
||||
/**
|
||||
* `json2object` has an annotation `@:jcustomwrite` which allows for custom serialization of values to be written to JSON.
|
||||
*
|
||||
* Functions must be of the signature `(T) -> String`, where `T` is the type of the property.
|
||||
*/
|
||||
class DataWrite {}
|
||||
class DataWrite
|
||||
{
|
||||
public static function dynamicValue(value:Dynamic):String
|
||||
{
|
||||
// Is this cheating? Yes. Do I care? No.
|
||||
return SerializerUtil.toJSON(value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,7 +67,6 @@ typedef UnnamedAnimationData =
|
|||
* ONLY for use by MultiSparrow characters.
|
||||
* @default The assetPath of the parent sprite
|
||||
*/
|
||||
@:default(null)
|
||||
@:optional
|
||||
var assetPath:Null<String>;
|
||||
|
||||
|
@ -85,7 +84,7 @@ typedef UnnamedAnimationData =
|
|||
*/
|
||||
@:default(false)
|
||||
@:optional
|
||||
var looped:Null<Bool>;
|
||||
var looped:Bool;
|
||||
|
||||
/**
|
||||
* Whether the animation's sprites should be flipped horizontally.
|
||||
|
|
|
@ -47,6 +47,26 @@ class LevelRegistry extends BaseRegistry<Level, LevelData>
|
|||
return parser.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and validate the JSON data and produce the corresponding data object.
|
||||
*
|
||||
* NOTE: Must be implemented on the implementation class.
|
||||
* @param contents The JSON as a string.
|
||||
* @param fileName An optional file name for error reporting.
|
||||
*/
|
||||
public function parseEntryDataRaw(contents:String, ?fileName:String):Null<LevelData>
|
||||
{
|
||||
var parser = new json2object.JsonParser<LevelData>();
|
||||
parser.fromJson(contents, fileName);
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
{
|
||||
printErrors(parser.errors, fileName);
|
||||
return null;
|
||||
}
|
||||
return parser.value;
|
||||
}
|
||||
|
||||
function createScriptedEntry(clsName:String):Level
|
||||
{
|
||||
return ScriptedLevel.init(clsName, "unknown");
|
||||
|
|
|
@ -54,6 +54,26 @@ class NoteStyleRegistry extends BaseRegistry<NoteStyle, NoteStyleData>
|
|||
return parser.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and validate the JSON data and produce the corresponding data object.
|
||||
*
|
||||
* NOTE: Must be implemented on the implementation class.
|
||||
* @param contents The JSON as a string.
|
||||
* @param fileName An optional file name for error reporting.
|
||||
*/
|
||||
public function parseEntryDataRaw(contents:String, ?fileName:String):Null<NoteStyleData>
|
||||
{
|
||||
var parser = new json2object.JsonParser<NoteStyleData>();
|
||||
parser.fromJson(contents, fileName);
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
{
|
||||
printErrors(parser.errors, fileName);
|
||||
return null;
|
||||
}
|
||||
return parser.value;
|
||||
}
|
||||
|
||||
function createScriptedEntry(clsName:String):NoteStyle
|
||||
{
|
||||
return ScriptedNoteStyle.init(clsName, "unknown");
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
package funkin.data.song;
|
||||
|
||||
import flixel.util.typeLimit.OneOfTwo;
|
||||
import funkin.play.song.SongMigrator;
|
||||
import funkin.play.song.SongValidator;
|
||||
import funkin.data.song.SongRegistry;
|
||||
import thx.semver.Version;
|
||||
|
||||
|
@ -47,32 +45,33 @@ class SongMetadata
|
|||
* Defaults to `default` or `''`. Populated later.
|
||||
*/
|
||||
@:jignored
|
||||
public var variation:String = 'default';
|
||||
public var variation:String;
|
||||
|
||||
public function new(songName:String, artist:String, variation:String = 'default')
|
||||
{
|
||||
this.version = SongMigrator.CHART_VERSION;
|
||||
this.version = SongRegistry.SONG_METADATA_VERSION;
|
||||
this.songName = songName;
|
||||
this.artist = artist;
|
||||
this.timeFormat = 'ms';
|
||||
this.divisions = null;
|
||||
this.timeChanges = [new SongTimeChange(0, 100)];
|
||||
this.looped = false;
|
||||
this.playData =
|
||||
{
|
||||
songVariations: [],
|
||||
difficulties: ['normal'],
|
||||
|
||||
playableChars: ['bf' => new SongPlayableChar('gf', 'dad')],
|
||||
|
||||
stage: 'mainStage',
|
||||
noteSkin: 'Normal'
|
||||
};
|
||||
this.playData = new SongPlayData();
|
||||
this.playData.songVariations = [];
|
||||
this.playData.difficulties = [];
|
||||
this.playData.characters = new SongCharacterData('bf', 'gf', 'dad');
|
||||
this.playData.stage = 'mainStage';
|
||||
this.playData.noteSkin = 'funkin';
|
||||
this.generatedBy = SongRegistry.DEFAULT_GENERATEDBY;
|
||||
// Variation ID.
|
||||
this.variation = variation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a copy of this SongMetadata with the same information.
|
||||
* @param newVariation Set to a new variation ID to change the new metadata.
|
||||
* @return The cloned SongMetadata
|
||||
*/
|
||||
public function clone(?newVariation:String = null):SongMetadata
|
||||
{
|
||||
var result:SongMetadata = new SongMetadata(this.songName, this.artist, newVariation == null ? this.variation : newVariation);
|
||||
|
@ -87,6 +86,21 @@ class SongMetadata
|
|||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize this SongMetadata into a JSON string.
|
||||
* @return The JSON string.
|
||||
*/
|
||||
public function serialize(?pretty:Bool = true):String
|
||||
{
|
||||
var writer = new json2object.JsonWriter<SongMetadata>();
|
||||
var output = this.clone();
|
||||
output.variation = null; // Not sure how to make a field optional on the reader and ignored on the writer.
|
||||
return writer.write(output, pretty ? ' ' : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a string representation suitable for debugging.
|
||||
*/
|
||||
public function toString():String
|
||||
{
|
||||
return 'SongMetadata(${this.songName} by ${this.artist}, variation ${this.variation})';
|
||||
|
@ -121,7 +135,6 @@ class SongTimeChange
|
|||
*/
|
||||
@:optional
|
||||
@:alias("b")
|
||||
// @:default(funkin.data.song.SongData.SongTimeChange.DEFAULT_BEAT_TIME)
|
||||
public var beatTime:Null<Float>;
|
||||
|
||||
/**
|
||||
|
@ -168,6 +181,9 @@ class SongTimeChange
|
|||
this.beatTuplets = beatTuplets == null ? DEFAULT_BEAT_TUPLETS : beatTuplets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a string representation suitable for debugging.
|
||||
*/
|
||||
public function toString():String
|
||||
{
|
||||
return 'SongTimeChange(${this.timeStamp}ms,${this.bpm}bpm)';
|
||||
|
@ -199,7 +215,7 @@ class SongMusicData
|
|||
|
||||
@:optional
|
||||
@:default(false)
|
||||
public var looped:Bool;
|
||||
public var looped:Null<Bool>;
|
||||
|
||||
// @:default(funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY)
|
||||
public var generatedBy:String;
|
||||
|
@ -218,7 +234,7 @@ class SongMusicData
|
|||
|
||||
public function new(songName:String, artist:String, variation:String = 'default')
|
||||
{
|
||||
this.version = SongMigrator.CHART_VERSION;
|
||||
this.version = SongRegistry.SONG_CHART_DATA_VERSION;
|
||||
this.songName = songName;
|
||||
this.artist = artist;
|
||||
this.timeFormat = 'ms';
|
||||
|
@ -243,53 +259,106 @@ class SongMusicData
|
|||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a string representation suitable for debugging.
|
||||
*/
|
||||
public function toString():String
|
||||
{
|
||||
return 'SongMusicData(${this.songName} by ${this.artist}, variation ${this.variation})';
|
||||
}
|
||||
}
|
||||
|
||||
typedef SongPlayData =
|
||||
class SongPlayData
|
||||
{
|
||||
/**
|
||||
* The variations this song has. The associated metadata files should exist.
|
||||
*/
|
||||
public var songVariations:Array<String>;
|
||||
|
||||
/**
|
||||
* The difficulties contained in this song's chart file.
|
||||
*/
|
||||
public var difficulties:Array<String>;
|
||||
|
||||
/**
|
||||
* Keys are the player characters and the values give info on what opponent/GF/inst to use.
|
||||
* The characters used by this song.
|
||||
*/
|
||||
public var playableChars:Map<String, SongPlayableChar>;
|
||||
public var characters:SongCharacterData;
|
||||
|
||||
/**
|
||||
* The stage used by this song.
|
||||
*/
|
||||
public var stage:String;
|
||||
|
||||
/**
|
||||
* The note style used by this song.
|
||||
* TODO: Rename to `noteStyle`? Renaming values is a breaking change to the metadata format.
|
||||
*/
|
||||
public var noteSkin:String;
|
||||
|
||||
/**
|
||||
* The difficulty rating for this song as displayed in Freeplay.
|
||||
* TODO: Adding this is a non-breaking change to the metadata format.
|
||||
*/
|
||||
// public var rating:Int;
|
||||
|
||||
/**
|
||||
* The album ID for the album to display in Freeplay.
|
||||
* TODO: Adding this is a non-breaking change to the metadata format.
|
||||
*/
|
||||
// public var album:String;
|
||||
|
||||
public function new() {}
|
||||
|
||||
/**
|
||||
* Produces a string representation suitable for debugging.
|
||||
*/
|
||||
public function toString():String
|
||||
{
|
||||
return 'SongPlayData(${this.songVariations}, ${this.difficulties})';
|
||||
}
|
||||
}
|
||||
|
||||
class SongPlayableChar
|
||||
/**
|
||||
* Information about the characters used in this variation of the song.
|
||||
* Create a new variation if you want to change the characters.
|
||||
*/
|
||||
class SongCharacterData
|
||||
{
|
||||
@:alias('g')
|
||||
@:optional
|
||||
@:default('')
|
||||
public var player:String = '';
|
||||
|
||||
@:optional
|
||||
@:default('')
|
||||
public var girlfriend:String = '';
|
||||
|
||||
@:alias('o')
|
||||
@:optional
|
||||
@:default('')
|
||||
public var opponent:String = '';
|
||||
|
||||
@:alias('i')
|
||||
@:optional
|
||||
@:default('')
|
||||
public var inst:String = '';
|
||||
public var instrumental:String = '';
|
||||
|
||||
public function new(girlfriend:String = '', opponent:String = '', inst:String = '')
|
||||
@:optional
|
||||
@:default([])
|
||||
public var altInstrumentals:Array<String> = [];
|
||||
|
||||
public function new(player:String = '', girlfriend:String = '', opponent:String = '', instrumental:String = '')
|
||||
{
|
||||
this.player = player;
|
||||
this.girlfriend = girlfriend;
|
||||
this.opponent = opponent;
|
||||
this.inst = inst;
|
||||
this.instrumental = instrumental;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a string representation suitable for debugging.
|
||||
*/
|
||||
public function toString():String
|
||||
{
|
||||
return 'SongPlayableChar(${this.girlfriend}, ${this.opponent}, ${this.inst})';
|
||||
return 'SongCharacterData(${this.player}, ${this.girlfriend}, ${this.opponent}, ${this.instrumental}, [${this.altInstrumentals.join(', ')}])';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -346,14 +415,21 @@ class SongChartData
|
|||
return value;
|
||||
}
|
||||
|
||||
public function getEvents():Array<SongEventData>
|
||||
/**
|
||||
* Convert this SongChartData into a JSON string.
|
||||
*/
|
||||
public function serialize(?pretty:Bool = true):String
|
||||
{
|
||||
return this.events;
|
||||
var writer = new json2object.JsonWriter<SongChartData>();
|
||||
return writer.write(this, pretty ? ' ' : null);
|
||||
}
|
||||
|
||||
public function setEvents(value:Array<SongEventData>):Array<SongEventData>
|
||||
/**
|
||||
* Produces a string representation suitable for debugging.
|
||||
*/
|
||||
public function toString():String
|
||||
{
|
||||
return this.events = value;
|
||||
return 'SongChartData(${this.events.length} events, ${this.notes.size()} difficulties, ${generatedBy})';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -387,6 +463,7 @@ class SongEventData
|
|||
@:alias("v")
|
||||
@:optional
|
||||
@:jcustomparse(funkin.data.DataParse.dynamicValue)
|
||||
@:jcustomwrite(funkin.data.DataWrite.dynamicValue)
|
||||
public var value:Dynamic = null;
|
||||
|
||||
/**
|
||||
|
@ -484,6 +561,9 @@ class SongEventData
|
|||
return this.time <= other.time;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a string representation suitable for debugging.
|
||||
*/
|
||||
public function toString():String
|
||||
{
|
||||
return 'SongEventData(${this.time}ms, ${this.event}: ${this.value})';
|
||||
|
@ -703,6 +783,9 @@ class SongNoteData
|
|||
return this.time <= other.time;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a string representation suitable for debugging.
|
||||
*/
|
||||
public function toString():String
|
||||
{
|
||||
return 'SongNoteData(${this.time}ms, ' + (this.length > 0 ? '[${this.length}ms hold]' : '') + ' ${this.data}'
|
||||
|
|
|
@ -8,6 +8,9 @@ import funkin.util.SerializerUtil;
|
|||
|
||||
using Lambda;
|
||||
|
||||
/**
|
||||
* Utility functions for working with song data, including note data, event data, metadata, etc.
|
||||
*/
|
||||
class SongDataUtils
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package funkin.data.song;
|
||||
|
||||
import funkin.data.song.SongData;
|
||||
import funkin.data.song.migrator.SongData_v2_0_0.SongMetadata_v2_0_0;
|
||||
import funkin.data.song.SongData.SongChartData;
|
||||
import funkin.data.song.SongData.SongMetadata;
|
||||
import funkin.play.song.ScriptedSong;
|
||||
|
@ -8,6 +9,8 @@ import funkin.play.song.Song;
|
|||
import funkin.util.assets.DataAssets;
|
||||
import funkin.util.VersionUtil;
|
||||
|
||||
using funkin.data.song.migrator.SongDataMigrator;
|
||||
|
||||
class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
||||
{
|
||||
/**
|
||||
|
@ -15,14 +18,18 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
* Handle breaking changes by incrementing this value
|
||||
* and adding migration to the `migrateStageData()` function.
|
||||
*/
|
||||
public static final SONG_METADATA_VERSION:thx.semver.Version = "2.0.0";
|
||||
public static final SONG_METADATA_VERSION:thx.semver.Version = "2.1.0";
|
||||
|
||||
public static final SONG_METADATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x";
|
||||
public static final SONG_METADATA_VERSION_RULE:thx.semver.VersionRule = "2.1.x";
|
||||
|
||||
public static final SONG_CHART_DATA_VERSION:thx.semver.Version = "2.0.0";
|
||||
|
||||
public static final SONG_CHART_DATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x";
|
||||
|
||||
public static final SONG_MUSIC_DATA_VERSION:thx.semver.Version = "2.0.0";
|
||||
|
||||
public static final SONG_MUSIC_DATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x";
|
||||
|
||||
public static var DEFAULT_GENERATEDBY(get, null):String;
|
||||
|
||||
static function get_DEFAULT_GENERATEDBY():String
|
||||
|
@ -30,6 +37,10 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
return '${Constants.TITLE} - ${Constants.VERSION}';
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: What if there was a Singleton macro which created static functions
|
||||
* that redirected to the instance?
|
||||
*/
|
||||
public static final instance:SongRegistry = new SongRegistry();
|
||||
|
||||
public function new()
|
||||
|
@ -101,13 +112,21 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
return parseEntryMetadata(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse, and validate the JSON data and produce the corresponding data object.
|
||||
*/
|
||||
public function parseEntryDataRaw(contents:String, ?fileName:String = 'raw'):Null<SongMetadata>
|
||||
{
|
||||
return parseEntryMetadataRaw(contents);
|
||||
}
|
||||
|
||||
public function parseEntryMetadata(id:String, variation:String = ""):Null<SongMetadata>
|
||||
{
|
||||
// JsonParser does not take type parameters,
|
||||
// otherwise this function would be in BaseRegistry.
|
||||
|
||||
var parser = new json2object.JsonParser<SongMetadata>();
|
||||
switch (loadEntryMetadataFile(id))
|
||||
switch (loadEntryMetadataFile(id, variation))
|
||||
{
|
||||
case {fileName: fileName, contents: contents}:
|
||||
parser.fromJson(contents, fileName);
|
||||
|
@ -123,6 +142,19 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
return parser.value;
|
||||
}
|
||||
|
||||
public function parseEntryMetadataRaw(contents:String, ?fileName:String = 'raw'):Null<SongMetadata>
|
||||
{
|
||||
var parser = new json2object.JsonParser<SongMetadata>();
|
||||
parser.fromJson(contents, fileName);
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
{
|
||||
printErrors(parser.errors, fileName);
|
||||
return null;
|
||||
}
|
||||
return parser.value;
|
||||
}
|
||||
|
||||
public function parseEntryMetadataWithMigration(id:String, variation:String = '', version:thx.semver.Version):Null<SongMetadata>
|
||||
{
|
||||
// If a version rule is not specified, do not check against it.
|
||||
|
@ -130,19 +162,73 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
{
|
||||
return parseEntryMetadata(id);
|
||||
}
|
||||
else if (VersionUtil.validateVersion(version, "2.0.x"))
|
||||
{
|
||||
return parseEntryMetadata_v2_0_0(id);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw '[${registryId}] Metadata entry ${id}:${variation == '' ? 'default' : variation} does not support migration to version ${SONG_METADATA_VERSION_RULE}.';
|
||||
}
|
||||
}
|
||||
|
||||
public function parseEntryMetadataRawWithMigration(contents:String, ?fileName:String = 'raw', version:thx.semver.Version):Null<SongMetadata>
|
||||
{
|
||||
// If a version rule is not specified, do not check against it.
|
||||
if (SONG_METADATA_VERSION_RULE == null || VersionUtil.validateVersion(version, SONG_METADATA_VERSION_RULE))
|
||||
{
|
||||
return parseEntryMetadataRaw(contents, fileName);
|
||||
}
|
||||
else if (VersionUtil.validateVersion(version, "2.0.x"))
|
||||
{
|
||||
return parseEntryMetadataRaw_v2_0_0(contents, fileName);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw '[${registryId}] Metadata entry "${fileName}" does not support migration to version ${SONG_METADATA_VERSION_RULE}.';
|
||||
}
|
||||
}
|
||||
|
||||
function parseEntryMetadata_v2_0_0(id:String, variation:String = ""):Null<SongMetadata>
|
||||
{
|
||||
// JsonParser does not take type parameters,
|
||||
// otherwise this function would be in BaseRegistry.
|
||||
var parser = new json2object.JsonParser<SongMetadata_v2_0_0>();
|
||||
switch (loadEntryMetadataFile(id))
|
||||
{
|
||||
case {fileName: fileName, contents: contents}:
|
||||
parser.fromJson(contents, fileName);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
if (parser.errors.length > 0)
|
||||
{
|
||||
printErrors(parser.errors, id);
|
||||
return null;
|
||||
}
|
||||
return parser.value.migrate();
|
||||
}
|
||||
|
||||
function parseEntryMetadataRaw_v2_0_0(contents:String, ?fileName:String = 'raw'):Null<SongMetadata>
|
||||
{
|
||||
var parser = new json2object.JsonParser<SongMetadata_v2_0_0>();
|
||||
parser.fromJson(contents, fileName);
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
{
|
||||
printErrors(parser.errors, fileName);
|
||||
return null;
|
||||
}
|
||||
return parser.value.migrate();
|
||||
}
|
||||
|
||||
public function parseMusicData(id:String, variation:String = ""):Null<SongMusicData>
|
||||
{
|
||||
// JsonParser does not take type parameters,
|
||||
// otherwise this function would be in BaseRegistry.
|
||||
|
||||
var parser = new json2object.JsonParser<SongMusicData>();
|
||||
switch (loadMusicDataFile(id))
|
||||
switch (loadMusicDataFile(id, variation))
|
||||
{
|
||||
case {fileName: fileName, contents: contents}:
|
||||
parser.fromJson(contents, fileName);
|
||||
|
@ -158,13 +244,52 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
return parser.value;
|
||||
}
|
||||
|
||||
public function parseMusicDataRaw(contents:String, ?fileName:String = 'raw'):Null<SongMusicData>
|
||||
{
|
||||
var parser = new json2object.JsonParser<SongMusicData>();
|
||||
parser.fromJson(contents, fileName);
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
{
|
||||
printErrors(parser.errors, fileName);
|
||||
return null;
|
||||
}
|
||||
return parser.value;
|
||||
}
|
||||
|
||||
public function parseMusicDataWithMigration(id:String, variation:String = '', version:thx.semver.Version):Null<SongMusicData>
|
||||
{
|
||||
// If a version rule is not specified, do not check against it.
|
||||
if (SONG_MUSIC_DATA_VERSION_RULE == null || VersionUtil.validateVersion(version, SONG_MUSIC_DATA_VERSION_RULE))
|
||||
{
|
||||
return parseMusicData(id, variation);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw '[${registryId}] Chart entry ${id}:${variation == '' ? 'default' : variation} does not support migration to version ${SONG_CHART_DATA_VERSION_RULE}.';
|
||||
}
|
||||
}
|
||||
|
||||
public function parseMusicDataRawWithMigration(contents:String, ?fileName:String = 'raw', version:thx.semver.Version):Null<SongMusicData>
|
||||
{
|
||||
// If a version rule is not specified, do not check against it.
|
||||
if (SONG_MUSIC_DATA_VERSION_RULE == null || VersionUtil.validateVersion(version, SONG_MUSIC_DATA_VERSION_RULE))
|
||||
{
|
||||
return parseMusicDataRaw(contents, fileName);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw '[${registryId}] Chart entry "$fileName" does not support migration to version ${SONG_CHART_DATA_VERSION_RULE}.';
|
||||
}
|
||||
}
|
||||
|
||||
public function parseEntryChartData(id:String, variation:String = ''):Null<SongChartData>
|
||||
{
|
||||
// JsonParser does not take type parameters,
|
||||
// otherwise this function would be in BaseRegistry.
|
||||
var parser = new json2object.JsonParser<SongChartData>();
|
||||
|
||||
switch (loadEntryChartFile(id))
|
||||
switch (loadEntryChartFile(id, variation))
|
||||
{
|
||||
case {fileName: fileName, contents: contents}:
|
||||
parser.fromJson(contents, fileName);
|
||||
|
@ -180,6 +305,19 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
return parser.value;
|
||||
}
|
||||
|
||||
public function parseEntryChartDataRaw(contents:String, ?fileName:String = 'raw'):Null<SongChartData>
|
||||
{
|
||||
var parser = new json2object.JsonParser<SongChartData>();
|
||||
parser.fromJson(contents, fileName);
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
{
|
||||
printErrors(parser.errors, fileName);
|
||||
return null;
|
||||
}
|
||||
return parser.value;
|
||||
}
|
||||
|
||||
public function parseEntryChartDataWithMigration(id:String, variation:String = '', version:thx.semver.Version):Null<SongChartData>
|
||||
{
|
||||
// If a version rule is not specified, do not check against it.
|
||||
|
@ -193,6 +331,19 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
}
|
||||
}
|
||||
|
||||
public function parseEntryChartDataRawWithMigration(contents:String, ?fileName:String = 'raw', version:thx.semver.Version):Null<SongChartData>
|
||||
{
|
||||
// If a version rule is not specified, do not check against it.
|
||||
if (SONG_CHART_DATA_VERSION_RULE == null || VersionUtil.validateVersion(version, SONG_CHART_DATA_VERSION_RULE))
|
||||
{
|
||||
return parseEntryChartDataRaw(contents, fileName);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw '[${registryId}] Chart entry "${fileName}" does not support migration to version ${SONG_CHART_DATA_VERSION_RULE}.';
|
||||
}
|
||||
}
|
||||
|
||||
function createScriptedEntry(clsName:String):Song
|
||||
{
|
||||
return ScriptedSong.init(clsName, "unknown");
|
||||
|
|
124
source/funkin/data/song/importer/FNFLegacyData.hx
Normal file
124
source/funkin/data/song/importer/FNFLegacyData.hx
Normal file
|
@ -0,0 +1,124 @@
|
|||
package funkin.data.song.importer;
|
||||
|
||||
import haxe.ds.Either;
|
||||
|
||||
/**
|
||||
* A data structure representing a song in the old chart format.
|
||||
* This only works for charts compatible with Week 7, so you'll need a custom program
|
||||
* to handle importing charts from mods or other engines.
|
||||
*/
|
||||
class FNFLegacyData
|
||||
{
|
||||
public var song:LegacySongData;
|
||||
}
|
||||
|
||||
class LegacySongData
|
||||
{
|
||||
public var player1:String; // Boyfriend
|
||||
public var player2:String; // Opponent
|
||||
|
||||
@:jcustomparse(funkin.data.DataParse.eitherLegacyScrollSpeeds)
|
||||
public var speed:Either<Float, LegacyScrollSpeeds>;
|
||||
public var stageDefault:String;
|
||||
public var bpm:Float;
|
||||
|
||||
@:jcustomparse(funkin.data.DataParse.eitherLegacyNoteData)
|
||||
public var notes:Either<Array<LegacyNoteSection>, LegacyNoteData>;
|
||||
public var song:String; // Song name
|
||||
|
||||
public function new() {}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
var notesStr:String = switch (notes)
|
||||
{
|
||||
case Left(sections): 'single difficulty w/ ${sections.length} sections';
|
||||
case Right(data):
|
||||
var difficultyCount:Int = 0;
|
||||
if (data.easy != null) difficultyCount++;
|
||||
if (data.normal != null) difficultyCount++;
|
||||
if (data.hard != null) difficultyCount++;
|
||||
'${difficultyCount} difficulties';
|
||||
};
|
||||
return 'LegacySongData($player1, $player2, $notesStr)';
|
||||
}
|
||||
}
|
||||
|
||||
typedef LegacyScrollSpeeds =
|
||||
{
|
||||
public var ?easy:Float;
|
||||
public var ?normal:Float;
|
||||
public var ?hard:Float;
|
||||
};
|
||||
|
||||
typedef LegacyNoteData =
|
||||
{
|
||||
/**
|
||||
* The easy difficulty.
|
||||
*/
|
||||
public var ?easy:Array<LegacyNoteSection>;
|
||||
|
||||
/**
|
||||
* The normal difficulty.
|
||||
*/
|
||||
public var ?normal:Array<LegacyNoteSection>;
|
||||
|
||||
/**
|
||||
* The hard difficulty.
|
||||
*/
|
||||
public var ?hard:Array<LegacyNoteSection>;
|
||||
};
|
||||
|
||||
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.
|
||||
*/
|
||||
public var mustHitSection:Bool;
|
||||
|
||||
/**
|
||||
* Array of note data:
|
||||
* - Direction
|
||||
* - Time (ms)
|
||||
* - Sustain Duration (ms)
|
||||
* - Note kind (true = "alt", or string)
|
||||
*/
|
||||
public var sectionNotes:Array<LegacyNote>;
|
||||
|
||||
public var ?typeOfSection:Int;
|
||||
|
||||
public var ?lengthInSteps:Int;
|
||||
|
||||
// BPM changes
|
||||
public var ?changeBPM:Bool;
|
||||
public var ?bpm:Float;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notes in the old format are stored as an Array<Dynamic>
|
||||
* We use a custom parser to manage this.
|
||||
*/
|
||||
@:jcustomparse(funkin.data.DataParse.legacyNote)
|
||||
class LegacyNote
|
||||
{
|
||||
public var time:Float;
|
||||
public var data:Int;
|
||||
public var length:Float;
|
||||
public var alt:Bool;
|
||||
|
||||
public function new(time:Float, data:Int, ?length:Float, ?alt:Bool)
|
||||
{
|
||||
this.time = time;
|
||||
this.data = data;
|
||||
|
||||
this.length = length ?? 0.0;
|
||||
this.alt = alt ?? false;
|
||||
}
|
||||
|
||||
public inline function getKind():String
|
||||
{
|
||||
return this.alt ? 'alt' : 'normal';
|
||||
}
|
||||
}
|
202
source/funkin/data/song/importer/FNFLegacyImporter.hx
Normal file
202
source/funkin/data/song/importer/FNFLegacyImporter.hx
Normal file
|
@ -0,0 +1,202 @@
|
|||
package funkin.data.song.importer; // import is a reserved word dumbass
|
||||
|
||||
import funkin.data.song.SongData.SongMetadata;
|
||||
import funkin.data.song.SongData.SongChartData;
|
||||
import funkin.data.song.SongData.SongCharacterData;
|
||||
import funkin.data.song.SongData.SongEventData;
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
import funkin.data.song.SongData.SongTimeChange;
|
||||
import funkin.data.song.importer.FNFLegacyData;
|
||||
import funkin.data.song.importer.FNFLegacyData.LegacyNoteSection;
|
||||
|
||||
class FNFLegacyImporter
|
||||
{
|
||||
public static function parseLegacyDataRaw(input:String, fileName:String = 'raw'):FNFLegacyData
|
||||
{
|
||||
var parser = new json2object.JsonParser<FNFLegacyData>();
|
||||
parser.fromJson(input, fileName);
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
{
|
||||
trace('[FNFLegacyImporter] Error parsing JSON data from ' + fileName + ':');
|
||||
for (error in parser.errors)
|
||||
DataError.printError(error);
|
||||
return null;
|
||||
}
|
||||
return parser.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The raw parsed JSON data to migrate, as a Dynamic.
|
||||
* @param difficulty
|
||||
* @return SongMetadata
|
||||
*/
|
||||
public static function migrateMetadata(songData:FNFLegacyData, difficulty:String = 'normal'):SongMetadata
|
||||
{
|
||||
trace('Migrating song metadata from FNF Legacy.');
|
||||
|
||||
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)';
|
||||
|
||||
songMetadata.playData.stage = songData?.song?.stageDefault ?? 'mainStage';
|
||||
songMetadata.songName = songData?.song?.song ?? 'Import';
|
||||
songMetadata.playData.difficulties = [];
|
||||
|
||||
if (songData?.song?.notes != null)
|
||||
{
|
||||
switch (songData.song.notes)
|
||||
{
|
||||
case Left(notes):
|
||||
// One difficulty of notes.
|
||||
songMetadata.playData.difficulties.push(difficulty);
|
||||
case Right(difficulties):
|
||||
if (difficulties.easy != null) songMetadata.playData.difficulties.push('easy');
|
||||
if (difficulties.normal != null) songMetadata.playData.difficulties.push('normal');
|
||||
if (difficulties.hard != null) songMetadata.playData.difficulties.push('hard');
|
||||
}
|
||||
}
|
||||
|
||||
songMetadata.playData.songVariations = [];
|
||||
|
||||
songMetadata.timeChanges = rebuildTimeChanges(songData);
|
||||
|
||||
songMetadata.playData.characters = new SongCharacterData(songData?.song?.player1 ?? 'bf', 'gf', songData?.song?.player2 ?? 'dad', 'mom');
|
||||
|
||||
return songMetadata;
|
||||
}
|
||||
|
||||
public static function migrateChartData(songData:FNFLegacyData, difficulty:String = 'normal'):SongChartData
|
||||
{
|
||||
trace('Migrating song chart data from FNF Legacy.');
|
||||
|
||||
var songChartData:SongChartData = new SongChartData([difficulty => 1.0], [], [difficulty => []]);
|
||||
|
||||
if (songData?.song?.notes != null)
|
||||
{
|
||||
switch (songData.song.notes)
|
||||
{
|
||||
case Left(notes):
|
||||
// One difficulty of notes.
|
||||
songChartData.notes.set(difficulty, migrateNoteSections(notes));
|
||||
case Right(difficulties):
|
||||
var baseDifficulty = null;
|
||||
if (difficulties.easy != null) songChartData.notes.set('easy', migrateNoteSections(difficulties.easy));
|
||||
if (difficulties.normal != null) songChartData.notes.set('normal', migrateNoteSections(difficulties.normal));
|
||||
if (difficulties.hard != null) songChartData.notes.set('hard', migrateNoteSections(difficulties.hard));
|
||||
}
|
||||
}
|
||||
|
||||
// Import event data.
|
||||
songChartData.events = rebuildEventData(songData);
|
||||
|
||||
switch (songData.song.speed)
|
||||
{
|
||||
case Left(speed):
|
||||
// All difficulties will use the one scroll speed.
|
||||
songChartData.scrollSpeed.set('default', speed);
|
||||
case Right(speeds):
|
||||
if (speeds.easy != null) songChartData.scrollSpeed.set('easy', speeds.easy);
|
||||
if (speeds.normal != null) songChartData.scrollSpeed.set('normal', speeds.normal);
|
||||
if (speeds.hard != null) songChartData.scrollSpeed.set('hard', speeds.hard);
|
||||
}
|
||||
|
||||
return songChartData;
|
||||
}
|
||||
|
||||
/**
|
||||
* FNF Legacy doesn't have song events, but without them the song won't look right,
|
||||
* so we insert camera events when the character changes.
|
||||
*/
|
||||
static function rebuildEventData(songData:FNFLegacyData):Array<SongEventData>
|
||||
{
|
||||
var result:Array<SongEventData> = [];
|
||||
|
||||
var noteSections = [];
|
||||
switch (songData.song.notes)
|
||||
{
|
||||
case Left(notes):
|
||||
// All difficulties will use the one scroll speed.
|
||||
noteSections = notes;
|
||||
case Right(difficulties):
|
||||
if (difficulties.normal != null) noteSections = difficulties.normal;
|
||||
if (difficulties.hard != null) noteSections = difficulties.normal;
|
||||
if (difficulties.easy != null) noteSections = difficulties.normal;
|
||||
}
|
||||
|
||||
if (noteSections == null || noteSections.length == 0) return result;
|
||||
|
||||
// Add camera events.
|
||||
var lastSectionWasMustHit:Null<Bool> = null;
|
||||
for (section in noteSections)
|
||||
{
|
||||
// Skip empty sections.
|
||||
if (section.sectionNotes.length == 0) continue;
|
||||
|
||||
if (section.mustHitSection != lastSectionWasMustHit)
|
||||
{
|
||||
lastSectionWasMustHit = section.mustHitSection;
|
||||
|
||||
var firstNote:LegacyNote = section.sectionNotes[0];
|
||||
|
||||
result.push(new SongEventData(firstNote.time, 'FocusCamera', {char: section.mustHitSection ? 0 : 1}));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Port over time changes from FNF Legacy.
|
||||
* If a section contains a BPM change, it will be applied at the timestamp of the first note in that section.
|
||||
*/
|
||||
static function rebuildTimeChanges(songData:FNFLegacyData):Array<SongTimeChange>
|
||||
{
|
||||
var result:Array<SongTimeChange> = [];
|
||||
|
||||
result.push(new SongTimeChange(0, songData?.song?.bpm ?? Constants.DEFAULT_BPM));
|
||||
|
||||
var noteSections = [];
|
||||
switch (songData.song.notes)
|
||||
{
|
||||
case Left(notes):
|
||||
// All difficulties will use the one scroll speed.
|
||||
noteSections = notes;
|
||||
case Right(difficulties):
|
||||
if (difficulties.normal != null) noteSections = difficulties.normal;
|
||||
if (difficulties.hard != null) noteSections = difficulties.normal;
|
||||
if (difficulties.easy != null) noteSections = difficulties.normal;
|
||||
}
|
||||
|
||||
if (noteSections == null || noteSections.length == 0) return result;
|
||||
|
||||
for (noteSection in noteSections)
|
||||
{
|
||||
if (noteSection.changeBPM ?? false)
|
||||
{
|
||||
var firstNote:LegacyNote = noteSection.sectionNotes[0];
|
||||
if (firstNote != null) result.push(new SongTimeChange(firstNote.time, noteSection.bpm));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static function migrateNoteSections(input:Array<LegacyNoteSection>):Array<SongNoteData>
|
||||
{
|
||||
var result:Array<SongNoteData> = [];
|
||||
|
||||
for (section in input)
|
||||
{
|
||||
for (note in section.sectionNotes)
|
||||
{
|
||||
result.push(new SongNoteData(note.time, note.data, note.length, note.getKind()));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
66
source/funkin/data/song/migrator/SongDataMigrator.hx
Normal file
66
source/funkin/data/song/migrator/SongDataMigrator.hx
Normal file
|
@ -0,0 +1,66 @@
|
|||
package funkin.data.song.migrator;
|
||||
|
||||
import funkin.data.song.SongData.SongMetadata;
|
||||
import funkin.data.song.SongData.SongPlayData;
|
||||
import funkin.data.song.SongData.SongCharacterData;
|
||||
import funkin.data.song.migrator.SongData_v2_0_0.SongMetadata_v2_0_0;
|
||||
import funkin.data.song.migrator.SongData_v2_0_0.SongPlayData_v2_0_0;
|
||||
import funkin.data.song.migrator.SongData_v2_0_0.SongPlayableChar_v2_0_0;
|
||||
|
||||
/**
|
||||
* This class contains functions to migrate older data formats to the current one.
|
||||
*
|
||||
* Utilizes static extensions with overloaded inline functions to make migration as easy as `.migrate()`.
|
||||
* @see https://try.haxe.org/#e1c1cf22
|
||||
*/
|
||||
class SongDataMigrator
|
||||
{
|
||||
public static overload extern inline function migrate(input:SongData_v2_0_0.SongMetadata_v2_0_0):SongMetadata
|
||||
{
|
||||
return migrate_SongMetadata_v2_0_0(input);
|
||||
}
|
||||
|
||||
public static function migrate_SongMetadata_v2_0_0(input:SongData_v2_0_0.SongMetadata_v2_0_0):SongMetadata
|
||||
{
|
||||
var result:SongMetadata = new SongMetadata(input.songName, input.artist, input.variation);
|
||||
result.version = input.version;
|
||||
result.timeFormat = input.timeFormat;
|
||||
result.divisions = input.divisions;
|
||||
result.timeChanges = input.timeChanges;
|
||||
result.looped = input.looped;
|
||||
result.playData = migrate_SongPlayData_v2_0_0(input.playData);
|
||||
result.generatedBy = input.generatedBy;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static overload extern inline function migrate(input:SongData_v2_0_0.SongPlayData_v2_0_0):SongPlayData
|
||||
{
|
||||
return migrate_SongPlayData_v2_0_0(input);
|
||||
}
|
||||
|
||||
public static function migrate_SongPlayData_v2_0_0(input:SongData_v2_0_0.SongPlayData_v2_0_0):SongPlayData
|
||||
{
|
||||
var result:SongPlayData = new SongPlayData();
|
||||
result.songVariations = input.songVariations;
|
||||
result.difficulties = input.difficulties;
|
||||
result.stage = input.stage;
|
||||
result.noteSkin = input.noteSkin;
|
||||
|
||||
// Fetch the first playable character and migrate it.
|
||||
var firstCharKey:Null<String> = input.playableChars.size() == 0 ? null : input.playableChars.keys().array()[0];
|
||||
var firstCharData:Null<SongPlayableChar_v2_0_0> = input.playableChars.get(firstCharKey);
|
||||
|
||||
if (firstCharData == null)
|
||||
{
|
||||
// Fill in a default playable character.
|
||||
result.characters = new SongCharacterData('bf', 'gf', 'dad');
|
||||
}
|
||||
else
|
||||
{
|
||||
result.characters = new SongCharacterData(firstCharKey, firstCharData.girlfriend, firstCharData.opponent, firstCharData.inst);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
122
source/funkin/data/song/migrator/SongData_v2_0_0.hx
Normal file
122
source/funkin/data/song/migrator/SongData_v2_0_0.hx
Normal file
|
@ -0,0 +1,122 @@
|
|||
package funkin.data.song.migrator;
|
||||
|
||||
import thx.semver.Version;
|
||||
import funkin.data.song.SongData;
|
||||
|
||||
class SongMetadata_v2_0_0
|
||||
{
|
||||
// ==========
|
||||
// MODIFIED VALUES
|
||||
// ===========
|
||||
|
||||
/**
|
||||
* In metadata `v2.1.0`, `SongPlayData` was refactored.
|
||||
*/
|
||||
public var playData:SongPlayData_v2_0_0;
|
||||
|
||||
/**
|
||||
* In metadata `v2.1.0`, `variation` was set to `ignore` when writing.
|
||||
*/
|
||||
@:optional
|
||||
@:default('default')
|
||||
public var variation:String;
|
||||
|
||||
// ==========
|
||||
// UNMODIFIED VALUES
|
||||
// ==========
|
||||
public var version:Version;
|
||||
|
||||
@:default("Unknown")
|
||||
public var songName:String;
|
||||
|
||||
@:default("Unknown")
|
||||
public var artist:String;
|
||||
|
||||
@:optional
|
||||
@:default(96)
|
||||
public var divisions:Null<Int>; // Optional field
|
||||
|
||||
@:optional
|
||||
@:default(false)
|
||||
public var looped:Bool;
|
||||
|
||||
public var generatedBy:String;
|
||||
|
||||
public var timeFormat:SongData.SongTimeFormat;
|
||||
|
||||
public var timeChanges:Array<SongData.SongTimeChange>;
|
||||
|
||||
public function new() {}
|
||||
|
||||
/**
|
||||
* Produces a string representation suitable for debugging.
|
||||
*/
|
||||
public function toString():String
|
||||
{
|
||||
return 'SongMetadata[LEGACY:v2.0.0](${this.songName} by ${this.artist}, variation ${this.variation})';
|
||||
}
|
||||
}
|
||||
|
||||
class SongPlayData_v2_0_0
|
||||
{
|
||||
// ==========
|
||||
// MODIFIED VALUES
|
||||
// ===========
|
||||
|
||||
/**
|
||||
* In metadata version `v2.1.0`, this was refactored to a single `SongCharacterData` object.
|
||||
*/
|
||||
public var playableChars:Map<String, SongPlayableChar_v2_0_0>;
|
||||
|
||||
// ==========
|
||||
// UNMODIFIED VALUES
|
||||
// ==========
|
||||
public var songVariations:Array<String>;
|
||||
public var difficulties:Array<String>;
|
||||
|
||||
public var stage:String;
|
||||
public var noteSkin:String;
|
||||
|
||||
public function new() {}
|
||||
|
||||
/**
|
||||
* Produces a string representation suitable for debugging.
|
||||
*/
|
||||
public function toString():String
|
||||
{
|
||||
return 'SongPlayData[LEGACY:v2.0.0](${this.songVariations}, ${this.difficulties})';
|
||||
}
|
||||
}
|
||||
|
||||
class SongPlayableChar_v2_0_0
|
||||
{
|
||||
@:alias('g')
|
||||
@:optional
|
||||
@:default('')
|
||||
public var girlfriend:String = '';
|
||||
|
||||
@:alias('o')
|
||||
@:optional
|
||||
@:default('')
|
||||
public var opponent:String = '';
|
||||
|
||||
@:alias('i')
|
||||
@:optional
|
||||
@:default('')
|
||||
public var inst:String = '';
|
||||
|
||||
public function new(girlfriend:String = '', opponent:String = '', inst:String = '')
|
||||
{
|
||||
this.girlfriend = girlfriend;
|
||||
this.opponent = opponent;
|
||||
this.inst = inst;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a string representation suitable for debugging.
|
||||
*/
|
||||
public function toString():String
|
||||
{
|
||||
return 'SongPlayableChar[LEGACY:v2.0.0](${this.girlfriend}, ${this.opponent}, ${this.inst})';
|
||||
}
|
||||
}
|
|
@ -46,7 +46,7 @@ import funkin.play.song.Song;
|
|||
import funkin.data.song.SongRegistry;
|
||||
import funkin.data.song.SongData.SongEventData;
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
import funkin.data.song.SongData.SongPlayableChar;
|
||||
import funkin.data.song.SongData.SongCharacterData;
|
||||
import funkin.play.stage.Stage;
|
||||
import funkin.play.stage.StageData.StageDataParser;
|
||||
import funkin.ui.PopUpStuff;
|
||||
|
@ -574,8 +574,8 @@ class PlayState extends MusicBeatSubState
|
|||
// Prepare the current song's instrumental and vocals to be played.
|
||||
if (!overrideMusic && currentChart != null)
|
||||
{
|
||||
currentChart.cacheInst(currentPlayerId);
|
||||
currentChart.cacheVocals(currentPlayerId);
|
||||
currentChart.cacheInst();
|
||||
currentChart.cacheVocals();
|
||||
}
|
||||
|
||||
// Prepare the Conductor.
|
||||
|
@ -733,7 +733,7 @@ class PlayState extends MusicBeatSubState
|
|||
// DO NOT FORGET TO REMOVE THE HARDCODE! WHEN I MAKE BETTER OFFSET SYSTEM!
|
||||
|
||||
// :nerd: um ackshually it's not 13 it's 11.97278911564
|
||||
if (Paths.SOUND_EXT == 'mp3') Conductor.offset = Constants.MP3_DELAY_MS;
|
||||
if (Constants.EXT_SOUND == 'mp3') Conductor.offset = Constants.MP3_DELAY_MS;
|
||||
|
||||
Conductor.update();
|
||||
|
||||
|
@ -1344,34 +1344,20 @@ class PlayState extends MusicBeatSubState
|
|||
trace('Song difficulty could not be loaded.');
|
||||
}
|
||||
|
||||
// Switch the character we are playing as by manipulating currentPlayerId.
|
||||
// TODO: How to choose which one to use for story mode?
|
||||
var playableChars:Array<String> = currentChart.getPlayableChars();
|
||||
|
||||
if (playableChars.length == 0)
|
||||
{
|
||||
trace('WARNING: No playable characters found for this song.');
|
||||
}
|
||||
else if (playableChars.indexOf(currentPlayerId) == -1)
|
||||
{
|
||||
currentPlayerId = playableChars[0];
|
||||
}
|
||||
|
||||
//
|
||||
var currentCharData:SongPlayableChar = currentChart.getPlayableChar(currentPlayerId);
|
||||
var currentCharacterData:SongCharacterData = currentChart.characters; // Switch the character we are playing as by manipulating currentPlayerId.
|
||||
|
||||
//
|
||||
// GIRLFRIEND
|
||||
//
|
||||
var girlfriend:BaseCharacter = CharacterDataParser.fetchCharacter(currentCharData.girlfriend);
|
||||
var girlfriend:BaseCharacter = CharacterDataParser.fetchCharacter(currentCharacterData.girlfriend);
|
||||
|
||||
if (girlfriend != null)
|
||||
{
|
||||
girlfriend.characterType = CharacterType.GF;
|
||||
}
|
||||
else if (currentCharData.girlfriend != '')
|
||||
else if (currentCharacterData.girlfriend != '')
|
||||
{
|
||||
trace('WARNING: Could not load girlfriend character with ID ${currentCharData.girlfriend}, skipping...');
|
||||
trace('WARNING: Could not load girlfriend character with ID ${currentCharacterData.girlfriend}, skipping...');
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -1381,7 +1367,7 @@ class PlayState extends MusicBeatSubState
|
|||
//
|
||||
// DAD
|
||||
//
|
||||
var dad:BaseCharacter = CharacterDataParser.fetchCharacter(currentCharData.opponent);
|
||||
var dad:BaseCharacter = CharacterDataParser.fetchCharacter(currentCharacterData.opponent);
|
||||
|
||||
if (dad != null)
|
||||
{
|
||||
|
@ -1400,7 +1386,7 @@ class PlayState extends MusicBeatSubState
|
|||
//
|
||||
// BOYFRIEND
|
||||
//
|
||||
var boyfriend:BaseCharacter = CharacterDataParser.fetchCharacter(currentPlayerId);
|
||||
var boyfriend:BaseCharacter = CharacterDataParser.fetchCharacter(currentCharacterData.player);
|
||||
|
||||
if (boyfriend != null)
|
||||
{
|
||||
|
@ -1549,7 +1535,7 @@ class PlayState extends MusicBeatSubState
|
|||
|
||||
if (!overrideMusic)
|
||||
{
|
||||
vocals = currentChart.buildVocals(currentPlayerId);
|
||||
vocals = currentChart.buildVocals();
|
||||
|
||||
if (vocals.members.length == 0)
|
||||
{
|
||||
|
|
|
@ -172,9 +172,13 @@ enum abstract BackdropType(String) from String to String
|
|||
class MusicData
|
||||
{
|
||||
public var asset:String;
|
||||
public var looped:Bool;
|
||||
|
||||
public var fadeTime:Float;
|
||||
|
||||
@:optional
|
||||
@:default(false)
|
||||
public var looped:Bool;
|
||||
|
||||
public function new(asset:String, looped:Bool, fadeTime:Float = 0.0)
|
||||
{
|
||||
this.asset = asset;
|
||||
|
|
|
@ -6,6 +6,7 @@ import funkin.play.cutscene.dialogue.ScriptedConversation;
|
|||
|
||||
/**
|
||||
* Contains utilities for loading and parsing conversation data.
|
||||
* TODO: Refactor to use the json2object + BaseRegistry system that actually validates things for you.
|
||||
*/
|
||||
class ConversationDataParser
|
||||
{
|
||||
|
|
|
@ -11,7 +11,7 @@ import funkin.data.song.SongData.SongEventData;
|
|||
import funkin.data.song.SongData.SongNoteData;
|
||||
import funkin.data.song.SongRegistry;
|
||||
import funkin.data.song.SongData.SongMetadata;
|
||||
import funkin.data.song.SongData.SongPlayableChar;
|
||||
import funkin.data.song.SongData.SongCharacterData;
|
||||
import funkin.data.song.SongData.SongTimeChange;
|
||||
import funkin.data.song.SongData.SongTimeFormat;
|
||||
import funkin.data.IRegistryEntry;
|
||||
|
@ -176,18 +176,11 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
|
|||
difficulty.generatedBy = metadata.generatedBy;
|
||||
|
||||
difficulty.stage = metadata.playData.stage;
|
||||
// difficulty.noteSkin = metadata.playData.noteSkin;
|
||||
difficulty.noteStyle = metadata.playData.noteSkin;
|
||||
|
||||
difficulties.set(diffId, difficulty);
|
||||
|
||||
difficulty.chars = new Map<String, SongPlayableChar>();
|
||||
if (metadata.playData.playableChars == null) continue;
|
||||
for (charId in metadata.playData.playableChars.keys())
|
||||
{
|
||||
var char:Null<SongPlayableChar> = metadata.playData.playableChars.get(charId);
|
||||
if (char == null) continue;
|
||||
difficulty.chars.set(charId, char);
|
||||
}
|
||||
difficulty.characters = metadata.playData.characters;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -365,19 +358,20 @@ class SongDifficulty
|
|||
*/
|
||||
public var events:Array<SongEventData>;
|
||||
|
||||
public var songName:String = SongValidator.DEFAULT_SONGNAME;
|
||||
public var songArtist:String = SongValidator.DEFAULT_ARTIST;
|
||||
public var timeFormat:SongTimeFormat = SongValidator.DEFAULT_TIMEFORMAT;
|
||||
public var divisions:Null<Int> = SongValidator.DEFAULT_DIVISIONS;
|
||||
public var looped:Bool = SongValidator.DEFAULT_LOOPED;
|
||||
public var songName:String = Constants.DEFAULT_SONGNAME;
|
||||
public var songArtist:String = Constants.DEFAULT_ARTIST;
|
||||
public var timeFormat:SongTimeFormat = Constants.DEFAULT_TIMEFORMAT;
|
||||
public var divisions:Null<Int> = null;
|
||||
public var looped:Bool = false;
|
||||
public var generatedBy:String = SongRegistry.DEFAULT_GENERATEDBY;
|
||||
|
||||
public var timeChanges:Array<SongTimeChange> = [];
|
||||
|
||||
public var stage:String = SongValidator.DEFAULT_STAGE;
|
||||
public var chars:Map<String, SongPlayableChar> = null;
|
||||
public var stage:String = Constants.DEFAULT_STAGE;
|
||||
public var noteStyle:String = Constants.DEFAULT_NOTE_STYLE;
|
||||
public var characters:SongCharacterData = null;
|
||||
|
||||
public var scrollSpeed:Float = SongValidator.DEFAULT_SCROLLSPEED;
|
||||
public var scrollSpeed:Float = Constants.DEFAULT_SCROLLSPEED;
|
||||
|
||||
public function new(song:Song, diffId:String, variation:String)
|
||||
{
|
||||
|
@ -401,28 +395,24 @@ class SongDifficulty
|
|||
return timeChanges[0].bpm;
|
||||
}
|
||||
|
||||
public function getPlayableChar(id:String):Null<SongPlayableChar>
|
||||
{
|
||||
if (id == null || id == '') return null;
|
||||
return chars.get(id);
|
||||
}
|
||||
|
||||
public function getPlayableChars():Array<String>
|
||||
{
|
||||
return chars.keys().array();
|
||||
}
|
||||
|
||||
public function getEvents():Array<SongEventData>
|
||||
{
|
||||
return cast events;
|
||||
}
|
||||
|
||||
public inline function cacheInst(?currentPlayerId:String = null):Void
|
||||
public function cacheInst(instrumental = ''):Void
|
||||
{
|
||||
var currentPlayer:Null<SongPlayableChar> = getPlayableChar(currentPlayerId);
|
||||
if (currentPlayer != null)
|
||||
if (characters != null)
|
||||
{
|
||||
FlxG.sound.cache(Paths.inst(this.song.id, currentPlayer.inst));
|
||||
if (instrumental != '' && characters.altInstrumentals.contains(instrumental))
|
||||
{
|
||||
FlxG.sound.cache(Paths.inst(this.song.id, instrumental));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback to default instrumental.
|
||||
FlxG.sound.cache(Paths.inst(this.song.id, characters.instrumental));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -440,9 +430,9 @@ class SongDifficulty
|
|||
* Cache the vocals for a given character.
|
||||
* @param id The character we are about to play.
|
||||
*/
|
||||
public inline function cacheVocals(?id:String = 'bf'):Void
|
||||
public inline function cacheVocals():Void
|
||||
{
|
||||
for (voice in buildVoiceList(id))
|
||||
for (voice in buildVoiceList())
|
||||
{
|
||||
FlxG.sound.cache(voice);
|
||||
}
|
||||
|
@ -454,22 +444,15 @@ class SongDifficulty
|
|||
*
|
||||
* @param id The character we are about to play.
|
||||
*/
|
||||
public function buildVoiceList(?id:String = 'bf'):Array<String>
|
||||
public function buildVoiceList():Array<String>
|
||||
{
|
||||
var playableCharData:SongPlayableChar = getPlayableChar(id);
|
||||
if (playableCharData == null)
|
||||
{
|
||||
trace('Could not find playable char $id for song ${this.song.id}');
|
||||
return [];
|
||||
}
|
||||
|
||||
var suffix:String = (variation != null && variation != '' && variation != 'default') ? '-$variation' : '';
|
||||
|
||||
// Automatically resolve voices by removing suffixes.
|
||||
// For example, if `Voices-bf-car.ogg` does not exist, check for `Voices-bf.ogg`.
|
||||
|
||||
var playerId:String = id;
|
||||
var voicePlayer:String = Paths.voices(this.song.id, '-$id$suffix');
|
||||
var playerId:String = characters.player;
|
||||
var voicePlayer:String = Paths.voices(this.song.id, '-$playerId$suffix');
|
||||
while (voicePlayer != null && !Assets.exists(voicePlayer))
|
||||
{
|
||||
// Remove the last suffix.
|
||||
|
@ -479,7 +462,7 @@ class SongDifficulty
|
|||
voicePlayer = playerId == '' ? null : Paths.voices(this.song.id, '-${playerId}$suffix');
|
||||
}
|
||||
|
||||
var opponentId:String = playableCharData.opponent;
|
||||
var opponentId:String = characters.opponent;
|
||||
var voiceOpponent:String = Paths.voices(this.song.id, '-${opponentId}$suffix');
|
||||
while (voiceOpponent != null && !Assets.exists(voiceOpponent))
|
||||
{
|
||||
|
@ -505,11 +488,11 @@ class SongDifficulty
|
|||
* @param charId The player ID.
|
||||
* @return The generated vocal group.
|
||||
*/
|
||||
public function buildVocals(charId:String = 'bf'):VoicesGroup
|
||||
public function buildVocals():VoicesGroup
|
||||
{
|
||||
var result:VoicesGroup = new VoicesGroup();
|
||||
|
||||
var voiceList:Array<String> = buildVoiceList(charId);
|
||||
var voiceList:Array<String> = buildVoiceList();
|
||||
|
||||
if (voiceList.length == 0)
|
||||
{
|
||||
|
|
|
@ -1,256 +0,0 @@
|
|||
package funkin.play.song;
|
||||
|
||||
import funkin.play.song.formats.FNFLegacy;
|
||||
import funkin.data.song.SongData.SongChartData;
|
||||
import funkin.data.song.SongData.SongEventData;
|
||||
import funkin.data.song.SongData.SongMetadata;
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
import funkin.data.song.SongData.SongPlayableChar;
|
||||
import funkin.util.VersionUtil;
|
||||
|
||||
class SongMigrator
|
||||
{
|
||||
/**
|
||||
* The current latest version string for the song data format.
|
||||
* Handle breaking changes by incrementing this value
|
||||
* and adding migration to the SongMigrator class.
|
||||
*/
|
||||
public static final CHART_VERSION:String = '2.0.0';
|
||||
|
||||
/**
|
||||
* 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 != null)
|
||||
{
|
||||
if (VersionUtil.validateVersionStr(jsonData.version, CHART_VERSION_RULE))
|
||||
{
|
||||
trace('Song (${songId}) metadata version (${jsonData.version}) is valid and up-to-date.');
|
||||
|
||||
var songMetadata:SongMetadata = cast jsonData;
|
||||
|
||||
return songMetadata;
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('Song (${songId}) metadata version (${jsonData.version}) is outdated.');
|
||||
switch (jsonData.version)
|
||||
{
|
||||
case '1.0.0':
|
||||
return migrateSongMetadataFromLegacy(jsonData);
|
||||
default:
|
||||
trace('Song (${songId}) has unknown metadata version (${jsonData.version}), assuming FNF Legacy.');
|
||||
return migrateSongMetadataFromLegacy(jsonData);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('Song metadata version is missing.');
|
||||
}
|
||||
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)
|
||||
{
|
||||
if (VersionUtil.validateVersionStr(jsonData.version, CHART_VERSION_RULE))
|
||||
{
|
||||
trace('Song (${songId}) chart version (${jsonData.version}) is valid and up-to-date.');
|
||||
|
||||
var songChartData:SongChartData = cast jsonData;
|
||||
|
||||
return songChartData;
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('Song (${songId}) chart version (${jsonData.version}) is outdated.');
|
||||
switch (jsonData.version)
|
||||
{
|
||||
// TODO: Add migration functions as cases here.
|
||||
default:
|
||||
// Unknown version.
|
||||
trace('Song (${songId}) unknown chart version: ${jsonData.version}');
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('Song chart version is missing.');
|
||||
}
|
||||
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, difficulty:String = 'normal'):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 (Std.isOfType(songData.song.notes, Array))
|
||||
{
|
||||
// One difficulty of notes.
|
||||
songMetadata.playData.difficulties.push(difficulty);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Multiple difficulties of notes.
|
||||
var songNoteDataDynamic:haxe.DynamicAccess<Dynamic> = cast songData.song.notes;
|
||||
for (difficultyKey in songNoteDataDynamic.keys())
|
||||
{
|
||||
songMetadata.playData.difficulties.push(difficultyKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
trace("Couldn't parse difficulties!");
|
||||
hadError = true;
|
||||
}
|
||||
|
||||
songMetadata.playData.songVariations = [];
|
||||
|
||||
// Set the song's song variations.
|
||||
songMetadata.playData.playableChars = [];
|
||||
try
|
||||
{
|
||||
songMetadata.playData.playableChars.set(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, difficulty:String = 'normal'):SongChartData
|
||||
{
|
||||
trace('Migrating song chart data from FNF Legacy.');
|
||||
|
||||
var songData:FNFLegacy = cast jsonData;
|
||||
|
||||
var songChartData:SongChartData = new SongChartData(["normal" => 1.0], [], ["normal" => []]);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -3,14 +3,14 @@ package funkin.play.song;
|
|||
import funkin.data.song.SongData.SongChartData;
|
||||
import funkin.data.song.SongData.SongMetadata;
|
||||
import funkin.util.SerializerUtil;
|
||||
import funkin.util.FileUtil;
|
||||
import lime.utils.Bytes;
|
||||
import openfl.events.Event;
|
||||
import openfl.events.IOErrorEvent;
|
||||
import openfl.net.FileReference;
|
||||
|
||||
/**
|
||||
* Utilities for exporting a chart to a JSON file.
|
||||
* Primarily used for the chart editor.
|
||||
* TODO: Refactor and remove this.
|
||||
*/
|
||||
class SongSerializer
|
||||
{
|
||||
|
@ -20,7 +20,7 @@ class SongSerializer
|
|||
*/
|
||||
public static function importSongChartDataSync(path:String):SongChartData
|
||||
{
|
||||
var fileData = readFile(path);
|
||||
var fileData = FileUtil.readStringFromPath(path);
|
||||
|
||||
if (fileData == null) return null;
|
||||
|
||||
|
@ -35,7 +35,7 @@ class SongSerializer
|
|||
*/
|
||||
public static function importSongMetadataSync(path:String):SongMetadata
|
||||
{
|
||||
var fileData = readFile(path);
|
||||
var fileData = FileUtil.readStringFromPath(path);
|
||||
|
||||
if (fileData == null) return null;
|
||||
|
||||
|
@ -50,7 +50,7 @@ class SongSerializer
|
|||
*/
|
||||
public static function importSongChartDataAsync(callback:SongChartData->Void):Void
|
||||
{
|
||||
browseFileReference(function(fileReference:FileReference) {
|
||||
FileUtil.browseFileReference(function(fileReference:FileReference) {
|
||||
var data = fileReference.data.toString();
|
||||
|
||||
if (data == null) return;
|
||||
|
@ -67,7 +67,7 @@ class SongSerializer
|
|||
*/
|
||||
public static function importSongMetadataAsync(callback:SongMetadata->Void):Void
|
||||
{
|
||||
browseFileReference(function(fileReference:FileReference) {
|
||||
FileUtil.browseFileReference(function(fileReference:FileReference) {
|
||||
var data = fileReference.data.toString();
|
||||
|
||||
if (data == null) return;
|
||||
|
@ -77,126 +77,4 @@ class SongSerializer
|
|||
if (songMetadata != null) callback(songMetadata);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, songId:String)
|
||||
{
|
||||
var path = '${songId}-chart.json';
|
||||
exportSongChartDataAs(path, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, songId:String)
|
||||
{
|
||||
var path = '${songId}-metadata.json';
|
||||
exportSongMetadataAs(path, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
var dataString = SerializerUtil.toJSON(data);
|
||||
|
||||
writeFileReference(path, dataString);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
var dataString = SerializerUtil.toJSON(data);
|
||||
|
||||
writeFileReference(path, dataString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the string contents of a file.
|
||||
* Only works on desktop platforms.
|
||||
* @param path The file path to read from.
|
||||
*/
|
||||
static function readFile(path:String):String
|
||||
{
|
||||
#if sys
|
||||
var fileBytes:Bytes = sys.io.File.getBytes(path);
|
||||
|
||||
if (fileBytes == null) return null;
|
||||
|
||||
return fileBytes.toString();
|
||||
#end
|
||||
|
||||
trace('ERROR: readFile not implemented for this platform');
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write string contents to a file.
|
||||
* Only works on desktop platforms.
|
||||
* @param path The file path to read from.
|
||||
*/
|
||||
static function writeFile(path:String, data:String):Void
|
||||
{
|
||||
#if sys
|
||||
sys.io.File.saveContent(path, data);
|
||||
return;
|
||||
#end
|
||||
trace('ERROR: writeFile not implemented for this platform');
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
var selectedFileRef:FileReference = e.target;
|
||||
trace('Selected file: ' + selectedFileRef.name);
|
||||
selectedFileRef.addEventListener(Event.COMPLETE, function(e) {
|
||||
var loadedFileRef:FileReference = e.target;
|
||||
trace('Loaded file: ' + loadedFileRef.name);
|
||||
callback(loadedFileRef);
|
||||
});
|
||||
selectedFileRef.load();
|
||||
});
|
||||
|
||||
file.browse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts the user to save a file to their computer.
|
||||
*/
|
||||
static function writeFileReference(path:String, data:String)
|
||||
{
|
||||
var file = new FileReference();
|
||||
file.addEventListener(Event.COMPLETE, function(e:Event) {
|
||||
trace('Successfully wrote file.');
|
||||
});
|
||||
file.addEventListener(Event.CANCEL, function(e:Event) {
|
||||
trace('Cancelled writing file.');
|
||||
});
|
||||
file.addEventListener(IOErrorEvent.IO_ERROR, function(e:IOErrorEvent) {
|
||||
trace('IO error writing file.');
|
||||
});
|
||||
file.save(data, path);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,149 +0,0 @@
|
|||
package funkin.play.song;
|
||||
|
||||
import funkin.data.song.SongRegistry;
|
||||
import funkin.data.song.SongData.SongChartData;
|
||||
import funkin.data.song.SongData.SongMetadata;
|
||||
import funkin.data.song.SongData.SongPlayData;
|
||||
import funkin.data.song.SongData.SongTimeChange;
|
||||
import funkin.data.song.SongData.SongTimeFormat;
|
||||
|
||||
/**
|
||||
* For SongMetadata and SongChartData objects,
|
||||
* ensures mandatory fields are present and populates optional fields with default values.
|
||||
*/
|
||||
class SongValidator
|
||||
{
|
||||
public static final DEFAULT_SONGNAME:String = "Unknown";
|
||||
public static final DEFAULT_ARTIST:String = "Unknown";
|
||||
public static final DEFAULT_TIMEFORMAT:SongTimeFormat = SongTimeFormat.MILLISECONDS;
|
||||
public static final DEFAULT_DIVISIONS:Null<Int> = null;
|
||||
public static final DEFAULT_LOOPED:Bool = false;
|
||||
public static final DEFAULT_STAGE:String = "mainStage";
|
||||
public static final DEFAULT_SCROLLSPEED:Float = 1.0;
|
||||
|
||||
public static var DEFAULT_GENERATEDBY(get, never):String;
|
||||
|
||||
static function get_DEFAULT_GENERATEDBY():String
|
||||
{
|
||||
return '${Constants.TITLE} - ${Constants.VERSION}';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public static function validateSongMetadata(input:SongMetadata, songId:String = 'unknown'):SongMetadata
|
||||
{
|
||||
if (input == null)
|
||||
{
|
||||
trace('[SONGDATA] Could not parse metadata for song ${songId}');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (input.songName == null)
|
||||
{
|
||||
trace('[SONGDATA] Song ${songId} is missing a songName field. ');
|
||||
input.songName = DEFAULT_SONGNAME;
|
||||
}
|
||||
if (input.artist == null)
|
||||
{
|
||||
trace('[SONGDATA] Song ${songId} is missing an artist field. ');
|
||||
input.artist = DEFAULT_ARTIST;
|
||||
}
|
||||
if (input.timeFormat == null)
|
||||
{
|
||||
trace('[SONGDATA] Song ${songId} is missing a timeFormat field. ');
|
||||
input.timeFormat = DEFAULT_TIMEFORMAT;
|
||||
}
|
||||
if (input.generatedBy == null)
|
||||
{
|
||||
input.generatedBy = SongRegistry.DEFAULT_GENERATEDBY;
|
||||
}
|
||||
|
||||
input.timeChanges = validateTimeChanges(input.timeChanges, songId);
|
||||
if (input.timeChanges == null)
|
||||
{
|
||||
trace('[SONGDATA] Song ${songId} is missing a timeChanges field. ');
|
||||
return null;
|
||||
}
|
||||
|
||||
input.playData = validatePlayData(input.playData, songId);
|
||||
|
||||
if (input.variation == null) input.variation = '';
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public static function validatePlayData(input:SongPlayData, songId:String = 'unknown'):SongPlayData
|
||||
{
|
||||
if (input == null)
|
||||
{
|
||||
trace('[SONGDATA] Could not parse metadata.playData for song ${songId}');
|
||||
return null;
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public static function validateTimeChange(input:SongTimeChange, songId:String = 'unknown'):SongTimeChange
|
||||
{
|
||||
if (input == null)
|
||||
{
|
||||
trace('[SONGDATA] Could not parse metadata.timeChange for song ${songId}');
|
||||
return null;
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates multiple TimeChange objects in an array.
|
||||
*/
|
||||
public static function validateTimeChanges(input:Array<SongTimeChange>, songId:String = 'unknown'):Array<SongTimeChange>
|
||||
{
|
||||
if (input == null)
|
||||
{
|
||||
trace('[SONGDATA] Could not parse metadata.timeChange for song ${songId}');
|
||||
return null;
|
||||
}
|
||||
|
||||
input = input.map((timeChange) -> validateTimeChange(timeChange, songId));
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public static function validateSongChartData(input:SongChartData, songId:String = 'unknown'):SongChartData
|
||||
{
|
||||
if (input == null)
|
||||
{
|
||||
trace('[SONGDATA] Could not parse chart data for song ${songId}');
|
||||
return null;
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
}
|
|
@ -1,131 +0,0 @@
|
|||
package funkin.play.song.formats;
|
||||
|
||||
typedef FNFLegacy =
|
||||
{
|
||||
var song:LegacySongData;
|
||||
}
|
||||
|
||||
typedef LegacySongData =
|
||||
{
|
||||
var player1:String; // Boyfriend
|
||||
var player2:String; // Opponent
|
||||
|
||||
var speed:Float;
|
||||
var stageDefault:String;
|
||||
var bpm:Float;
|
||||
var notes:Array<LegacyNoteSection>;
|
||||
var song:String; // Song name
|
||||
};
|
||||
|
||||
typedef LegacyScrollSpeeds =
|
||||
{
|
||||
var easy:Float;
|
||||
var normal:Float;
|
||||
var hard:Float;
|
||||
};
|
||||
|
||||
typedef LegacyNoteData =
|
||||
{
|
||||
/**
|
||||
* The easy difficulty.
|
||||
*/
|
||||
var ?easy:Array<LegacyNoteSection>;
|
||||
|
||||
/**
|
||||
* The normal difficulty.
|
||||
*/
|
||||
var ?normal:Array<LegacyNoteSection>;
|
||||
|
||||
/**
|
||||
* The hard difficulty.
|
||||
*/
|
||||
var ?hard:Array<LegacyNoteSection>;
|
||||
};
|
||||
|
||||
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<LegacyNote>;
|
||||
|
||||
var typeOfSection:Int;
|
||||
var lengthInSteps:Int;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notes in the old format are stored as an Array<Dynamic>
|
||||
*/
|
||||
abstract LegacyNote(Array<Dynamic>)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
170
source/funkin/ui/debug/charting/ChartEditorAudioHandler.hx
Normal file
170
source/funkin/ui/debug/charting/ChartEditorAudioHandler.hx
Normal file
|
@ -0,0 +1,170 @@
|
|||
package funkin.ui.debug.charting;
|
||||
|
||||
import openfl.utils.Assets;
|
||||
import flixel.system.FlxAssets.FlxSoundAsset;
|
||||
import flixel.system.FlxSound;
|
||||
import funkin.play.character.BaseCharacter.CharacterType;
|
||||
import flixel.system.FlxSound;
|
||||
import haxe.io.Path;
|
||||
|
||||
/**
|
||||
* Functions for loading audio for the chart editor.
|
||||
*/
|
||||
@:nullSafety
|
||||
@:allow(funkin.ui.debug.charting.ChartEditorState)
|
||||
@:allow(funkin.ui.debug.charting.ChartEditorDialogHandler)
|
||||
@:allow(funkin.ui.debug.charting.ChartEditorImportExportHandler)
|
||||
class ChartEditorAudioHandler
|
||||
{
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
static function loadVocalsFromPath(state:ChartEditorState, path:Path, charKey:String = 'default'):Bool
|
||||
{
|
||||
#if sys
|
||||
var fileBytes:haxe.io.Bytes = sys.io.File.getBytes(path.toString());
|
||||
return loadVocalsFromBytes(state, fileBytes, charKey);
|
||||
#else
|
||||
trace("[WARN] This platform can't load audio from a file path, you'll need to fetch the bytes some other way.");
|
||||
return false;
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a vocal track for a given song and character and add it to the voices group.
|
||||
*
|
||||
* @param path ID of the asset.
|
||||
* @param charKey Character to load the vocal track for.
|
||||
* @return Success or failure.
|
||||
*/
|
||||
static function loadVocalsFromAsset(state:ChartEditorState, path:String, charType:CharacterType = OTHER):Bool
|
||||
{
|
||||
var vocalTrack:FlxSound = FlxG.sound.load(path, 1.0, false);
|
||||
if (vocalTrack != null)
|
||||
{
|
||||
switch (charType)
|
||||
{
|
||||
case CharacterType.BF:
|
||||
if (state.audioVocalTrackGroup != null) state.audioVocalTrackGroup.addPlayerVoice(vocalTrack);
|
||||
state.audioVocalTrackData.set(state.currentSongCharacterPlayer, Assets.getBytes(path));
|
||||
case CharacterType.DAD:
|
||||
if (state.audioVocalTrackGroup != null) state.audioVocalTrackGroup.addOpponentVoice(vocalTrack);
|
||||
state.audioVocalTrackData.set(state.currentSongCharacterOpponent, Assets.getBytes(path));
|
||||
default:
|
||||
if (state.audioVocalTrackGroup != null) state.audioVocalTrackGroup.add(vocalTrack);
|
||||
state.audioVocalTrackData.set('default', Assets.getBytes(path));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a vocal track from audio byte data.
|
||||
*/
|
||||
static function loadVocalsFromBytes(state:ChartEditorState, bytes:haxe.io.Bytes, charKey:String = ''):Bool
|
||||
{
|
||||
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);
|
||||
if (state.audioVocalTrackGroup != null) state.audioVocalTrackGroup.add(vocalTrack);
|
||||
state.audioVocalTrackData.set(charKey, bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
static function loadInstrumentalFromPath(state:ChartEditorState, path:Path):Bool
|
||||
{
|
||||
#if sys
|
||||
// Validate file extension.
|
||||
if (path.ext != null && !ChartEditorState.SUPPORTED_MUSIC_FORMATS.contains(path.ext))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var fileBytes:haxe.io.Bytes = sys.io.File.getBytes(path.toString());
|
||||
return loadInstrumentalFromBytes(state, fileBytes, '${path.file}.${path.ext}');
|
||||
#else
|
||||
trace("[WARN] This platform can't load audio from a file path, you'll need to fetch the bytes some other way.");
|
||||
return false;
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an instrumental from audio byte data, replacing the current instrumental.
|
||||
* @param bytes The audio byte data.
|
||||
* @param fileName The name of the file, if available. Used for notifications.
|
||||
* @return Success or failure.
|
||||
*/
|
||||
static function loadInstrumentalFromBytes(state:ChartEditorState, 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);
|
||||
state.audioInstTrack = FlxG.sound.load(openflSound, 1.0, false);
|
||||
state.audioInstTrack.autoDestroy = false;
|
||||
state.audioInstTrack.pause();
|
||||
|
||||
state.audioInstTrackData = bytes;
|
||||
|
||||
state.postLoadInstrumental();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an instrumental from an OpenFL asset, replacing the current instrumental.
|
||||
* @param path The path to the asset. Use `Paths` to build this.
|
||||
* @return Success or failure.
|
||||
*/
|
||||
static function loadInstrumentalFromAsset(state:ChartEditorState, path:String):Bool
|
||||
{
|
||||
var instTrack:FlxSound = FlxG.sound.load(path, 1.0, false);
|
||||
if (instTrack != null)
|
||||
{
|
||||
state.audioInstTrack = instTrack;
|
||||
|
||||
state.audioInstTrackData = Assets.getBytes(path);
|
||||
|
||||
state.postLoadInstrumental();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Play a sound effect.
|
||||
* Automatically cleans up after itself and recycles previous FlxSound instances if available, for performance.
|
||||
*/
|
||||
public static function playSound(path:String):Void
|
||||
{
|
||||
var snd:FlxSound = FlxG.sound.list.recycle(FlxSound) ?? new FlxSound();
|
||||
|
||||
var asset:Null<FlxSoundAsset> = FlxG.sound.cache(path);
|
||||
if (asset == null)
|
||||
{
|
||||
trace('WARN: Failed to play sound $path, asset not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
snd.loadEmbedded(asset);
|
||||
snd.autoDestroy = true;
|
||||
FlxG.sound.list.add(snd);
|
||||
snd.play();
|
||||
}
|
||||
}
|
|
@ -64,7 +64,7 @@ class AddNotesCommand implements ChartEditorCommand
|
|||
state.currentEventSelection = [];
|
||||
}
|
||||
|
||||
state.playSound(Paths.sound('funnyNoise/funnyNoise-08'));
|
||||
ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-08'));
|
||||
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
|
@ -78,7 +78,7 @@ class AddNotesCommand implements ChartEditorCommand
|
|||
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes);
|
||||
state.currentNoteSelection = [];
|
||||
state.currentEventSelection = [];
|
||||
state.playSound(Paths.sound('funnyNoise/funnyNoise-01'));
|
||||
ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-01'));
|
||||
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
|
@ -114,7 +114,7 @@ class RemoveNotesCommand implements ChartEditorCommand
|
|||
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes);
|
||||
state.currentNoteSelection = [];
|
||||
state.currentEventSelection = [];
|
||||
state.playSound(Paths.sound('funnyNoise/funnyNoise-01'));
|
||||
ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-01'));
|
||||
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
|
@ -131,7 +131,7 @@ class RemoveNotesCommand implements ChartEditorCommand
|
|||
}
|
||||
state.currentNoteSelection = notes;
|
||||
state.currentEventSelection = [];
|
||||
state.playSound(Paths.sound('funnyNoise/funnyNoise-08'));
|
||||
ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-08'));
|
||||
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
|
@ -252,7 +252,7 @@ class AddEventsCommand implements ChartEditorCommand
|
|||
state.currentEventSelection = events;
|
||||
}
|
||||
|
||||
state.playSound(Paths.sound('funnyNoise/funnyNoise-08'));
|
||||
ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-08'));
|
||||
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
|
@ -296,7 +296,7 @@ class RemoveEventsCommand implements ChartEditorCommand
|
|||
{
|
||||
state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events);
|
||||
state.currentEventSelection = [];
|
||||
state.playSound(Paths.sound('funnyNoise/funnyNoise-01'));
|
||||
ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-01'));
|
||||
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
|
@ -312,7 +312,7 @@ class RemoveEventsCommand implements ChartEditorCommand
|
|||
state.currentSongChartEventData.push(event);
|
||||
}
|
||||
state.currentEventSelection = events;
|
||||
state.playSound(Paths.sound('funnyNoise/funnyNoise-08'));
|
||||
ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-08'));
|
||||
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
|
@ -352,7 +352,7 @@ class RemoveItemsCommand implements ChartEditorCommand
|
|||
state.currentNoteSelection = [];
|
||||
state.currentEventSelection = [];
|
||||
|
||||
state.playSound(Paths.sound('funnyNoise/funnyNoise-01'));
|
||||
ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-01'));
|
||||
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
|
@ -376,7 +376,7 @@ class RemoveItemsCommand implements ChartEditorCommand
|
|||
state.currentNoteSelection = notes;
|
||||
state.currentEventSelection = events;
|
||||
|
||||
state.playSound(Paths.sound('funnyNoise/funnyNoise-08'));
|
||||
ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-08'));
|
||||
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
|
|
|
@ -1,40 +1,45 @@
|
|||
package funkin.ui.debug.charting;
|
||||
|
||||
import funkin.play.character.CharacterData;
|
||||
import funkin.util.Constants;
|
||||
import funkin.util.SerializerUtil;
|
||||
import funkin.ui.haxeui.components.FunkinDropDown;
|
||||
import flixel.util.FlxTimer;
|
||||
import funkin.data.song.importer.FNFLegacyData;
|
||||
import funkin.data.song.importer.FNFLegacyImporter;
|
||||
import funkin.data.song.SongData.SongCharacterData;
|
||||
import funkin.data.song.SongData.SongChartData;
|
||||
import funkin.data.song.SongData.SongMetadata;
|
||||
import flixel.util.FlxTimer;
|
||||
import funkin.ui.haxeui.components.FunkinLink;
|
||||
import funkin.util.SortUtil;
|
||||
import funkin.data.song.SongData.SongTimeChange;
|
||||
import funkin.data.song.SongRegistry;
|
||||
import funkin.input.Cursor;
|
||||
import funkin.play.character.BaseCharacter;
|
||||
import funkin.play.character.CharacterData;
|
||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||
import funkin.play.song.Song;
|
||||
import funkin.play.song.SongMigrator;
|
||||
import funkin.play.song.SongValidator;
|
||||
import funkin.data.song.SongRegistry;
|
||||
import funkin.data.song.SongData.SongPlayableChar;
|
||||
import funkin.data.song.SongData.SongTimeChange;
|
||||
import funkin.play.stage.StageData;
|
||||
import funkin.ui.haxeui.components.FunkinLink;
|
||||
import funkin.util.Constants;
|
||||
import funkin.util.FileUtil;
|
||||
import funkin.util.SerializerUtil;
|
||||
import funkin.util.SortUtil;
|
||||
import funkin.util.VersionUtil;
|
||||
import haxe.io.Path;
|
||||
import haxe.ui.components.Button;
|
||||
import haxe.ui.components.DropDown;
|
||||
import haxe.ui.components.Label;
|
||||
import haxe.ui.components.Link;
|
||||
import haxe.ui.components.NumberStepper;
|
||||
import haxe.ui.components.Slider;
|
||||
import haxe.ui.components.TextField;
|
||||
import haxe.ui.containers.Box;
|
||||
import haxe.ui.containers.dialogs.Dialog;
|
||||
import haxe.ui.containers.dialogs.Dialog.DialogButton;
|
||||
import haxe.ui.containers.dialogs.Dialogs;
|
||||
import haxe.ui.containers.properties.PropertyGrid;
|
||||
import haxe.ui.containers.properties.PropertyGroup;
|
||||
import haxe.ui.containers.Form;
|
||||
import haxe.ui.containers.VBox;
|
||||
import haxe.ui.core.Component;
|
||||
import haxe.ui.events.UIEvent;
|
||||
import haxe.ui.notifications.NotificationManager;
|
||||
import haxe.ui.notifications.NotificationType;
|
||||
import thx.semver.Version;
|
||||
|
||||
using Lambda;
|
||||
|
||||
|
@ -48,13 +53,14 @@ class ChartEditorDialogHandler
|
|||
static final CHART_EDITOR_DIALOG_WELCOME_LAYOUT:String = Paths.ui('chart-editor/dialogs/welcome');
|
||||
static final CHART_EDITOR_DIALOG_UPLOAD_INST_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-inst');
|
||||
static final CHART_EDITOR_DIALOG_SONG_METADATA_LAYOUT:String = Paths.ui('chart-editor/dialogs/song-metadata');
|
||||
static final CHART_EDITOR_DIALOG_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');
|
||||
static final CHART_EDITOR_DIALOG_ADD_VARIATION_LAYOUT:String = Paths.ui('chart-editor/dialogs/add-variation');
|
||||
static final CHART_EDITOR_DIALOG_ADD_DIFFICULTY_LAYOUT:String = Paths.ui('chart-editor/dialogs/add-difficulty');
|
||||
|
||||
/**
|
||||
* Builds and opens a dialog giving brief credits for the chart editor.
|
||||
|
@ -135,7 +141,7 @@ class ChartEditorDialogHandler
|
|||
dialog.hideDialog(DialogButton.CANCEL);
|
||||
|
||||
// Load song from template
|
||||
state.loadSongAsTemplate(targetSongId);
|
||||
ChartEditorImportExportHandler.loadSongAsTemplate(state, targetSongId);
|
||||
}
|
||||
|
||||
splashTemplateContainer.addComponent(linkTemplateSong);
|
||||
|
@ -298,7 +304,7 @@ class ChartEditorDialogHandler
|
|||
{label: 'Audio File (.ogg)', extension: 'ogg'}], function(selectedFile:SelectedFileInfo) {
|
||||
if (selectedFile != null && selectedFile.bytes != null)
|
||||
{
|
||||
if (state.loadInstrumentalFromBytes(selectedFile.bytes))
|
||||
if (ChartEditorAudioHandler.loadInstrumentalFromBytes(state, selectedFile.bytes))
|
||||
{
|
||||
trace('Selected file: ' + selectedFile.fullPath);
|
||||
#if !mac
|
||||
|
@ -335,7 +341,7 @@ class ChartEditorDialogHandler
|
|||
onDropFile = function(pathStr:String) {
|
||||
var path:Path = new Path(pathStr);
|
||||
trace('Dropped file (${path})');
|
||||
if (state.loadInstrumentalFromPath(path))
|
||||
if (ChartEditorAudioHandler.loadInstrumentalFromPath(state, path))
|
||||
{
|
||||
// Tell the user the load was successful.
|
||||
#if !mac
|
||||
|
@ -457,62 +463,96 @@ class ChartEditorDialogHandler
|
|||
dialog.hideDialog(DialogButton.CANCEL);
|
||||
}
|
||||
|
||||
var dialogSongName:Null<TextField> = dialog.findComponent('dialogSongName', TextField);
|
||||
if (dialogSongName == null) throw 'Could not locate dialogSongName TextField in Song Metadata dialog';
|
||||
dialogSongName.onChange = function(event:UIEvent) {
|
||||
var newSongMetadata:SongMetadata = new SongMetadata('', '', 'default');
|
||||
|
||||
var inputSongName:Null<TextField> = dialog.findComponent('inputSongName', TextField);
|
||||
if (inputSongName == null) throw 'Could not locate inputSongName TextField in Song Metadata dialog';
|
||||
inputSongName.onChange = function(event:UIEvent) {
|
||||
var valid:Bool = event.target.text != null && event.target.text != '';
|
||||
|
||||
if (valid)
|
||||
{
|
||||
dialogSongName.removeClass('invalid-value');
|
||||
state.currentSongMetadata.songName = event.target.text;
|
||||
inputSongName.removeClass('invalid-value');
|
||||
newSongMetadata.songName = event.target.text;
|
||||
}
|
||||
else
|
||||
{
|
||||
state.currentSongMetadata.songName = "";
|
||||
newSongMetadata.songName = "";
|
||||
}
|
||||
};
|
||||
state.currentSongMetadata.songName = "";
|
||||
inputSongName.text = "";
|
||||
|
||||
var dialogSongArtist:Null<TextField> = dialog.findComponent('dialogSongArtist', TextField);
|
||||
if (dialogSongArtist == null) throw 'Could not locate dialogSongArtist TextField in Song Metadata dialog';
|
||||
dialogSongArtist.onChange = function(event:UIEvent) {
|
||||
var inputSongArtist:Null<TextField> = dialog.findComponent('inputSongArtist', TextField);
|
||||
if (inputSongArtist == null) throw 'Could not locate inputSongArtist TextField in Song Metadata dialog';
|
||||
inputSongArtist.onChange = function(event:UIEvent) {
|
||||
var valid:Bool = event.target.text != null && event.target.text != '';
|
||||
|
||||
if (valid)
|
||||
{
|
||||
dialogSongArtist.removeClass('invalid-value');
|
||||
state.currentSongMetadata.artist = event.target.text;
|
||||
inputSongArtist.removeClass('invalid-value');
|
||||
newSongMetadata.artist = event.target.text;
|
||||
}
|
||||
else
|
||||
{
|
||||
state.currentSongMetadata.artist = "";
|
||||
newSongMetadata.artist = "";
|
||||
}
|
||||
};
|
||||
state.currentSongMetadata.artist = "";
|
||||
inputSongArtist.text = "";
|
||||
|
||||
var dialogStage:Null<DropDown> = dialog.findComponent('dialogStage', DropDown);
|
||||
if (dialogStage == null) throw 'Could not locate dialogStage DropDown in Song Metadata dialog';
|
||||
dialogStage.onChange = function(event:UIEvent) {
|
||||
var inputStage:Null<DropDown> = dialog.findComponent('inputStage', DropDown);
|
||||
if (inputStage == null) throw 'Could not locate inputStage DropDown in Song Metadata dialog';
|
||||
inputStage.onChange = function(event:UIEvent) {
|
||||
if (event.data == null && event.data.id == null) return;
|
||||
state.currentSongMetadata.playData.stage = event.data.id;
|
||||
newSongMetadata.playData.stage = event.data.id;
|
||||
};
|
||||
state.currentSongMetadata.playData.stage = 'mainStage';
|
||||
var startingValueStage = ChartEditorDropdowns.populateDropdownWithStages(inputStage, newSongMetadata.playData.stage);
|
||||
inputStage.value = startingValueStage;
|
||||
|
||||
var dialogNoteSkin:Null<DropDown> = dialog.findComponent('dialogNoteSkin', DropDown);
|
||||
if (dialogNoteSkin == null) throw 'Could not locate dialogNoteSkin DropDown in Song Metadata dialog';
|
||||
dialogNoteSkin.onChange = function(event:UIEvent) {
|
||||
var inputNoteStyle:Null<FunkinDropDown> = dialog.findComponent('inputNoteStyle', FunkinDropDown);
|
||||
if (inputNoteStyle == null) throw 'Could not locate inputNoteStyle DropDown in Song Metadata dialog';
|
||||
inputNoteStyle.onChange = function(event:UIEvent) {
|
||||
if (event.data.id == null) return;
|
||||
state.currentSongNoteSkin = event.data.id;
|
||||
newSongMetadata.playData.noteSkin = event.data.id;
|
||||
};
|
||||
state.currentSongNoteSkin = 'funkin';
|
||||
var startingValueNoteStyle = ChartEditorDropdowns.populateDropdownWithNoteStyles(inputNoteStyle, newSongMetadata.playData.noteSkin);
|
||||
inputNoteStyle.value = startingValueNoteStyle;
|
||||
|
||||
var inputCharacterPlayer:Null<FunkinDropDown> = dialog.findComponent('inputCharacterPlayer', FunkinDropDown);
|
||||
if (inputCharacterPlayer == null) throw 'ChartEditorToolboxHandler.buildToolboxMetadataLayout() - Could not find inputCharacterPlayer component.';
|
||||
inputCharacterPlayer.onChange = function(event:UIEvent) {
|
||||
if (event.data?.id == null) return;
|
||||
newSongMetadata.playData.characters.player = event.data.id;
|
||||
};
|
||||
var startingValuePlayer = ChartEditorDropdowns.populateDropdownWithCharacters(inputCharacterPlayer, CharacterType.BF,
|
||||
newSongMetadata.playData.characters.player);
|
||||
inputCharacterPlayer.value = startingValuePlayer;
|
||||
|
||||
var inputCharacterOpponent:Null<FunkinDropDown> = dialog.findComponent('inputCharacterOpponent', FunkinDropDown);
|
||||
if (inputCharacterOpponent == null) throw 'ChartEditorToolboxHandler.buildToolboxMetadataLayout() - Could not find inputCharacterOpponent component.';
|
||||
inputCharacterOpponent.onChange = function(event:UIEvent) {
|
||||
if (event.data?.id == null) return;
|
||||
newSongMetadata.playData.characters.opponent = event.data.id;
|
||||
};
|
||||
var startingValueOpponent = ChartEditorDropdowns.populateDropdownWithCharacters(inputCharacterOpponent, CharacterType.DAD,
|
||||
newSongMetadata.playData.characters.opponent);
|
||||
inputCharacterOpponent.value = startingValueOpponent;
|
||||
|
||||
var inputCharacterGirlfriend:Null<FunkinDropDown> = dialog.findComponent('inputCharacterGirlfriend', FunkinDropDown);
|
||||
if (inputCharacterGirlfriend == null) throw 'ChartEditorToolboxHandler.buildToolboxMetadataLayout() - Could not find inputCharacterGirlfriend component.';
|
||||
inputCharacterGirlfriend.onChange = function(event:UIEvent) {
|
||||
if (event.data?.id == null) return;
|
||||
newSongMetadata.playData.characters.girlfriend = event.data.id == "none" ? "" : event.data.id;
|
||||
};
|
||||
var startingValueGirlfriend = ChartEditorDropdowns.populateDropdownWithCharacters(inputCharacterGirlfriend, CharacterType.GF,
|
||||
newSongMetadata.playData.characters.girlfriend);
|
||||
inputCharacterGirlfriend.value = startingValueGirlfriend;
|
||||
|
||||
var dialogBPM:Null<NumberStepper> = dialog.findComponent('dialogBPM', NumberStepper);
|
||||
if (dialogBPM == null) throw 'Could not locate dialogBPM NumberStepper in Song Metadata dialog';
|
||||
dialogBPM.onChange = function(event:UIEvent) {
|
||||
if (event.value == null || event.value <= 0) return;
|
||||
|
||||
var timeChanges:Array<SongTimeChange> = state.currentSongMetadata.timeChanges;
|
||||
var timeChanges:Array<SongTimeChange> = newSongMetadata.timeChanges;
|
||||
if (timeChanges == null || timeChanges.length == 0)
|
||||
{
|
||||
timeChanges = [new SongTimeChange(0, event.value)];
|
||||
|
@ -524,24 +564,9 @@ class ChartEditorDialogHandler
|
|||
|
||||
Conductor.forceBPM(event.value);
|
||||
|
||||
state.currentSongMetadata.timeChanges = timeChanges;
|
||||
newSongMetadata.timeChanges = timeChanges;
|
||||
};
|
||||
|
||||
var dialogCharGrid:Null<PropertyGrid> = dialog.findComponent('dialogCharGrid', PropertyGrid);
|
||||
if (dialogCharGrid == null) throw 'Could not locate dialogCharGrid PropertyGrid in Song Metadata dialog';
|
||||
var dialogCharAdd:Null<Button> = dialog.findComponent('dialogCharAdd', Button);
|
||||
if (dialogCharAdd == null) throw 'Could not locate dialogCharAdd Button in Song Metadata dialog';
|
||||
dialogCharAdd.onClick = function(event:UIEvent) {
|
||||
var charGroup:PropertyGroup;
|
||||
charGroup = buildCharGroup(state, null, () -> dialogCharGrid.removeComponent(charGroup));
|
||||
dialogCharGrid.addComponent(charGroup);
|
||||
};
|
||||
|
||||
// Empty the character list.
|
||||
state.currentSongMetadata.playData.playableChars = [];
|
||||
// Add at least one character group with no Remove button.
|
||||
dialogCharGrid.addComponent(buildCharGroup(state, 'bf'));
|
||||
|
||||
var dialogContinue:Null<Button> = dialog.findComponent('dialogContinue', Button);
|
||||
if (dialogContinue == null) throw 'Could not locate dialogContinue button in Song Metadata dialog';
|
||||
dialogContinue.onClick = (_event) -> dialog.hideDialog(DialogButton.APPLY);
|
||||
|
@ -549,78 +574,6 @@ class ChartEditorDialogHandler
|
|||
return dialog;
|
||||
}
|
||||
|
||||
static function buildCharGroup(state:ChartEditorState, key:String = '', removeFunc:Void->Void = null):PropertyGroup
|
||||
{
|
||||
var groupKey:String = key;
|
||||
|
||||
var getCharData:Void->Null<SongPlayableChar> = function():Null<SongPlayableChar> {
|
||||
if (state.currentSongMetadata.playData == null) return null;
|
||||
if (groupKey == null) groupKey = 'newChar${state.currentSongMetadata.playData.playableChars.keys().count()}';
|
||||
|
||||
var result = state.currentSongMetadata.playData.playableChars.get(groupKey);
|
||||
if (result == null)
|
||||
{
|
||||
result = new SongPlayableChar('', 'dad');
|
||||
state.currentSongMetadata.playData.playableChars.set(groupKey, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
var moveCharGroup:String->Void = function(target:String):Void {
|
||||
var charData:Null<SongPlayableChar> = getCharData();
|
||||
if (charData == null) return;
|
||||
|
||||
if (state.currentSongMetadata.playData.playableChars == null) return;
|
||||
state.currentSongMetadata.playData.playableChars.remove(groupKey);
|
||||
state.currentSongMetadata.playData.playableChars.set(target, charData);
|
||||
groupKey = target;
|
||||
}
|
||||
|
||||
var removeGroup:Void->Void = function():Void {
|
||||
if (state?.currentSongMetadata?.playData?.playableChars == null) return;
|
||||
state.currentSongMetadata.playData.playableChars.remove(groupKey);
|
||||
if (removeFunc != null) removeFunc();
|
||||
}
|
||||
|
||||
var charData:Null<SongPlayableChar> = getCharData();
|
||||
|
||||
var charGroup:PropertyGroup = cast state.buildComponent(CHART_EDITOR_DIALOG_SONG_METADATA_CHARGROUP_LAYOUT);
|
||||
|
||||
var charGroupPlayer:Null<DropDown> = charGroup.findComponent('charGroupPlayer', DropDown);
|
||||
if (charGroupPlayer == null) throw 'Could not locate charGroupPlayer DropDown in Song Metadata dialog';
|
||||
charGroupPlayer.onChange = function(event:UIEvent):Void {
|
||||
if (charData != null) return;
|
||||
charGroup.text = event.data.text;
|
||||
moveCharGroup(event.data.id);
|
||||
};
|
||||
|
||||
var charGroupOpponent:Null<DropDown> = charGroup.findComponent('charGroupOpponent', DropDown);
|
||||
if (charGroupOpponent == null) throw 'Could not locate charGroupOpponent DropDown in Song Metadata dialog';
|
||||
charGroupOpponent.onChange = function(event:UIEvent):Void {
|
||||
if (charData == null) return;
|
||||
charData.opponent = event.data.id;
|
||||
};
|
||||
charGroupOpponent.value = charData.opponent;
|
||||
|
||||
var charGroupGirlfriend:Null<DropDown> = charGroup.findComponent('charGroupGirlfriend', DropDown);
|
||||
if (charGroupGirlfriend == null) throw 'Could not locate charGroupGirlfriend DropDown in Song Metadata dialog';
|
||||
charGroupGirlfriend.onChange = function(event:UIEvent):Void {
|
||||
if (charData == null) return;
|
||||
charData.girlfriend = event.data.id;
|
||||
};
|
||||
charGroupGirlfriend.value = charData.girlfriend;
|
||||
|
||||
var charGroupRemove:Null<Button> = charGroup.findComponent('charGroupRemove', Button);
|
||||
if (charGroupRemove == null) throw 'Could not locate charGroupRemove Button in Song Metadata dialog';
|
||||
charGroupRemove.onClick = function(event:UIEvent):Void {
|
||||
removeGroup();
|
||||
};
|
||||
|
||||
if (removeFunc == null) charGroupRemove.hidden = true;
|
||||
|
||||
return charGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and opens a dialog where the user uploads vocals for the current song.
|
||||
* @param state The current chart editor state.
|
||||
|
@ -631,13 +584,10 @@ class ChartEditorDialogHandler
|
|||
{
|
||||
var charIdsForVocals:Array<String> = [];
|
||||
|
||||
for (charKey in state.currentSongMetadata.playData.playableChars.keys())
|
||||
{
|
||||
var charData:Null<SongPlayableChar> = state.currentSongMetadata.playData.playableChars.get(charKey);
|
||||
if (charData == null) continue;
|
||||
charIdsForVocals.push(charKey);
|
||||
if (charData.opponent != null) charIdsForVocals.push(charData.opponent);
|
||||
}
|
||||
var charData:SongCharacterData = state.currentSongMetadata.playData.characters;
|
||||
|
||||
charIdsForVocals.push(charData.player);
|
||||
charIdsForVocals.push(charData.opponent);
|
||||
|
||||
var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_UPLOAD_VOCALS_LAYOUT, true, closable);
|
||||
if (dialog == null) throw 'Could not locate Upload Vocals dialog';
|
||||
|
@ -678,7 +628,7 @@ class ChartEditorDialogHandler
|
|||
trace('Selected file: $pathStr');
|
||||
var path:Path = new Path(pathStr);
|
||||
|
||||
if (state.loadVocalsFromPath(path, charKey))
|
||||
if (ChartEditorAudioHandler.loadVocalsFromPath(state, path, charKey))
|
||||
{
|
||||
// Tell the user the load was successful.
|
||||
#if !mac
|
||||
|
@ -740,7 +690,7 @@ class ChartEditorDialogHandler
|
|||
#else
|
||||
vocalsEntryLabel.text = 'Vocals for $charName (click to browse)\n${selectedFile.name}';
|
||||
#end
|
||||
state.loadVocalsFromBytes(selectedFile.bytes, charKey);
|
||||
ChartEditorAudioHandler.loadVocalsFromBytes(state, selectedFile.bytes, charKey);
|
||||
dialogNoVocals.hidden = true;
|
||||
removeDropHandler(onDropFile);
|
||||
}
|
||||
|
@ -793,7 +743,7 @@ class ChartEditorDialogHandler
|
|||
var buttonContinue:Null<Button> = dialog.findComponent('dialogContinue', Button);
|
||||
if (buttonContinue == null) throw 'Could not locate dialogContinue button in Open Chart dialog';
|
||||
buttonContinue.onClick = function(_event) {
|
||||
state.loadSong(songMetadata, songChartData);
|
||||
ChartEditorImportExportHandler.loadSong(state, songMetadata, songChartData);
|
||||
|
||||
dialog.hideDialog(DialogButton.APPLY);
|
||||
}
|
||||
|
@ -880,9 +830,26 @@ class ChartEditorDialogHandler
|
|||
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');
|
||||
var songMetadataTxt:String = FileUtil.readStringFromPath(path.toString());
|
||||
|
||||
var songMetadataVersion:Null<Version> = VersionUtil.getVersionFromJSON(songMetadataTxt);
|
||||
if (songMetadataVersion == null)
|
||||
{
|
||||
// Tell the user the load was not successful.
|
||||
#if !mac
|
||||
NotificationManager.instance.addNotification(
|
||||
{
|
||||
title: 'Failure',
|
||||
body: 'Could not parse metadata file version (${path.file}.${path.ext})',
|
||||
type: NotificationType.Error,
|
||||
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
|
||||
});
|
||||
#end
|
||||
return;
|
||||
}
|
||||
|
||||
var songMetadataVariation:Null<SongMetadata> = SongRegistry.instance.parseEntryMetadataRawWithMigration(songMetadataTxt, path.toString(),
|
||||
songMetadataVersion);
|
||||
|
||||
if (songMetadataVariation == null)
|
||||
{
|
||||
|
@ -928,31 +895,63 @@ class ChartEditorDialogHandler
|
|||
{
|
||||
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;
|
||||
var songMetadataTxt:String = selectedFile.bytes.toString();
|
||||
|
||||
songMetadata.set(variation, songMetadataVariation);
|
||||
var songMetadataVersion:Null<Version> = VersionUtil.getVersionFromJSON(songMetadataTxt);
|
||||
if (songMetadataVersion == null)
|
||||
{
|
||||
// Tell the user the load was not successful.
|
||||
#if !mac
|
||||
NotificationManager.instance.addNotification(
|
||||
{
|
||||
title: 'Failure',
|
||||
body: 'Could not parse metadata file version (${selectedFile.name})',
|
||||
type: NotificationType.Error,
|
||||
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
|
||||
});
|
||||
#end
|
||||
return;
|
||||
}
|
||||
|
||||
// Tell the user the load was successful.
|
||||
#if !mac
|
||||
NotificationManager.instance.addNotification(
|
||||
{
|
||||
title: 'Success',
|
||||
body: 'Loaded metadata file (${selectedFile.name})',
|
||||
type: NotificationType.Success,
|
||||
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
|
||||
});
|
||||
#end
|
||||
var songMetadataVariation:Null<SongMetadata> = SongRegistry.instance.parseEntryMetadataRawWithMigration(songMetadataTxt, selectedFile.name,
|
||||
songMetadataVersion);
|
||||
|
||||
#if FILE_DROP_SUPPORTED
|
||||
label.text = 'Metadata file (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}';
|
||||
#else
|
||||
label.text = 'Metadata file (click to browse)\n${selectedFile.name}';
|
||||
#end
|
||||
if (songMetadataVariation != null)
|
||||
{
|
||||
songMetadata.set(variation, songMetadataVariation);
|
||||
|
||||
if (variation == Constants.DEFAULT_VARIATION) constructVariationEntries(songMetadataVariation.playData.songVariations);
|
||||
// Tell the user the load was successful.
|
||||
#if !mac
|
||||
NotificationManager.instance.addNotification(
|
||||
{
|
||||
title: 'Success',
|
||||
body: 'Loaded metadata file (${selectedFile.name})',
|
||||
type: NotificationType.Success,
|
||||
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
|
||||
});
|
||||
#end
|
||||
|
||||
#if FILE_DROP_SUPPORTED
|
||||
label.text = 'Metadata file (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}';
|
||||
#else
|
||||
label.text = 'Metadata file (click to browse)\n${selectedFile.name}';
|
||||
#end
|
||||
|
||||
if (variation == Constants.DEFAULT_VARIATION) constructVariationEntries(songMetadataVariation.playData.songVariations);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Tell the user the load was unsuccessful.
|
||||
#if !mac
|
||||
NotificationManager.instance.addNotification(
|
||||
{
|
||||
title: 'Failure',
|
||||
body: 'Failed to load metadata file (${selectedFile.name})',
|
||||
type: NotificationType.Error,
|
||||
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
|
||||
});
|
||||
#end
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -961,31 +960,64 @@ class ChartEditorDialogHandler
|
|||
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');
|
||||
var songChartDataTxt:String = FileUtil.readStringFromPath(path.toString());
|
||||
|
||||
songChartData.set(variation, songChartDataVariation);
|
||||
state.notePreviewDirty = true;
|
||||
state.notePreviewViewportBoundsDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
var songChartDataVersion:Null<Version> = VersionUtil.getVersionFromJSON(songChartDataTxt);
|
||||
if (songChartDataVersion == null)
|
||||
{
|
||||
// Tell the user the load was not successful.
|
||||
#if !mac
|
||||
NotificationManager.instance.addNotification(
|
||||
{
|
||||
title: 'Failure',
|
||||
body: 'Could not parse chart data file version (${path.file}.${path.ext})',
|
||||
type: NotificationType.Error,
|
||||
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
|
||||
});
|
||||
#end
|
||||
return;
|
||||
}
|
||||
|
||||
// Tell the user the load was successful.
|
||||
#if !mac
|
||||
NotificationManager.instance.addNotification(
|
||||
{
|
||||
title: 'Success',
|
||||
body: 'Loaded chart data file (${path.file}.${path.ext})',
|
||||
type: NotificationType.Success,
|
||||
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
|
||||
});
|
||||
#end
|
||||
var songChartDataVariation:Null<SongChartData> = SongRegistry.instance.parseEntryChartDataRawWithMigration(songChartDataTxt, path.toString(),
|
||||
songChartDataVersion);
|
||||
|
||||
#if FILE_DROP_SUPPORTED
|
||||
label.text = 'Chart data file (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}';
|
||||
#else
|
||||
label.text = 'Chart data file (click to browse)\n${path.file}.${path.ext}';
|
||||
#end
|
||||
if (songChartDataVariation != null)
|
||||
{
|
||||
songChartData.set(variation, songChartDataVariation);
|
||||
state.notePreviewDirty = true;
|
||||
state.notePreviewViewportBoundsDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
|
||||
// Tell the user the load was successful.
|
||||
#if !mac
|
||||
NotificationManager.instance.addNotification(
|
||||
{
|
||||
title: 'Success',
|
||||
body: 'Loaded chart data file (${path.file}.${path.ext})',
|
||||
type: NotificationType.Success,
|
||||
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
|
||||
});
|
||||
#end
|
||||
|
||||
#if FILE_DROP_SUPPORTED
|
||||
label.text = 'Chart data file (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}';
|
||||
#else
|
||||
label.text = 'Chart data file (click to browse)\n${path.file}.${path.ext}';
|
||||
#end
|
||||
}
|
||||
else
|
||||
{
|
||||
// Tell the user the load was unsuccessful.
|
||||
#if !mac
|
||||
NotificationManager.instance.addNotification(
|
||||
{
|
||||
title: 'Failure',
|
||||
body: 'Failed to load chart data file (${path.file}.${path.ext})',
|
||||
type: NotificationType.Error,
|
||||
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
|
||||
});
|
||||
#end
|
||||
}
|
||||
};
|
||||
|
||||
onClickChartDataVariation = function(variation:String, label:Label, _event:UIEvent) {
|
||||
|
@ -995,31 +1027,51 @@ class ChartEditorDialogHandler
|
|||
{
|
||||
trace('Selected file: ' + selectedFile.name);
|
||||
|
||||
var songChartDataJson:Dynamic = SerializerUtil.fromJSONBytes(selectedFile.bytes);
|
||||
var songChartDataVariation:SongChartData = SongMigrator.migrateSongChartData(songChartDataJson, 'import');
|
||||
songChartDataVariation = SongValidator.validateSongChartData(songChartDataVariation, 'import');
|
||||
var songChartDataTxt:String = selectedFile.bytes.toString();
|
||||
|
||||
songChartData.set(variation, songChartDataVariation);
|
||||
state.notePreviewDirty = true;
|
||||
state.notePreviewViewportBoundsDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
var songChartDataVersion:Null<Version> = VersionUtil.getVersionFromJSON(songChartDataTxt);
|
||||
if (songChartDataVersion == null)
|
||||
{
|
||||
// Tell the user the load was not successful.
|
||||
#if !mac
|
||||
NotificationManager.instance.addNotification(
|
||||
{
|
||||
title: 'Failure',
|
||||
body: 'Could not parse chart data file version (${selectedFile.name})',
|
||||
type: NotificationType.Error,
|
||||
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
|
||||
});
|
||||
#end
|
||||
return;
|
||||
}
|
||||
|
||||
// Tell the user the load was successful.
|
||||
#if !mac
|
||||
NotificationManager.instance.addNotification(
|
||||
{
|
||||
title: 'Success',
|
||||
body: 'Loaded chart data file (${selectedFile.name})',
|
||||
type: NotificationType.Success,
|
||||
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
|
||||
});
|
||||
#end
|
||||
var songChartDataVariation:Null<SongChartData> = SongRegistry.instance.parseEntryChartDataRawWithMigration(songChartDataTxt, selectedFile.name,
|
||||
songChartDataVersion);
|
||||
|
||||
#if FILE_DROP_SUPPORTED
|
||||
label.text = 'Chart data file (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}';
|
||||
#else
|
||||
label.text = 'Chart data file (click to browse)\n${selectedFile.name}';
|
||||
#end
|
||||
if (songChartDataVariation != null)
|
||||
{
|
||||
songChartData.set(variation, songChartDataVariation);
|
||||
state.notePreviewDirty = true;
|
||||
state.notePreviewViewportBoundsDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
|
||||
// Tell the user the load was successful.
|
||||
#if !mac
|
||||
NotificationManager.instance.addNotification(
|
||||
{
|
||||
title: 'Success',
|
||||
body: 'Loaded chart data file (${selectedFile.name})',
|
||||
type: NotificationType.Success,
|
||||
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
|
||||
});
|
||||
#end
|
||||
|
||||
#if FILE_DROP_SUPPORTED
|
||||
label.text = 'Chart data file (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}';
|
||||
#else
|
||||
label.text = 'Chart data file (click to browse)\n${selectedFile.name}';
|
||||
#end
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -1102,11 +1154,27 @@ class ChartEditorDialogHandler
|
|||
if (selectedFile != null && selectedFile.bytes != 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);
|
||||
var selectedFileTxt:String = selectedFile.bytes.toString();
|
||||
var fnfLegacyData:Null<FNFLegacyData> = FNFLegacyImporter.parseLegacyDataRaw(selectedFileTxt, selectedFile.fullPath);
|
||||
|
||||
state.loadSong([Constants.DEFAULT_VARIATION => songMetadata], [Constants.DEFAULT_VARIATION => songChartData]);
|
||||
if (fnfLegacyData == null)
|
||||
{
|
||||
#if !mac
|
||||
NotificationManager.instance.addNotification(
|
||||
{
|
||||
title: 'Failure',
|
||||
body: 'Failed to parse FNF chart file (${selectedFile.name})',
|
||||
type: NotificationType.Error,
|
||||
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
|
||||
});
|
||||
#end
|
||||
return;
|
||||
}
|
||||
|
||||
var songMetadata:SongMetadata = FNFLegacyImporter.migrateMetadata(fnfLegacyData);
|
||||
var songChartData:SongChartData = FNFLegacyImporter.migrateChartData(fnfLegacyData);
|
||||
|
||||
ChartEditorImportExportHandler.loadSong(state, [Constants.DEFAULT_VARIATION => songMetadata], [Constants.DEFAULT_VARIATION => songChartData]);
|
||||
|
||||
dialog.hideDialog(DialogButton.APPLY);
|
||||
#if !mac
|
||||
|
@ -1124,11 +1192,12 @@ class ChartEditorDialogHandler
|
|||
|
||||
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);
|
||||
var selectedFileText:String = FileUtil.readStringFromPath(path.toString());
|
||||
var selectedFileData:FNFLegacyData = FNFLegacyImporter.parseLegacyDataRaw(selectedFileText, path.toString());
|
||||
var songMetadata:SongMetadata = FNFLegacyImporter.migrateMetadata(selectedFileData);
|
||||
var songChartData:SongChartData = FNFLegacyImporter.migrateChartData(selectedFileData);
|
||||
|
||||
state.loadSong([Constants.DEFAULT_VARIATION => songMetadata], [Constants.DEFAULT_VARIATION => songChartData]);
|
||||
ChartEditorImportExportHandler.loadSong(state, [Constants.DEFAULT_VARIATION => songMetadata], [Constants.DEFAULT_VARIATION => songChartData]);
|
||||
|
||||
dialog.hideDialog(DialogButton.APPLY);
|
||||
#if !mac
|
||||
|
@ -1181,4 +1250,161 @@ class ChartEditorDialogHandler
|
|||
|
||||
return dialog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and opens a dialog where the user can add a new variation 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.
|
||||
*/
|
||||
public static function openAddVariationDialog(state:ChartEditorState, closable:Bool = true):Dialog
|
||||
{
|
||||
var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_ADD_VARIATION_LAYOUT, true, false);
|
||||
if (dialog == null) throw 'Could not locate Add Variation dialog';
|
||||
|
||||
var variationForm:Null<Form> = dialog.findComponent('variationForm', Form);
|
||||
if (variationForm == null) throw 'Could not locate variationForm Form in Add Variation dialog';
|
||||
|
||||
var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button);
|
||||
if (buttonCancel == null) throw 'Could not locate dialogCancel button in Add Variation dialog';
|
||||
buttonCancel.onClick = function(_event) {
|
||||
dialog.hideDialog(DialogButton.CANCEL);
|
||||
}
|
||||
|
||||
var buttonAdd:Null<Button> = dialog.findComponent('dialogAdd', Button);
|
||||
if (buttonAdd == null) throw 'Could not locate dialogAdd button in Add Variation dialog';
|
||||
buttonAdd.onClick = function(_event) {
|
||||
// This performs validation before the onSubmit callback is called.
|
||||
variationForm.submit();
|
||||
}
|
||||
|
||||
var dialogSongName:Null<TextField> = dialog.findComponent('dialogSongName', TextField);
|
||||
if (dialogSongName == null) throw 'Could not locate dialogSongName TextField in Add Variation dialog';
|
||||
dialogSongName.value = state.currentSongMetadata.songName;
|
||||
|
||||
var dialogSongArtist:Null<TextField> = dialog.findComponent('dialogSongArtist', TextField);
|
||||
if (dialogSongArtist == null) throw 'Could not locate dialogSongArtist TextField in Add Variation dialog';
|
||||
dialogSongArtist.value = state.currentSongMetadata.artist;
|
||||
|
||||
var dialogStage:Null<DropDown> = dialog.findComponent('dialogStage', DropDown);
|
||||
if (dialogStage == null) throw 'Could not locate dialogStage DropDown in Add Variation dialog';
|
||||
var startingValueStage = ChartEditorDropdowns.populateDropdownWithStages(dialogStage, state.currentSongMetadata.playData.stage);
|
||||
dialogStage.value = startingValueStage;
|
||||
|
||||
var dialogNoteStyle:Null<DropDown> = dialog.findComponent('dialogNoteStyle', DropDown);
|
||||
if (dialogNoteStyle == null) throw 'Could not locate dialogNoteStyle DropDown in Add Variation dialog';
|
||||
dialogNoteStyle.value = state.currentSongMetadata.playData.noteSkin;
|
||||
|
||||
var dialogCharacterPlayer:Null<DropDown> = dialog.findComponent('dialogCharacterPlayer', DropDown);
|
||||
if (dialogCharacterPlayer == null) throw 'Could not locate dialogCharacterPlayer DropDown in Add Variation dialog';
|
||||
dialogCharacterPlayer.value = ChartEditorDropdowns.populateDropdownWithCharacters(dialogCharacterPlayer, CharacterType.BF,
|
||||
state.currentSongMetadata.playData.characters.player);
|
||||
|
||||
var dialogCharacterOpponent:Null<DropDown> = dialog.findComponent('dialogCharacterOpponent', DropDown);
|
||||
if (dialogCharacterOpponent == null) throw 'Could not locate dialogCharacterOpponent DropDown in Add Variation dialog';
|
||||
dialogCharacterOpponent.value = ChartEditorDropdowns.populateDropdownWithCharacters(dialogCharacterOpponent, CharacterType.DAD,
|
||||
state.currentSongMetadata.playData.characters.opponent);
|
||||
|
||||
var dialogCharacterGirlfriend:Null<DropDown> = dialog.findComponent('dialogCharacterGirlfriend', DropDown);
|
||||
if (dialogCharacterGirlfriend == null) throw 'Could not locate dialogCharacterGirlfriend DropDown in Add Variation dialog';
|
||||
dialogCharacterGirlfriend.value = ChartEditorDropdowns.populateDropdownWithCharacters(dialogCharacterGirlfriend, CharacterType.GF,
|
||||
state.currentSongMetadata.playData.characters.girlfriend);
|
||||
|
||||
var dialogBPM:Null<NumberStepper> = dialog.findComponent('dialogBPM', NumberStepper);
|
||||
if (dialogBPM == null) throw 'Could not locate dialogBPM NumberStepper in Add Variation dialog';
|
||||
dialogBPM.value = state.currentSongMetadata.timeChanges[0].bpm;
|
||||
|
||||
// If all validators succeeded, this callback is called.
|
||||
|
||||
variationForm.onSubmit = function(_event) {
|
||||
trace('Add Variation dialog submitted, validation succeeded!');
|
||||
|
||||
var dialogVariationName:Null<TextField> = dialog.findComponent('dialogVariationName', TextField);
|
||||
if (dialogVariationName == null) throw 'Could not locate dialogVariationName TextField in Add Variation dialog';
|
||||
|
||||
var pendingVariation:SongMetadata = new SongMetadata(dialogSongName.text, dialogSongArtist.text, dialogVariationName.text.toLowerCase());
|
||||
|
||||
pendingVariation.playData.stage = dialogStage.value.id;
|
||||
pendingVariation.playData.noteSkin = dialogNoteStyle.value;
|
||||
pendingVariation.timeChanges[0].bpm = dialogBPM.value;
|
||||
|
||||
state.songMetadata.set(pendingVariation.variation, pendingVariation);
|
||||
state.difficultySelectDirty = true; // Force the Difficulty toolbox to update.
|
||||
#if !mac
|
||||
NotificationManager.instance.addNotification(
|
||||
{
|
||||
title: "Add Variation",
|
||||
body: 'Added new variation "${pendingVariation.variation}"',
|
||||
type: NotificationType.Success
|
||||
});
|
||||
#end
|
||||
dialog.hideDialog(DialogButton.APPLY);
|
||||
}
|
||||
|
||||
return dialog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and opens a dialog where the user can add a new difficulty 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.
|
||||
*/
|
||||
public static function openAddDifficultyDialog(state:ChartEditorState, closable:Bool = true):Dialog
|
||||
{
|
||||
var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_ADD_DIFFICULTY_LAYOUT, true, false);
|
||||
if (dialog == null) throw 'Could not locate Add Difficulty dialog';
|
||||
|
||||
var difficultyForm:Null<Form> = dialog.findComponent('difficultyForm', Form);
|
||||
if (difficultyForm == null) throw 'Could not locate difficultyForm Form in Add Difficulty dialog';
|
||||
|
||||
var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button);
|
||||
if (buttonCancel == null) throw 'Could not locate dialogCancel button in Add Difficulty dialog';
|
||||
buttonCancel.onClick = function(_event) {
|
||||
dialog.hideDialog(DialogButton.CANCEL);
|
||||
}
|
||||
|
||||
var buttonAdd:Null<Button> = dialog.findComponent('dialogAdd', Button);
|
||||
if (buttonAdd == null) throw 'Could not locate dialogAdd button in Add Difficulty dialog';
|
||||
buttonAdd.onClick = function(_event) {
|
||||
// This performs validation before the onSubmit callback is called.
|
||||
difficultyForm.submit();
|
||||
}
|
||||
|
||||
var dialogVariation:Null<DropDown> = dialog.findComponent('dialogVariation', DropDown);
|
||||
if (dialogVariation == null) throw 'Could not locate dialogVariation DropDown in Add Variation dialog';
|
||||
dialogVariation.value = ChartEditorDropdowns.populateDropdownWithVariations(dialogVariation, state, true);
|
||||
|
||||
var labelScrollSpeed:Null<Label> = dialog.findComponent('labelScrollSpeed', Label);
|
||||
if (labelScrollSpeed == null) throw 'Could not find labelScrollSpeed component.';
|
||||
|
||||
var inputScrollSpeed:Null<Slider> = dialog.findComponent('inputScrollSpeed', Slider);
|
||||
if (inputScrollSpeed == null) throw 'Could not find inputScrollSpeed component.';
|
||||
inputScrollSpeed.onChange = function(event:UIEvent) {
|
||||
labelScrollSpeed.text = 'Scroll Speed: ${inputScrollSpeed.value}x';
|
||||
};
|
||||
inputScrollSpeed.value = state.currentSongChartScrollSpeed;
|
||||
labelScrollSpeed.text = 'Scroll Speed: ${inputScrollSpeed.value}x';
|
||||
|
||||
difficultyForm.onSubmit = function(_event) {
|
||||
trace('Add Difficulty dialog submitted, validation succeeded!');
|
||||
|
||||
var dialogDifficultyName:Null<TextField> = dialog.findComponent('dialogDifficultyName', TextField);
|
||||
if (dialogDifficultyName == null) throw 'Could not locate dialogDifficultyName TextField in Add Difficulty dialog';
|
||||
|
||||
state.createDifficulty(dialogVariation.value.id, dialogDifficultyName.text.toLowerCase(), inputScrollSpeed.value ?? 1.0);
|
||||
|
||||
#if !mac
|
||||
NotificationManager.instance.addNotification(
|
||||
{
|
||||
title: "Add Difficulty",
|
||||
body: 'Added new difficulty "${dialogDifficultyName.text.toLowerCase()}"',
|
||||
type: NotificationType.Success
|
||||
});
|
||||
#end
|
||||
dialog.hideDialog(DialogButton.APPLY);
|
||||
}
|
||||
|
||||
return dialog;
|
||||
}
|
||||
}
|
||||
|
|
129
source/funkin/ui/debug/charting/ChartEditorDropdowns.hx
Normal file
129
source/funkin/ui/debug/charting/ChartEditorDropdowns.hx
Normal file
|
@ -0,0 +1,129 @@
|
|||
package funkin.ui.debug.charting;
|
||||
|
||||
import funkin.data.notestyle.NoteStyleRegistry;
|
||||
import funkin.play.notes.notestyle.NoteStyle;
|
||||
import funkin.play.stage.StageData;
|
||||
import funkin.play.stage.StageData.StageDataParser;
|
||||
import funkin.play.character.CharacterData;
|
||||
import haxe.ui.components.DropDown;
|
||||
import funkin.play.character.BaseCharacter.CharacterType;
|
||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||
|
||||
/**
|
||||
* This class contains functions for populating dropdowns based on game data.
|
||||
* These get used by both dialogs and toolboxes so they're in their own class to prevent "reaching over."
|
||||
*/
|
||||
@:nullSafety
|
||||
@:access(ChartEditorState)
|
||||
class ChartEditorDropdowns
|
||||
{
|
||||
public static function populateDropdownWithCharacters(dropDown:DropDown, charType:CharacterType, startingCharId:String):DropDownEntry
|
||||
{
|
||||
dropDown.dataSource.clear();
|
||||
|
||||
// TODO: Filter based on charType.
|
||||
var charIds:Array<String> = CharacterDataParser.listCharacterIds();
|
||||
|
||||
var returnValue:DropDownEntry = switch (charType)
|
||||
{
|
||||
case BF: {id: "bf", text: "Boyfriend"};
|
||||
case DAD: {id: "dad", text: "Daddy Dearest"};
|
||||
default: {
|
||||
dropDown.dataSource.add({id: "none", text: ""});
|
||||
{id: "none", text: "None"};
|
||||
}
|
||||
}
|
||||
|
||||
for (charId in charIds)
|
||||
{
|
||||
var character:Null<CharacterData> = CharacterDataParser.fetchCharacterData(charId);
|
||||
if (character == null) continue;
|
||||
|
||||
var value = {id: charId, text: character.name};
|
||||
if (startingCharId == charId) returnValue = value;
|
||||
|
||||
dropDown.dataSource.add(value);
|
||||
}
|
||||
|
||||
dropDown.dataSource.sort('text', ASCENDING);
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
public static function populateDropdownWithStages(dropDown:DropDown, startingStageId:String):DropDownEntry
|
||||
{
|
||||
dropDown.dataSource.clear();
|
||||
|
||||
var stageIds:Array<String> = StageDataParser.listStageIds();
|
||||
|
||||
var returnValue:DropDownEntry = {id: "mainStage", text: "Main Stage"};
|
||||
|
||||
for (stageId in stageIds)
|
||||
{
|
||||
var stage:Null<StageData> = StageDataParser.parseStageData(stageId);
|
||||
if (stage == null) continue;
|
||||
|
||||
var value = {id: stageId, text: stage.name};
|
||||
if (startingStageId == stageId) returnValue = value;
|
||||
|
||||
dropDown.dataSource.add(value);
|
||||
}
|
||||
|
||||
dropDown.dataSource.sort('text', ASCENDING);
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
public static function populateDropdownWithNoteStyles(dropDown:DropDown, startingStyleId:String):DropDownEntry
|
||||
{
|
||||
dropDown.dataSource.clear();
|
||||
|
||||
var noteStyleIds:Array<String> = NoteStyleRegistry.instance.listEntryIds();
|
||||
|
||||
var returnValue:DropDownEntry = {id: "funkin", text: "Funkin'"};
|
||||
|
||||
for (noteStyleId in noteStyleIds)
|
||||
{
|
||||
var noteStyle:Null<NoteStyle> = NoteStyleRegistry.instance.fetchEntry(noteStyleId);
|
||||
if (noteStyle == null) continue;
|
||||
|
||||
var value = {id: noteStyleId, text: noteStyle.getName()};
|
||||
if (startingStyleId == noteStyleId) returnValue = value;
|
||||
|
||||
dropDown.dataSource.add(value);
|
||||
}
|
||||
|
||||
dropDown.dataSource.sort('text', ASCENDING);
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
public static function populateDropdownWithVariations(dropDown:DropDown, state:ChartEditorState, includeNone:Bool = true):DropDownEntry
|
||||
{
|
||||
dropDown.dataSource.clear();
|
||||
|
||||
var variationIds:Array<String> = state.availableVariations;
|
||||
|
||||
if (includeNone)
|
||||
{
|
||||
dropDown.dataSource.add({id: "none", text: ""});
|
||||
}
|
||||
|
||||
var returnValue:DropDownEntry = includeNone ? ({id: "none", text: ""}) : ({id: "default", text: "Default"});
|
||||
|
||||
for (variationId in variationIds)
|
||||
{
|
||||
dropDown.dataSource.add({id: variationId, text: variationId.toTitleCase()});
|
||||
}
|
||||
|
||||
dropDown.dataSource.sort('text', ASCENDING);
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
}
|
||||
|
||||
typedef DropDownEntry =
|
||||
{
|
||||
id:String,
|
||||
text:String
|
||||
};
|
|
@ -0,0 +1,195 @@
|
|||
package funkin.ui.debug.charting;
|
||||
|
||||
import haxe.ui.notifications.NotificationType;
|
||||
import funkin.util.DateUtil;
|
||||
import haxe.io.Path;
|
||||
import funkin.util.SerializerUtil;
|
||||
import haxe.ui.notifications.NotificationManager;
|
||||
import funkin.util.FileUtil;
|
||||
import funkin.util.FileUtil;
|
||||
import funkin.play.song.Song;
|
||||
import funkin.data.song.SongData.SongChartData;
|
||||
import funkin.data.song.SongData.SongMetadata;
|
||||
import funkin.data.song.SongRegistry;
|
||||
|
||||
/**
|
||||
* Contains functions for importing, loading, saving, and exporting charts.
|
||||
*/
|
||||
@:nullSafety
|
||||
@:allow(funkin.ui.debug.charting.ChartEditorState)
|
||||
class ChartEditorImportExportHandler
|
||||
{
|
||||
/**
|
||||
* Fetch's a song's existing chart and audio and loads it, replacing the current song.
|
||||
*/
|
||||
public static function loadSongAsTemplate(state:ChartEditorState, songId:String):Void
|
||||
{
|
||||
var song:Null<Song> = SongRegistry.instance.fetchEntry(songId);
|
||||
|
||||
if (song == null) return;
|
||||
|
||||
// Load the song metadata.
|
||||
var rawSongMetadata:Array<SongMetadata> = song.getRawMetadata();
|
||||
var songMetadata:Map<String, SongMetadata> = [];
|
||||
var songChartData:Map<String, SongChartData> = [];
|
||||
|
||||
for (metadata in rawSongMetadata)
|
||||
{
|
||||
if (metadata == null) continue;
|
||||
var variation = (metadata.variation == null || metadata.variation == '') ? 'default' : metadata.variation;
|
||||
|
||||
// Clone to prevent modifying the original.
|
||||
var metadataClone:SongMetadata = metadata.clone(variation);
|
||||
if (metadataClone != null) songMetadata.set(variation, metadataClone);
|
||||
|
||||
var chartData:Null<SongChartData> = SongRegistry.instance.parseEntryChartData(songId, metadata.variation);
|
||||
if (chartData != null) songChartData.set(variation, chartData);
|
||||
}
|
||||
|
||||
loadSong(state, songMetadata, songChartData);
|
||||
|
||||
state.sortChartData();
|
||||
|
||||
state.clearVocals();
|
||||
|
||||
ChartEditorAudioHandler.loadInstrumentalFromAsset(state, Paths.inst(songId));
|
||||
|
||||
var diff:Null<SongDifficulty> = song.getDifficulty(state.selectedDifficulty);
|
||||
var voiceList:Array<String> = diff != null ? diff.buildVoiceList() : [];
|
||||
if (voiceList.length == 2)
|
||||
{
|
||||
ChartEditorAudioHandler.loadVocalsFromAsset(state, voiceList[0], BF);
|
||||
ChartEditorAudioHandler.loadVocalsFromAsset(state, voiceList[1], DAD);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (voicePath in voiceList)
|
||||
{
|
||||
ChartEditorAudioHandler.loadVocalsFromAsset(state, voicePath);
|
||||
}
|
||||
}
|
||||
|
||||
state.refreshMetadataToolbox();
|
||||
|
||||
#if !mac
|
||||
NotificationManager.instance.addNotification(
|
||||
{
|
||||
title: 'Success',
|
||||
body: 'Loaded song (${rawSongMetadata[0].songName})',
|
||||
type: NotificationType.Success,
|
||||
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
|
||||
});
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 static function loadSong(state:ChartEditorState, newSongMetadata:Map<String, SongMetadata>, newSongChartData:Map<String, SongChartData>):Void
|
||||
{
|
||||
state.songMetadata = newSongMetadata;
|
||||
state.songChartData = newSongChartData;
|
||||
|
||||
Conductor.forceBPM(null); // Disable the forced BPM.
|
||||
Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges);
|
||||
|
||||
state.notePreviewDirty = true;
|
||||
state.notePreviewViewportBoundsDirty = true;
|
||||
state.difficultySelectDirty = true;
|
||||
state.opponentPreviewDirty = true;
|
||||
state.playerPreviewDirty = true;
|
||||
|
||||
// Remove instrumental and vocal tracks, they will be loaded next.
|
||||
if (state.audioInstTrack != null)
|
||||
{
|
||||
state.audioInstTrack.stop();
|
||||
state.audioInstTrack = null;
|
||||
}
|
||||
if (state.audioVocalTrackGroup != null)
|
||||
{
|
||||
state.audioVocalTrackGroup.stop();
|
||||
state.audioVocalTrackGroup.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param force Whether to force the export without prompting the user for a file location.
|
||||
* @param tmp If true, save to the temporary directory instead of the local `backup` directory.
|
||||
*/
|
||||
public static function exportAllSongData(state:ChartEditorState, force:Bool = false, tmp:Bool = false):Void
|
||||
{
|
||||
var zipEntries:Array<haxe.zip.Entry> = [];
|
||||
|
||||
for (variation in state.availableVariations)
|
||||
{
|
||||
var variationId:String = variation;
|
||||
if (variation == '' || variation == 'default' || variation == 'normal')
|
||||
{
|
||||
variationId = '';
|
||||
}
|
||||
|
||||
if (variationId == '')
|
||||
{
|
||||
var variationMetadata:Null<SongMetadata> = state.songMetadata.get(variation);
|
||||
if (variationMetadata != null) zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-metadata.json', SerializerUtil.toJSON(variationMetadata)));
|
||||
var variationChart:Null<SongChartData> = state.songChartData.get(variation);
|
||||
if (variationChart != null) zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-chart.json', SerializerUtil.toJSON(variationChart)));
|
||||
}
|
||||
else
|
||||
{
|
||||
var variationMetadata:Null<SongMetadata> = state.songMetadata.get(variation);
|
||||
if (variationMetadata != null) zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-metadata-$variationId.json',
|
||||
SerializerUtil.toJSON(variationMetadata)));
|
||||
var variationChart:Null<SongChartData> = state.songChartData.get(variation);
|
||||
if (variationChart != null) zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-chart-$variationId.json',
|
||||
SerializerUtil.toJSON(variationChart)));
|
||||
}
|
||||
}
|
||||
|
||||
if (state.audioInstTrackData != null) zipEntries.push(FileUtil.makeZIPEntryFromBytes('Inst.ogg', state.audioInstTrackData));
|
||||
for (charId in state.audioVocalTrackData.keys())
|
||||
{
|
||||
var entryData = state.audioVocalTrackData.get(charId);
|
||||
if (entryData == null) continue;
|
||||
zipEntries.push(FileUtil.makeZIPEntryFromBytes('Vocals-$charId.ogg', entryData));
|
||||
}
|
||||
|
||||
trace('Exporting ${zipEntries.length} files to ZIP...');
|
||||
|
||||
if (force)
|
||||
{
|
||||
var targetPath:String = if (tmp)
|
||||
{
|
||||
Path.join([
|
||||
FileUtil.getTempDir(),
|
||||
'chart-editor-exit-${DateUtil.generateTimestamp()}.${Constants.EXT_CHART}'
|
||||
]);
|
||||
}
|
||||
else
|
||||
{
|
||||
Path.join([
|
||||
'./backups/',
|
||||
'chart-editor-exit-${DateUtil.generateTimestamp()}.${Constants.EXT_CHART}'
|
||||
]);
|
||||
}
|
||||
|
||||
// We have to force write because the program will die before the save dialog is closed.
|
||||
trace('Force exporting to $targetPath...');
|
||||
FileUtil.saveFilesAsZIPToPath(zipEntries, targetPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prompt and save.
|
||||
var onSave:Array<String>->Void = function(paths:Array<String>) {
|
||||
trace('Successfully exported files.');
|
||||
};
|
||||
|
||||
var onCancel:Void->Void = function() {
|
||||
trace('Export cancelled.');
|
||||
};
|
||||
|
||||
FileUtil.saveMultipleFiles(zipEntries, onSave, onCancel, '${state.currentSongId}-chart.${Constants.EXT_CHART}');
|
||||
}
|
||||
}
|
|
@ -19,7 +19,7 @@ class ChartEditorNoteSprite extends FlxSprite
|
|||
/**
|
||||
* The list of available note skin to validate against.
|
||||
*/
|
||||
public static final NOTE_STYLES:Array<String> = ['Normal', 'Pixel'];
|
||||
public static final NOTE_STYLES:Array<String> = ['funkin', 'pixel'];
|
||||
|
||||
/**
|
||||
* The ChartEditorState this note belongs to.
|
||||
|
@ -54,20 +54,20 @@ class ChartEditorNoteSprite extends FlxSprite
|
|||
|
||||
// Initialize all the animations, not just the one we're going to use immediately,
|
||||
// so that later we can reuse the sprite without having to initialize more animations during scrolling.
|
||||
this.animation.addByPrefix('tapLeftNormal', 'purple instance');
|
||||
this.animation.addByPrefix('tapDownNormal', 'blue instance');
|
||||
this.animation.addByPrefix('tapUpNormal', 'green instance');
|
||||
this.animation.addByPrefix('tapRightNormal', 'red instance');
|
||||
this.animation.addByPrefix('tapLeftFunkin', 'purple instance');
|
||||
this.animation.addByPrefix('tapDownFunkin', 'blue instance');
|
||||
this.animation.addByPrefix('tapUpFunkin', 'green instance');
|
||||
this.animation.addByPrefix('tapRightFunkin', 'red instance');
|
||||
|
||||
this.animation.addByPrefix('holdLeftNormal', 'LeftHoldPiece');
|
||||
this.animation.addByPrefix('holdDownNormal', 'DownHoldPiece');
|
||||
this.animation.addByPrefix('holdUpNormal', 'UpHoldPiece');
|
||||
this.animation.addByPrefix('holdRightNormal', 'RightHoldPiece');
|
||||
this.animation.addByPrefix('holdLeftFunkin', 'LeftHoldPiece');
|
||||
this.animation.addByPrefix('holdDownFunkin', 'DownHoldPiece');
|
||||
this.animation.addByPrefix('holdUpFunkin', 'UpHoldPiece');
|
||||
this.animation.addByPrefix('holdRightFunkin', 'RightHoldPiece');
|
||||
|
||||
this.animation.addByPrefix('holdEndLeftNormal', 'LeftHoldEnd');
|
||||
this.animation.addByPrefix('holdEndDownNormal', 'DownHoldEnd');
|
||||
this.animation.addByPrefix('holdEndUpNormal', 'UpHoldEnd');
|
||||
this.animation.addByPrefix('holdEndRightNormal', 'RightHoldEnd');
|
||||
this.animation.addByPrefix('holdEndLeftFunkin', 'LeftHoldEnd');
|
||||
this.animation.addByPrefix('holdEndDownFunkin', 'DownHoldEnd');
|
||||
this.animation.addByPrefix('holdEndUpFunkin', 'UpHoldEnd');
|
||||
this.animation.addByPrefix('holdEndRightFunkin', 'RightHoldEnd');
|
||||
|
||||
this.animation.addByPrefix('tapLeftPixel', 'pixel4');
|
||||
this.animation.addByPrefix('tapDownPixel', 'pixel5');
|
||||
|
@ -187,8 +187,8 @@ class ChartEditorNoteSprite extends FlxSprite
|
|||
|
||||
function get_noteStyle():String
|
||||
{
|
||||
// Fall back to 'Normal' if it's not a valid note style.
|
||||
return if (NOTE_STYLES.contains(this.parentState.currentSongNoteSkin)) this.parentState.currentSongNoteSkin else 'Normal';
|
||||
// Fall back to Funkin' if it's not a valid note style.
|
||||
return if (NOTE_STYLES.contains(this.parentState.currentSongNoteStyle)) this.parentState.currentSongNoteStyle else 'funkin';
|
||||
}
|
||||
|
||||
public function playNoteAnimation():Void
|
||||
|
@ -199,7 +199,7 @@ class ChartEditorNoteSprite extends FlxSprite
|
|||
var baseAnimationName:String = 'tap';
|
||||
|
||||
// Play the appropriate animation for the type, direction, and skin.
|
||||
var animationName:String = '${baseAnimationName}${this.noteData.getDirectionName()}${this.noteStyle}';
|
||||
var animationName:String = '${baseAnimationName}${this.noteData.getDirectionName()}${this.noteStyle.toTitleCase()}';
|
||||
|
||||
this.animation.play(animationName);
|
||||
|
||||
|
@ -213,7 +213,7 @@ class ChartEditorNoteSprite extends FlxSprite
|
|||
this.updateHitbox();
|
||||
|
||||
// TODO: Make this an attribute of the note skin.
|
||||
this.antialiasing = (this.parentState.currentSongNoteSkin != 'Pixel');
|
||||
this.antialiasing = (this.parentState.currentSongNoteStyle != 'Pixel');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
package funkin.ui.debug.charting;
|
||||
|
||||
import funkin.play.stage.StageData;
|
||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||
import funkin.play.character.CharacterData;
|
||||
import flixel.system.FlxAssets.FlxSoundAsset;
|
||||
import flixel.math.FlxMath;
|
||||
import haxe.ui.components.TextField;
|
||||
|
@ -41,7 +44,7 @@ import funkin.data.song.SongRegistry;
|
|||
import funkin.data.song.SongData.SongEventData;
|
||||
import funkin.data.song.SongData.SongMetadata;
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
import funkin.data.song.SongData.SongPlayableChar;
|
||||
import funkin.data.song.SongData.SongCharacterData;
|
||||
import funkin.data.song.SongDataUtils;
|
||||
import funkin.ui.debug.charting.ChartEditorCommand;
|
||||
import funkin.ui.debug.charting.ChartEditorCommand;
|
||||
|
@ -88,8 +91,11 @@ using Lambda;
|
|||
// @:nullSafety(Loose) // Enable this while developing, then disable to keep unit tests functional!
|
||||
|
||||
@:allow(funkin.ui.debug.charting.ChartEditorCommand)
|
||||
@:allow(funkin.ui.debug.charting.ChartEditorDropdowns)
|
||||
@:allow(funkin.ui.debug.charting.ChartEditorDialogHandler)
|
||||
@:allow(funkin.ui.debug.charting.ChartEditorThemeHandler)
|
||||
@:allow(funkin.ui.debug.charting.ChartEditorAudioHandler)
|
||||
@:allow(funkin.ui.debug.charting.ChartEditorImportExportHandler)
|
||||
@:allow(funkin.ui.debug.charting.ChartEditorToolboxHandler)
|
||||
class ChartEditorState extends HaxeUIState
|
||||
{
|
||||
|
@ -108,7 +114,6 @@ class ChartEditorState extends HaxeUIState
|
|||
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');
|
||||
|
||||
|
@ -950,19 +955,19 @@ class ChartEditorState extends HaxeUIState
|
|||
return currentSongChartData.events = value;
|
||||
}
|
||||
|
||||
public var currentSongNoteSkin(get, set):String;
|
||||
public var currentSongNoteStyle(get, set):String;
|
||||
|
||||
function get_currentSongNoteSkin():String
|
||||
function get_currentSongNoteStyle():String
|
||||
{
|
||||
if (currentSongMetadata.playData.noteSkin == null)
|
||||
{
|
||||
// Initialize to the default value if not set.
|
||||
currentSongMetadata.playData.noteSkin = 'Normal';
|
||||
currentSongMetadata.playData.noteSkin = 'funkin';
|
||||
}
|
||||
return currentSongMetadata.playData.noteSkin;
|
||||
}
|
||||
|
||||
function set_currentSongNoteSkin(value:String):String
|
||||
function set_currentSongNoteStyle(value:String):String
|
||||
{
|
||||
return currentSongMetadata.playData.noteSkin = value;
|
||||
}
|
||||
|
@ -1025,57 +1030,28 @@ class ChartEditorState extends HaxeUIState
|
|||
return currentSongMetadata.artist = value;
|
||||
}
|
||||
|
||||
var currentSongPlayableCharacters(get, never):Array<String>;
|
||||
|
||||
function get_currentSongPlayableCharacters():Array<String>
|
||||
{
|
||||
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;
|
||||
return currentSongMetadata.playData.characters.player;
|
||||
}
|
||||
|
||||
function set_currentSongCharacterPlayer(value:String):String
|
||||
{
|
||||
if (!currentSongPlayableCharacters.contains(value))
|
||||
{
|
||||
trace('Invalid character selected: ' + value);
|
||||
return value;
|
||||
}
|
||||
|
||||
return selectedCharacter = value;
|
||||
return currentSongMetadata.playData.characters.player = 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;
|
||||
return currentSongMetadata.playData.characters.opponent;
|
||||
}
|
||||
|
||||
function set_currentSongCharacterOpponent(value:String):String
|
||||
{
|
||||
var playableCharData:SongPlayableChar = currentSongMetadata.playData.playableChars.get(selectedCharacter);
|
||||
return playableCharData.opponent = value;
|
||||
return currentSongMetadata.playData.characters.opponent = value;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1602,7 +1578,7 @@ class ChartEditorState extends HaxeUIState
|
|||
|
||||
addUIClickListener('menubarItemNewChart', _ -> ChartEditorDialogHandler.openWelcomeDialog(this, true));
|
||||
addUIClickListener('menubarItemOpenChart', _ -> ChartEditorDialogHandler.openBrowseWizard(this, true));
|
||||
addUIClickListener('menubarItemSaveChartAs', _ -> exportAllSongData());
|
||||
addUIClickListener('menubarItemSaveChartAs', _ -> ChartEditorImportExportHandler.exportAllSongData(this));
|
||||
addUIClickListener('menubarItemLoadInst', _ -> ChartEditorDialogHandler.openUploadInstDialog(this, true));
|
||||
addUIClickListener('menubarItemImportChart', _ -> ChartEditorDialogHandler.openImportChartDialog(this, 'legacy', true));
|
||||
|
||||
|
@ -1738,18 +1714,14 @@ class ChartEditorState extends HaxeUIState
|
|||
});
|
||||
}
|
||||
|
||||
addUIChangeListener('menubarItemToggleToolboxTools',
|
||||
event -> ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT, event.value));
|
||||
addUIChangeListener('menubarItemToggleToolboxNotes',
|
||||
event -> ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT, event.value));
|
||||
addUIChangeListener('menubarItemToggleToolboxEvents',
|
||||
event -> ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT, event.value));
|
||||
addUIChangeListener('menubarItemToggleToolboxDifficulty',
|
||||
event -> ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT, event.value));
|
||||
addUIChangeListener('menubarItemToggleToolboxMetadata',
|
||||
event -> ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_METADATA_LAYOUT, event.value));
|
||||
addUIChangeListener('menubarItemToggleToolboxCharacters',
|
||||
event -> ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT, event.value));
|
||||
addUIChangeListener('menubarItemToggleToolboxNotes',
|
||||
event -> ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT, event.value));
|
||||
addUIChangeListener('menubarItemToggleToolboxEvents',
|
||||
event -> ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT, event.value));
|
||||
addUIChangeListener('menubarItemToggleToolboxPlayerPreview',
|
||||
event -> ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT, event.value));
|
||||
addUIChangeListener('menubarItemToggleToolboxOpponentPreview',
|
||||
|
@ -1795,7 +1767,7 @@ class ChartEditorState extends HaxeUIState
|
|||
// Auto-save to local storage.
|
||||
#else
|
||||
// Auto-save to temp file.
|
||||
exportAllSongData(true, true);
|
||||
ChartEditorImportExportHandler.exportAllSongData(this, true, true);
|
||||
#end
|
||||
}
|
||||
|
||||
|
@ -1806,7 +1778,7 @@ class ChartEditorState extends HaxeUIState
|
|||
|
||||
if (saveDataDirty)
|
||||
{
|
||||
exportAllSongData(true);
|
||||
ChartEditorImportExportHandler.exportAllSongData(this, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2407,13 +2379,20 @@ class ChartEditorState extends HaxeUIState
|
|||
var dragLengthMs:Float = dragLengthSteps * Conductor.stepLengthMs;
|
||||
var dragLengthPixels:Float = dragLengthSteps * GRID_SIZE;
|
||||
|
||||
gridGhostHoldNote.visible = true;
|
||||
gridGhostHoldNote.noteData = gridGhostNote.noteData;
|
||||
gridGhostHoldNote.noteDirection = gridGhostNote.noteData.getDirection();
|
||||
if (dragLengthSteps > 0)
|
||||
{
|
||||
gridGhostHoldNote.visible = true;
|
||||
gridGhostHoldNote.noteData = gridGhostNote.noteData;
|
||||
gridGhostHoldNote.noteDirection = gridGhostNote.noteData.getDirection();
|
||||
|
||||
gridGhostHoldNote.setHeightDirectly(dragLengthPixels);
|
||||
gridGhostHoldNote.setHeightDirectly(dragLengthPixels);
|
||||
|
||||
gridGhostHoldNote.updateHoldNotePosition(renderedHoldNotes);
|
||||
gridGhostHoldNote.updateHoldNotePosition(renderedHoldNotes);
|
||||
}
|
||||
else
|
||||
{
|
||||
gridGhostHoldNote.visible = false;
|
||||
}
|
||||
|
||||
if (FlxG.mouse.justReleased)
|
||||
{
|
||||
|
@ -3016,6 +2995,12 @@ class ChartEditorState extends HaxeUIState
|
|||
ChartEditorDialogHandler.openBrowseWizard(this, true);
|
||||
}
|
||||
|
||||
// CTRL + SHIFT + S = Save As
|
||||
if (FlxG.keys.pressed.CONTROL && FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.S)
|
||||
{
|
||||
ChartEditorImportExportHandler.exportAllSongData(this, false);
|
||||
}
|
||||
|
||||
// CTRL + Q = Quit to Menu
|
||||
if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.Q)
|
||||
{
|
||||
|
@ -3167,7 +3152,7 @@ class ChartEditorState extends HaxeUIState
|
|||
selectedDifficulty = prevDifficulty;
|
||||
|
||||
refreshDifficultyTreeSelection();
|
||||
refreshSongMetadataToolbox();
|
||||
refreshMetadataToolbox();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -3176,7 +3161,7 @@ class ChartEditorState extends HaxeUIState
|
|||
selectedDifficulty = prevDifficulty;
|
||||
|
||||
refreshDifficultyTreeSelection();
|
||||
refreshSongMetadataToolbox();
|
||||
refreshMetadataToolbox();
|
||||
}
|
||||
}
|
||||
else
|
||||
|
@ -3195,7 +3180,7 @@ class ChartEditorState extends HaxeUIState
|
|||
selectedDifficulty = nextDifficulty;
|
||||
|
||||
refreshDifficultyTreeSelection();
|
||||
refreshSongMetadataToolbox();
|
||||
refreshMetadataToolbox();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -3204,7 +3189,7 @@ class ChartEditorState extends HaxeUIState
|
|||
selectedDifficulty = nextDifficulty;
|
||||
|
||||
refreshDifficultyTreeSelection();
|
||||
refreshSongMetadataToolbox();
|
||||
refreshMetadataToolbox();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3296,6 +3281,28 @@ class ChartEditorState extends HaxeUIState
|
|||
}
|
||||
}
|
||||
|
||||
public function createDifficulty(variation:String, difficulty:String, scrollSpeed:Float = 1.0)
|
||||
{
|
||||
var variationMetadata:Null<SongMetadata> = songMetadata.get(variation);
|
||||
if (variationMetadata == null) return;
|
||||
|
||||
variationMetadata.playData.difficulties.push(difficulty);
|
||||
|
||||
var resultChartData = songChartData.get(variation);
|
||||
if (resultChartData == null)
|
||||
{
|
||||
resultChartData = new SongChartData([difficulty => scrollSpeed], [], [difficulty => []]);
|
||||
songChartData.set(variation, resultChartData);
|
||||
}
|
||||
else
|
||||
{
|
||||
resultChartData.scrollSpeed.set(difficulty, scrollSpeed);
|
||||
resultChartData.notes.set(difficulty, []);
|
||||
}
|
||||
|
||||
difficultySelectDirty = true; // Force the Difficulty toolbox to update.
|
||||
}
|
||||
|
||||
function refreshDifficultyTreeSelection(?treeView:TreeView):Void
|
||||
{
|
||||
if (treeView == null)
|
||||
|
@ -3469,7 +3476,7 @@ class ChartEditorState extends HaxeUIState
|
|||
selectedVariation = variation;
|
||||
selectedDifficulty = difficulty;
|
||||
// refreshDifficultyTreeSelection(treeView);
|
||||
refreshSongMetadataToolbox();
|
||||
refreshMetadataToolbox();
|
||||
}
|
||||
// case 'song':
|
||||
// case 'variation':
|
||||
|
@ -3478,14 +3485,14 @@ class ChartEditorState extends HaxeUIState
|
|||
trace('Selected wrong node type, resetting selection.');
|
||||
var currentTreeDifficultyNode = getCurrentTreeDifficultyNode(treeView);
|
||||
if (currentTreeDifficultyNode != null) treeView.selectedNode = currentTreeDifficultyNode;
|
||||
refreshSongMetadataToolbox();
|
||||
refreshMetadataToolbox();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When the difficulty changes, update the song metadata toolbox to reflect the new data.
|
||||
*/
|
||||
function refreshSongMetadataToolbox():Void
|
||||
function refreshMetadataToolbox():Void
|
||||
{
|
||||
var toolbox:Null<CollapsibleDialog> = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
|
||||
if (toolbox == null) return;
|
||||
|
@ -3499,8 +3506,8 @@ class ChartEditorState extends HaxeUIState
|
|||
var inputStage:Null<DropDown> = toolbox.findComponent('inputStage', DropDown);
|
||||
if (inputStage != null) inputStage.value = currentSongMetadata.playData.stage;
|
||||
|
||||
var inputNoteSkin:Null<DropDown> = toolbox.findComponent('inputNoteSkin', DropDown);
|
||||
if (inputNoteSkin != null) inputNoteSkin.value = currentSongMetadata.playData.noteSkin;
|
||||
var inputNoteStyle:Null<DropDown> = toolbox.findComponent('inputNoteStyle', DropDown);
|
||||
if (inputNoteStyle != null) inputNoteStyle.value = currentSongMetadata.playData.noteSkin;
|
||||
|
||||
var inputBPM:Null<NumberStepper> = toolbox.findComponent('inputBPM', NumberStepper);
|
||||
if (inputBPM != null) inputBPM.value = currentSongMetadata.timeChanges[0].bpm;
|
||||
|
@ -3515,16 +3522,54 @@ class ChartEditorState extends HaxeUIState
|
|||
if (frameVariation != null) frameVariation.text = 'Variation: ${selectedVariation.toTitleCase()}';
|
||||
var frameDifficulty:Null<Frame> = toolbox.findComponent('frameDifficulty', Frame);
|
||||
if (frameDifficulty != null) frameDifficulty.text = 'Difficulty: ${selectedDifficulty.toTitleCase()}';
|
||||
}
|
||||
|
||||
function addDifficulty(variation:String):Void {}
|
||||
var inputStage:Null<DropDown> = toolbox.findComponent('inputStage', DropDown);
|
||||
var stageId:String = currentSongMetadata.playData.stage;
|
||||
var stageData:Null<StageData> = StageDataParser.parseStageData(stageId);
|
||||
if (stageData != null)
|
||||
{
|
||||
inputStage.value = {id: stageId, text: stageData.name};
|
||||
}
|
||||
else
|
||||
{
|
||||
inputStage.value = {id: "mainStage", text: "Main Stage"};
|
||||
}
|
||||
|
||||
function addVariation(variationId:String):Void
|
||||
{
|
||||
// Create a new variation with the specified ID.
|
||||
songMetadata.set(variationId, currentSongMetadata.clone(variationId));
|
||||
// Switch to the new variation.
|
||||
selectedVariation = variationId;
|
||||
var inputCharacterPlayer:Null<DropDown> = toolbox.findComponent('inputCharacterPlayer', DropDown);
|
||||
var charIdPlayer:String = currentSongMetadata.playData.characters.player;
|
||||
var charDataPlayer:Null<CharacterData> = CharacterDataParser.fetchCharacterData(charIdPlayer);
|
||||
if (charDataPlayer != null)
|
||||
{
|
||||
inputCharacterPlayer.value = {id: charIdPlayer, text: charDataPlayer.name};
|
||||
}
|
||||
else
|
||||
{
|
||||
inputCharacterPlayer.value = {id: "bf", text: "Boyfriend"};
|
||||
}
|
||||
|
||||
var inputCharacterOpponent:Null<DropDown> = toolbox.findComponent('inputCharacterOpponent', DropDown);
|
||||
var charIdOpponent:String = currentSongMetadata.playData.characters.opponent;
|
||||
var charDataOpponent:Null<CharacterData> = CharacterDataParser.fetchCharacterData(charIdOpponent);
|
||||
if (charDataOpponent != null)
|
||||
{
|
||||
inputCharacterOpponent.value = {id: charIdOpponent, text: charDataOpponent.name};
|
||||
}
|
||||
else
|
||||
{
|
||||
inputCharacterOpponent.value = {id: "dad", text: "Dad"};
|
||||
}
|
||||
|
||||
var inputCharacterGirlfriend:Null<DropDown> = toolbox.findComponent('inputCharacterGirlfriend', DropDown);
|
||||
var charIdGirlfriend:String = currentSongMetadata.playData.characters.girlfriend;
|
||||
var charDataGirlfriend:Null<CharacterData> = CharacterDataParser.fetchCharacterData(charIdGirlfriend);
|
||||
if (charDataGirlfriend != null)
|
||||
{
|
||||
inputCharacterGirlfriend.value = {id: charIdGirlfriend, text: charDataGirlfriend.name};
|
||||
}
|
||||
else
|
||||
{
|
||||
inputCharacterGirlfriend.value = {id: "none", text: "None"};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -3710,9 +3755,9 @@ class ChartEditorState extends HaxeUIState
|
|||
switch (noteData.getStrumlineIndex())
|
||||
{
|
||||
case 0: // Player
|
||||
if (hitsoundsEnabledPlayer) playSound(Paths.sound('funnyNoise/funnyNoise-09'));
|
||||
if (hitsoundsEnabledPlayer) ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-09'));
|
||||
case 1: // Opponent
|
||||
if (hitsoundsEnabledOpponent) playSound(Paths.sound('funnyNoise/funnyNoise-010'));
|
||||
if (hitsoundsEnabledOpponent) ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-010'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3913,77 +3958,6 @@ class ChartEditorState extends HaxeUIState
|
|||
Conductor.update(targetPos);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public function loadInstrumentalFromPath(path:Path):Bool
|
||||
{
|
||||
#if sys
|
||||
// Validate file extension.
|
||||
if (path.ext != null && !SUPPORTED_MUSIC_FORMATS.contains(path.ext))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var fileBytes:haxe.io.Bytes = sys.io.File.getBytes(path.toString());
|
||||
return loadInstrumentalFromBytes(fileBytes, '${path.file}.${path.ext}');
|
||||
#else
|
||||
trace("[WARN] This platform can't load audio from a file path, you'll need to fetch the bytes some other way.");
|
||||
return false;
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an instrumental from audio byte data, replacing the current instrumental.
|
||||
* @param bytes The audio byte data.
|
||||
* @param fileName The name of the file, if available. Used for notifications.
|
||||
* @return Success or failure.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an instrumental from an OpenFL asset, replacing the current instrumental.
|
||||
* @param path The path to the asset. Use `Paths` to build this.
|
||||
* @return Success or failure.
|
||||
*/
|
||||
public function loadInstrumentalFromAsset(path:String):Bool
|
||||
{
|
||||
var instTrack:FlxSound = FlxG.sound.load(path, 1.0, false);
|
||||
if (instTrack != null)
|
||||
{
|
||||
audioInstTrack = instTrack;
|
||||
|
||||
audioInstTrackData = Assets.getBytes(path);
|
||||
|
||||
postLoadInstrumental();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function postLoadInstrumental():Void
|
||||
{
|
||||
if (audioInstTrack != null)
|
||||
|
@ -4014,23 +3988,6 @@ class ChartEditorState extends HaxeUIState
|
|||
moveSongToScrollPosition();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = 'default'):Bool
|
||||
{
|
||||
#if sys
|
||||
var fileBytes:haxe.io.Bytes = sys.io.File.getBytes(path.toString());
|
||||
return loadVocalsFromBytes(fileBytes, charKey);
|
||||
#else
|
||||
trace("[WARN] This platform can't load audio from a file path, you'll need to fetch the bytes some other way.");
|
||||
return false;
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the voices group.
|
||||
*/
|
||||
|
@ -4039,141 +3996,6 @@ class ChartEditorState extends HaxeUIState
|
|||
if (audioVocalTrackGroup != null) audioVocalTrackGroup.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a vocal track for a given song and character and add it to the voices group.
|
||||
*
|
||||
* @param path ID of the asset.
|
||||
* @param charKey Character to load the vocal track for.
|
||||
* @return Success or failure.
|
||||
*/
|
||||
public function loadVocalsFromAsset(path:String, charType:CharacterType = OTHER):Bool
|
||||
{
|
||||
var vocalTrack:FlxSound = FlxG.sound.load(path, 1.0, false);
|
||||
if (vocalTrack != null)
|
||||
{
|
||||
switch (charType)
|
||||
{
|
||||
case CharacterType.BF:
|
||||
if (audioVocalTrackGroup != null) audioVocalTrackGroup.addPlayerVoice(vocalTrack);
|
||||
audioVocalTrackData.set(currentSongCharacterPlayer, Assets.getBytes(path));
|
||||
case CharacterType.DAD:
|
||||
if (audioVocalTrackGroup != null) audioVocalTrackGroup.addOpponentVoice(vocalTrack);
|
||||
audioVocalTrackData.set(currentSongCharacterOpponent, Assets.getBytes(path));
|
||||
default:
|
||||
if (audioVocalTrackGroup != null) audioVocalTrackGroup.add(vocalTrack);
|
||||
audioVocalTrackData.set('default', Assets.getBytes(path));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a vocal track from audio byte data.
|
||||
*/
|
||||
public function loadVocalsFromBytes(bytes:haxe.io.Bytes, charKey:String = ''):Bool
|
||||
{
|
||||
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);
|
||||
if (audioVocalTrackGroup != null) audioVocalTrackGroup.add(vocalTrack);
|
||||
audioVocalTrackData.set(charKey, bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch's a song's existing chart and audio and loads it, replacing the current song.
|
||||
*/
|
||||
public function loadSongAsTemplate(songId:String):Void
|
||||
{
|
||||
var song:Null<Song> = SongRegistry.instance.fetchEntry(songId);
|
||||
|
||||
if (song == null) return;
|
||||
|
||||
// Load the song metadata.
|
||||
var rawSongMetadata:Array<SongMetadata> = song.getRawMetadata();
|
||||
var songMetadata:Map<String, SongMetadata> = [];
|
||||
var songChartData:Map<String, SongChartData> = [];
|
||||
|
||||
for (metadata in rawSongMetadata)
|
||||
{
|
||||
if (metadata == null) continue;
|
||||
var variation = (metadata.variation == null || metadata.variation == '') ? 'default' : metadata.variation;
|
||||
|
||||
// Clone to prevent modifying the original.
|
||||
var metadataClone:SongMetadata = metadata.clone(variation);
|
||||
if (metadataClone != null) songMetadata.set(variation, metadataClone);
|
||||
|
||||
songChartData.set(variation, SongRegistry.instance.parseEntryChartData(songId, metadata.variation));
|
||||
}
|
||||
|
||||
loadSong(songMetadata, songChartData);
|
||||
|
||||
sortChartData();
|
||||
|
||||
clearVocals();
|
||||
|
||||
loadInstrumentalFromAsset(Paths.inst(songId));
|
||||
|
||||
var diff:Null<SongDifficulty> = song.getDifficulty(selectedDifficulty);
|
||||
var voiceList:Array<String> = diff != null ? diff.buildVoiceList(currentSongCharacterPlayer) : [];
|
||||
if (voiceList.length == 2)
|
||||
{
|
||||
loadVocalsFromAsset(voiceList[0], BF);
|
||||
loadVocalsFromAsset(voiceList[1], DAD);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (voicePath in voiceList)
|
||||
{
|
||||
loadVocalsFromAsset(voicePath);
|
||||
}
|
||||
}
|
||||
|
||||
#if !mac
|
||||
NotificationManager.instance.addNotification(
|
||||
{
|
||||
title: 'Success',
|
||||
body: 'Loaded song (${rawSongMetadata[0].songName})',
|
||||
type: NotificationType.Success,
|
||||
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
|
||||
});
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<String, SongMetadata>, newSongChartData:Map<String, SongChartData>):Void
|
||||
{
|
||||
this.songMetadata = newSongMetadata;
|
||||
this.songChartData = newSongChartData;
|
||||
|
||||
Conductor.forceBPM(null); // Disable the forced BPM.
|
||||
Conductor.mapTimeChanges(currentSongMetadata.timeChanges);
|
||||
|
||||
notePreviewDirty = true;
|
||||
notePreviewViewportBoundsDirty = true;
|
||||
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.
|
||||
|
@ -4291,7 +4113,7 @@ class ChartEditorState extends HaxeUIState
|
|||
|
||||
function playMetronomeTick(high:Bool = false):Void
|
||||
{
|
||||
playSound(Paths.sound('pianoStuff/piano-${high ? '001' : '008'}'));
|
||||
ChartEditorAudioHandler.playSound(Paths.sound('pianoStuff/piano-${high ? '001' : '008'}'));
|
||||
}
|
||||
|
||||
function isNoteSelected(note:Null<SongNoteData>):Bool
|
||||
|
@ -4304,27 +4126,6 @@ class ChartEditorState extends HaxeUIState
|
|||
return event != null && currentEventSelection.indexOf(event) != -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Play a sound effect.
|
||||
* Automatically cleans up after itself and recycles previous FlxSound instances if available, for performance.
|
||||
*/
|
||||
function playSound(path:String):Void
|
||||
{
|
||||
var snd:FlxSound = FlxG.sound.list.recycle(FlxSound) ?? new FlxSound();
|
||||
|
||||
var asset:Null<FlxSoundAsset> = FlxG.sound.cache(path);
|
||||
if (asset == null)
|
||||
{
|
||||
trace('WARN: Failed to play sound $path, asset not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
snd.loadEmbedded(asset);
|
||||
snd.autoDestroy = true;
|
||||
FlxG.sound.list.add(snd);
|
||||
snd.play();
|
||||
}
|
||||
|
||||
override function destroy():Void
|
||||
{
|
||||
super.destroy();
|
||||
|
@ -4345,78 +4146,6 @@ class ChartEditorState extends HaxeUIState
|
|||
{
|
||||
NotificationManager.instance.clearNotifications();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param force Whether to force the export without prompting the user for a file location.
|
||||
* @param tmp If true, save to the temporary directory instead of the local `backup` directory.
|
||||
*/
|
||||
public function exportAllSongData(force:Bool = false, tmp:Bool = false):Void
|
||||
{
|
||||
var zipEntries:Array<haxe.zip.Entry> = [];
|
||||
|
||||
for (variation in availableVariations)
|
||||
{
|
||||
var variationId:String = variation;
|
||||
if (variation == '' || variation == 'default' || variation == 'normal')
|
||||
{
|
||||
variationId = '';
|
||||
}
|
||||
|
||||
if (variationId == '')
|
||||
{
|
||||
var variationMetadata:Null<SongMetadata> = songMetadata.get(variation);
|
||||
if (variationMetadata != null) zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-metadata.json', SerializerUtil.toJSON(variationMetadata)));
|
||||
var variationChart:Null<SongChartData> = songChartData.get(variation);
|
||||
if (variationChart != null) zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-chart.json', SerializerUtil.toJSON(variationChart)));
|
||||
}
|
||||
else
|
||||
{
|
||||
var variationMetadata:Null<SongMetadata> = songMetadata.get(variation);
|
||||
if (variationMetadata != null) zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-metadata-$variationId.json',
|
||||
SerializerUtil.toJSON(variationMetadata)));
|
||||
var variationChart:Null<SongChartData> = songChartData.get(variation);
|
||||
if (variationChart != null) zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-chart-$variationId.json', SerializerUtil.toJSON(variationChart)));
|
||||
}
|
||||
}
|
||||
|
||||
if (audioInstTrackData != null) zipEntries.push(FileUtil.makeZIPEntryFromBytes('Inst.ogg', audioInstTrackData));
|
||||
for (charId in audioVocalTrackData.keys())
|
||||
{
|
||||
var entryData = audioVocalTrackData.get(charId);
|
||||
if (entryData == null) continue;
|
||||
zipEntries.push(FileUtil.makeZIPEntryFromBytes('Vocals-$charId.ogg', entryData));
|
||||
}
|
||||
|
||||
trace('Exporting ${zipEntries.length} files to ZIP...');
|
||||
|
||||
if (force)
|
||||
{
|
||||
var targetPath:String = if (tmp)
|
||||
{
|
||||
Path.join([FileUtil.getTempDir(), 'chart-editor-exit-${DateUtil.generateTimestamp()}.zip']);
|
||||
}
|
||||
else
|
||||
{
|
||||
Path.join(['./backups/', 'chart-editor-exit-${DateUtil.generateTimestamp()}.zip']);
|
||||
}
|
||||
|
||||
// We have to force write because the program will die before the save dialog is closed.
|
||||
trace('Force exporting to $targetPath...');
|
||||
FileUtil.saveFilesAsZIPToPath(zipEntries, targetPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prompt and save.
|
||||
var onSave:Array<String>->Void = function(paths:Array<String>) {
|
||||
trace('Successfully exported files.');
|
||||
};
|
||||
|
||||
var onCancel:Void->Void = function() {
|
||||
trace('Export cancelled.');
|
||||
};
|
||||
|
||||
FileUtil.saveMultipleFiles(zipEntries, onSave, onCancel, '$currentSongId-chart.zip');
|
||||
}
|
||||
}
|
||||
|
||||
enum LiveInputStyle
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
package funkin.ui.debug.charting;
|
||||
|
||||
import funkin.ui.haxeui.components.FunkinDropDown;
|
||||
import funkin.play.stage.StageData.StageDataParser;
|
||||
import funkin.play.stage.StageData;
|
||||
import funkin.play.character.CharacterData;
|
||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||
import haxe.ui.components.HorizontalSlider;
|
||||
import haxe.ui.containers.TreeView;
|
||||
import haxe.ui.containers.TreeViewNode;
|
||||
|
@ -9,6 +14,7 @@ import funkin.data.event.SongEventData;
|
|||
import funkin.data.song.SongData.SongTimeChange;
|
||||
import funkin.play.song.SongSerializer;
|
||||
import funkin.ui.haxeui.components.CharacterPlayer;
|
||||
import funkin.util.FileUtil;
|
||||
import haxe.ui.components.Button;
|
||||
import haxe.ui.components.CheckBox;
|
||||
import haxe.ui.components.DropDown;
|
||||
|
@ -78,8 +84,6 @@ class ChartEditorToolboxHandler
|
|||
onShowToolboxDifficulty(state, toolbox);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:
|
||||
onShowToolboxMetadata(state, toolbox);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT:
|
||||
onShowToolboxCharacters(state, toolbox);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT:
|
||||
onShowToolboxPlayerPreview(state, toolbox);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT:
|
||||
|
@ -117,8 +121,6 @@ class ChartEditorToolboxHandler
|
|||
onHideToolboxDifficulty(state, toolbox);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:
|
||||
onHideToolboxMetadata(state, toolbox);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT:
|
||||
onHideToolboxCharacters(state, toolbox);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT:
|
||||
onHideToolboxPlayerPreview(state, toolbox);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT:
|
||||
|
@ -167,8 +169,6 @@ class ChartEditorToolboxHandler
|
|||
toolbox = buildToolboxDifficultyLayout(state);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:
|
||||
toolbox = buildToolboxMetadataLayout(state);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT:
|
||||
toolbox = buildToolboxCharactersLayout(state);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT:
|
||||
toolbox = buildToolboxPlayerPreviewLayout(state);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT:
|
||||
|
@ -445,14 +445,20 @@ class ChartEditorToolboxHandler
|
|||
state.setUICheckboxSelected('menubarItemToggleToolboxDifficulty', false);
|
||||
}
|
||||
|
||||
var difficultyToolboxAddVariation:Null<Button> = toolbox.findComponent('difficultyToolboxAddVariation', Button);
|
||||
if (difficultyToolboxAddVariation == null)
|
||||
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxAddVariation component.';
|
||||
var difficultyToolboxAddDifficulty:Null<Button> = toolbox.findComponent('difficultyToolboxAddDifficulty', Button);
|
||||
if (difficultyToolboxAddDifficulty == null)
|
||||
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxAddDifficulty component.';
|
||||
var difficultyToolboxSaveMetadata:Null<Button> = toolbox.findComponent('difficultyToolboxSaveMetadata', Button);
|
||||
if (difficultyToolboxSaveMetadata == null)
|
||||
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxSaveMetadata component.';
|
||||
var difficultyToolboxSaveChart:Null<Button> = toolbox.findComponent('difficultyToolboxSaveChart', Button);
|
||||
if (difficultyToolboxSaveChart == null)
|
||||
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxSaveChart component.';
|
||||
var difficultyToolboxSaveAll:Null<Button> = toolbox.findComponent('difficultyToolboxSaveAll', Button);
|
||||
if (difficultyToolboxSaveAll == null) throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxSaveAll component.';
|
||||
// var difficultyToolboxSaveAll:Null<Button> = toolbox.findComponent('difficultyToolboxSaveAll', Button);
|
||||
// if (difficultyToolboxSaveAll == null) throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxSaveAll component.';
|
||||
var difficultyToolboxLoadMetadata:Null<Button> = toolbox.findComponent('difficultyToolboxLoadMetadata', Button);
|
||||
if (difficultyToolboxLoadMetadata == null)
|
||||
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxLoadMetadata component.';
|
||||
|
@ -460,26 +466,32 @@ class ChartEditorToolboxHandler
|
|||
if (difficultyToolboxLoadChart == null)
|
||||
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxLoadChart component.';
|
||||
|
||||
difficultyToolboxSaveMetadata.onClick = function(event:UIEvent) {
|
||||
SongSerializer.exportSongMetadata(state.currentSongMetadata, state.currentSongId);
|
||||
difficultyToolboxAddVariation.onClick = function(_:UIEvent) {
|
||||
ChartEditorDialogHandler.openAddVariationDialog(state, true);
|
||||
};
|
||||
|
||||
difficultyToolboxSaveChart.onClick = function(event:UIEvent) {
|
||||
SongSerializer.exportSongChartData(state.currentSongChartData, state.currentSongId);
|
||||
difficultyToolboxAddDifficulty.onClick = function(_:UIEvent) {
|
||||
ChartEditorDialogHandler.openAddDifficultyDialog(state, true);
|
||||
};
|
||||
|
||||
difficultyToolboxSaveAll.onClick = function(event:UIEvent) {
|
||||
state.exportAllSongData();
|
||||
difficultyToolboxSaveMetadata.onClick = function(_:UIEvent) {
|
||||
var vari:String = state.selectedVariation != Constants.DEFAULT_VARIATION ? '-${state.selectedVariation}' : '';
|
||||
FileUtil.writeFileReference('${state.currentSongId}$vari-metadata.json', state.currentSongMetadata.serialize());
|
||||
};
|
||||
|
||||
difficultyToolboxLoadMetadata.onClick = function(event:UIEvent) {
|
||||
difficultyToolboxSaveChart.onClick = function(_:UIEvent) {
|
||||
var vari:String = state.selectedVariation != Constants.DEFAULT_VARIATION ? '-${state.selectedVariation}' : '';
|
||||
FileUtil.writeFileReference('${state.currentSongId}$vari-chart.json', state.currentSongChartData.serialize());
|
||||
};
|
||||
|
||||
difficultyToolboxLoadMetadata.onClick = function(_:UIEvent) {
|
||||
// Replace metadata for current variation.
|
||||
SongSerializer.importSongMetadataAsync(function(songMetadata) {
|
||||
state.currentSongMetadata = songMetadata;
|
||||
});
|
||||
};
|
||||
|
||||
difficultyToolboxLoadChart.onClick = function(event:UIEvent) {
|
||||
difficultyToolboxLoadChart.onClick = function(_:UIEvent) {
|
||||
// Replace chart data for current variation.
|
||||
SongSerializer.importSongChartDataAsync(function(songChartData) {
|
||||
state.currentSongChartData = songChartData;
|
||||
|
@ -554,7 +566,7 @@ class ChartEditorToolboxHandler
|
|||
};
|
||||
inputSongArtist.value = state.currentSongMetadata.artist;
|
||||
|
||||
var inputStage:Null<DropDown> = toolbox.findComponent('inputStage', DropDown);
|
||||
var inputStage:Null<FunkinDropDown> = toolbox.findComponent('inputStage', FunkinDropDown);
|
||||
if (inputStage == null) throw 'ChartEditorToolboxHandler.buildToolboxMetadataLayout() - Could not find inputStage component.';
|
||||
inputStage.onChange = function(event:UIEvent) {
|
||||
var valid:Bool = event.data != null && event.data.id != null;
|
||||
|
@ -564,15 +576,48 @@ class ChartEditorToolboxHandler
|
|||
state.currentSongMetadata.playData.stage = event.data.id;
|
||||
}
|
||||
};
|
||||
inputStage.value = state.currentSongMetadata.playData.stage;
|
||||
var startingValueStage = ChartEditorDropdowns.populateDropdownWithStages(inputStage, state.currentSongMetadata.playData.stage);
|
||||
inputStage.value = startingValueStage;
|
||||
|
||||
var inputNoteSkin:Null<DropDown> = toolbox.findComponent('inputNoteSkin', DropDown);
|
||||
if (inputNoteSkin == null) throw 'ChartEditorToolboxHandler.buildToolboxMetadataLayout() - Could not find inputNoteSkin component.';
|
||||
inputNoteSkin.onChange = function(event:UIEvent) {
|
||||
if ((event?.data?.id ?? null) == null) return;
|
||||
state.currentSongNoteSkin = event.data.id;
|
||||
var inputNoteStyle:Null<FunkinDropDown> = toolbox.findComponent('inputNoteStyle', FunkinDropDown);
|
||||
if (inputNoteStyle == null) throw 'ChartEditorToolboxHandler.buildToolboxMetadataLayout() - Could not find inputNoteStyle component.';
|
||||
inputNoteStyle.onChange = function(event:UIEvent) {
|
||||
if (event.data?.id == null) return;
|
||||
state.currentSongNoteStyle = event.data.id;
|
||||
};
|
||||
inputNoteSkin.value = state.currentSongNoteSkin;
|
||||
inputNoteStyle.value = state.currentSongNoteStyle;
|
||||
|
||||
// By using this flag, we prevent the dropdown value from changing while it is being populated.
|
||||
|
||||
var inputCharacterPlayer:Null<FunkinDropDown> = toolbox.findComponent('inputCharacterPlayer', FunkinDropDown);
|
||||
if (inputCharacterPlayer == null) throw 'ChartEditorToolboxHandler.buildToolboxMetadataLayout() - Could not find inputCharacterPlayer component.';
|
||||
inputCharacterPlayer.onChange = function(event:UIEvent) {
|
||||
if (event.data?.id == null) return;
|
||||
state.currentSongMetadata.playData.characters.player = event.data.id;
|
||||
};
|
||||
var startingValuePlayer = ChartEditorDropdowns.populateDropdownWithCharacters(inputCharacterPlayer, CharacterType.BF,
|
||||
state.currentSongMetadata.playData.characters.player);
|
||||
inputCharacterPlayer.value = startingValuePlayer;
|
||||
|
||||
var inputCharacterOpponent:Null<FunkinDropDown> = toolbox.findComponent('inputCharacterOpponent', FunkinDropDown);
|
||||
if (inputCharacterOpponent == null) throw 'ChartEditorToolboxHandler.buildToolboxMetadataLayout() - Could not find inputCharacterOpponent component.';
|
||||
inputCharacterOpponent.onChange = function(event:UIEvent) {
|
||||
if (event.data?.id == null) return;
|
||||
state.currentSongMetadata.playData.characters.opponent = event.data.id;
|
||||
};
|
||||
var startingValueOpponent = ChartEditorDropdowns.populateDropdownWithCharacters(inputCharacterOpponent, CharacterType.DAD,
|
||||
state.currentSongMetadata.playData.characters.opponent);
|
||||
inputCharacterOpponent.value = startingValueOpponent;
|
||||
|
||||
var inputCharacterGirlfriend:Null<FunkinDropDown> = toolbox.findComponent('inputCharacterGirlfriend', FunkinDropDown);
|
||||
if (inputCharacterGirlfriend == null) throw 'ChartEditorToolboxHandler.buildToolboxMetadataLayout() - Could not find inputCharacterGirlfriend component.';
|
||||
inputCharacterGirlfriend.onChange = function(event:UIEvent) {
|
||||
if (event.data?.id == null) return;
|
||||
state.currentSongMetadata.playData.characters.girlfriend = event.data.id == "none" ? "" : event.data.id;
|
||||
};
|
||||
var startingValueGirlfriend = ChartEditorDropdowns.populateDropdownWithCharacters(inputCharacterGirlfriend, CharacterType.GF,
|
||||
state.currentSongMetadata.playData.characters.girlfriend);
|
||||
inputCharacterGirlfriend.value = startingValueGirlfriend;
|
||||
|
||||
var inputBPM:Null<NumberStepper> = toolbox.findComponent('inputBPM', NumberStepper);
|
||||
if (inputBPM == null) throw 'ChartEditorToolboxHandler.buildToolboxMetadataLayout() - Could not find inputBPM component.';
|
||||
|
@ -630,32 +675,11 @@ class ChartEditorToolboxHandler
|
|||
|
||||
static function onShowToolboxMetadata(state:ChartEditorState, toolbox:CollapsibleDialog):Void
|
||||
{
|
||||
state.refreshSongMetadataToolbox();
|
||||
state.refreshMetadataToolbox();
|
||||
}
|
||||
|
||||
static function onHideToolboxMetadata(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
|
||||
|
||||
static function buildToolboxCharactersLayout(state:ChartEditorState):Null<CollapsibleDialog>
|
||||
{
|
||||
var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT);
|
||||
|
||||
if (toolbox == null) return null;
|
||||
|
||||
// Starting position.
|
||||
toolbox.x = 175;
|
||||
toolbox.y = 300;
|
||||
|
||||
toolbox.onDialogClosed = function(event:DialogEvent) {
|
||||
state.setUICheckboxSelected('menubarItemToggleToolboxCharacters', false);
|
||||
}
|
||||
|
||||
return toolbox;
|
||||
}
|
||||
|
||||
static function onShowToolboxCharacters(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
|
||||
|
||||
static function onHideToolboxCharacters(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
|
||||
|
||||
static function buildToolboxPlayerPreviewLayout(state:ChartEditorState):Null<CollapsibleDialog>
|
||||
{
|
||||
var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT);
|
||||
|
|
|
@ -2,6 +2,7 @@ package funkin.util;
|
|||
|
||||
import flixel.util.FlxColor;
|
||||
import lime.app.Application;
|
||||
import funkin.data.song.SongData.SongTimeFormat;
|
||||
|
||||
class Constants
|
||||
{
|
||||
|
@ -22,6 +23,16 @@ class Constants
|
|||
*/
|
||||
public static var VERSION(get, never):String;
|
||||
|
||||
/**
|
||||
* The generatedBy string embedded in the chart files made by this application.
|
||||
*/
|
||||
public static var GENERATED_BY(get, never):String;
|
||||
|
||||
static function get_GENERATED_BY():String
|
||||
{
|
||||
return '${Constants.TITLE} - ${Constants.VERSION}';
|
||||
}
|
||||
|
||||
/**
|
||||
* A suffix to add to the game version.
|
||||
* Add a suffix to prototype builds and remove it for releases.
|
||||
|
@ -140,7 +151,32 @@ class Constants
|
|||
/**
|
||||
* The default BPM for charts, so things don't break if none is specified.
|
||||
*/
|
||||
public static final DEFAULT_BPM:Int = 100;
|
||||
public static final DEFAULT_BPM:Float = 100.0;
|
||||
|
||||
/**
|
||||
* The default name for songs.
|
||||
*/
|
||||
public static final DEFAULT_SONGNAME:String = "Unknown";
|
||||
|
||||
/**
|
||||
* The default artist for songs.
|
||||
*/
|
||||
public static final DEFAULT_ARTIST:String = "Unknown";
|
||||
|
||||
/**
|
||||
* The default note style for songs.
|
||||
*/
|
||||
public static final DEFAULT_NOTE_STYLE:String = "funkin";
|
||||
|
||||
/**
|
||||
* The default timing format for songs.
|
||||
*/
|
||||
public static final DEFAULT_TIMEFORMAT:SongTimeFormat = SongTimeFormat.MILLISECONDS;
|
||||
|
||||
/**
|
||||
* The default scroll speed for songs.
|
||||
*/
|
||||
public static final DEFAULT_SCROLLSPEED:Float = 1.0;
|
||||
|
||||
/**
|
||||
* Default numerator for the time signature.
|
||||
|
@ -293,6 +329,39 @@ class Constants
|
|||
*/
|
||||
public static final GHOST_TAPPING:Bool = false;
|
||||
|
||||
/**
|
||||
* FILE EXTENSIONS
|
||||
*/
|
||||
// ==============================
|
||||
|
||||
/**
|
||||
* The file extension used when exporting chart files.
|
||||
*
|
||||
* - "I made a new file format"
|
||||
* - "Actually new or just a renamed ZIP?"
|
||||
*/
|
||||
public static final EXT_CHART = "fnfc";
|
||||
|
||||
/**
|
||||
* The file extension used when loading audio files.
|
||||
*/
|
||||
public static final EXT_SOUND = #if web "mp3" #else "ogg" #end;
|
||||
|
||||
/**
|
||||
* The file extension used when loading video files.
|
||||
*/
|
||||
public static final EXT_VIDEO = "mp4";
|
||||
|
||||
/**
|
||||
* The file extension used when loading image files.
|
||||
*/
|
||||
public static final EXT_IMAGE = "png";
|
||||
|
||||
/**
|
||||
* The file extension used when loading data files.
|
||||
*/
|
||||
public static final EXT_DATA = "json";
|
||||
|
||||
/**
|
||||
* OTHER
|
||||
*/
|
||||
|
|
|
@ -5,10 +5,9 @@ import lime.utils.Bytes;
|
|||
import lime.ui.FileDialog;
|
||||
import openfl.net.FileFilter;
|
||||
import haxe.io.Path;
|
||||
#if html5
|
||||
import openfl.net.FileReference;
|
||||
import openfl.events.Event;
|
||||
#end
|
||||
import openfl.events.IOErrorEvent;
|
||||
|
||||
/**
|
||||
* Utilities for reading and writing files on various platforms.
|
||||
|
@ -260,8 +259,7 @@ class FileUtil
|
|||
/**
|
||||
* Takes an array of file entries and prompts the user to save them as a ZIP file.
|
||||
*/
|
||||
public static function saveFilesAsZIP(resources:Array<Entry>, ?onSave:Array<String>->Void, ?onCancel:Void->Void, ?defaultPath:String,
|
||||
force:Bool = false):Bool
|
||||
public static function saveFilesAsZIP(resources:Array<Entry>, ?onSave:Array<String>->Void, ?onCancel:Void->Void, ?defaultPath:String, force:Bool = false):Bool
|
||||
{
|
||||
// Create a ZIP file.
|
||||
var zipBytes:Bytes = createZIPFromEntries(resources);
|
||||
|
@ -309,6 +307,7 @@ class FileUtil
|
|||
#if sys
|
||||
return sys.io.File.getContent(path);
|
||||
#else
|
||||
trace('ERROR: readStringFromPath not implemented for this platform');
|
||||
return null;
|
||||
#end
|
||||
}
|
||||
|
@ -329,6 +328,48 @@ class FileUtil
|
|||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public static function browseFileReference(callback:FileReference->Void)
|
||||
{
|
||||
var file = new FileReference();
|
||||
|
||||
file.addEventListener(Event.SELECT, function(e) {
|
||||
var selectedFileRef:FileReference = e.target;
|
||||
trace('Selected file: ' + selectedFileRef.name);
|
||||
selectedFileRef.addEventListener(Event.COMPLETE, function(e) {
|
||||
var loadedFileRef:FileReference = e.target;
|
||||
trace('Loaded file: ' + loadedFileRef.name);
|
||||
callback(loadedFileRef);
|
||||
});
|
||||
selectedFileRef.load();
|
||||
});
|
||||
|
||||
file.browse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts the user to save a file to their computer.
|
||||
*/
|
||||
public static function writeFileReference(path:String, data:String)
|
||||
{
|
||||
var file = new FileReference();
|
||||
file.addEventListener(Event.COMPLETE, function(e:Event) {
|
||||
trace('Successfully wrote file.');
|
||||
});
|
||||
file.addEventListener(Event.CANCEL, function(e:Event) {
|
||||
trace('Cancelled writing file.');
|
||||
});
|
||||
file.addEventListener(IOErrorEvent.IO_ERROR, function(e:IOErrorEvent) {
|
||||
trace('IO error writing file.');
|
||||
});
|
||||
file.save(data, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read JSON file contents directly from a given path.
|
||||
* Only works on desktop.
|
||||
|
|
|
@ -13,6 +13,7 @@ typedef ScoreInput =
|
|||
|
||||
/**
|
||||
* A class of functions dedicated to serializing and deserializing data.
|
||||
* TODO: Rewrite/refactor this to use json2object.
|
||||
*/
|
||||
class SerializerUtil
|
||||
{
|
||||
|
|
Loading…
Reference in a new issue