Funkin/source/funkin/play/song/SongData.hx

709 lines
14 KiB
Haxe

package funkin.play.song;
import flixel.util.typeLimit.OneOfTwo;
import funkin.play.song.ScriptedSong;
import funkin.util.assets.DataAssets;
import haxe.DynamicAccess;
import haxe.Json;
import openfl.utils.Assets;
import thx.semver.Version;
using StringTools;
/**
* Contains utilities for loading and parsing stage data.
*/
class SongDataParser
{
/**
* A list containing all the songs available to the game.
*/
static final songCache:Map<String, Song> = new Map<String, Song>();
static final DEFAULT_SONG_ID = 'UNKNOWN';
static final SONG_DATA_PATH = 'songs/';
static final SONG_DATA_SUFFIX = '-metadata.json';
/**
* Parses and preloads the game's song metadata and scripts when the game starts.
*
* If you want to force song metadata to be reloaded, you can just call this function again.
*/
public static function loadSongCache():Void
{
clearSongCache();
trace("[SONGDATA] Loading song cache...");
//
// SCRIPTED SONGS
//
var scriptedSongClassNames:Array<String> = ScriptedSong.listScriptClasses();
trace(' Instantiating ${scriptedSongClassNames.length} scripted songs...');
for (songCls in scriptedSongClassNames)
{
var song:Song = ScriptedSong.init(songCls, DEFAULT_SONG_ID);
if (song != null)
{
trace(' Loaded scripted song: ${song.songId}');
songCache.set(song.songId, song);
}
else
{
trace(' Failed to instantiate scripted song class: ${songCls}');
}
}
//
// UNSCRIPTED SONGS
//
var songIdList:Array<String> = DataAssets.listDataFilesInPath(SONG_DATA_PATH, SONG_DATA_SUFFIX).map(function(songDataPath:String):String
{
return songDataPath.split('/')[0];
});
var unscriptedSongIds:Array<String> = songIdList.filter(function(songId:String):Bool
{
return !songCache.exists(songId);
});
trace(' Instantiating ${unscriptedSongIds.length} non-scripted songs...');
for (songId in unscriptedSongIds)
{
try
{
var song = new Song(songId);
if (song != null)
{
trace(' Loaded song data: ${song.songId}');
songCache.set(song.songId, song);
}
}
catch (e)
{
trace(' An error occurred while loading song data: ${songId}');
trace(e);
// Assume error was already logged.
continue;
}
}
trace(' Successfully loaded ${Lambda.count(songCache)} stages.');
}
/**
* Retrieves a particular song from the cache.
*/
public static function fetchSong(songId:String):Null<Song>
{
if (songCache.exists(songId))
{
var song:Song = songCache.get(songId);
trace('[STAGEDATA] Successfully fetch song: ${songId}');
return song;
}
else
{
trace('[STAGEDATA] Failed to fetch song, not found in cache: ${songId}');
return null;
}
}
static function clearSongCache():Void
{
if (songCache != null)
{
songCache.clear();
}
}
public static function parseSongMetadata(songId:String):Array<SongMetadata>
{
var result:Array<SongMetadata> = [];
var rawJson:String = loadSongMetadataFile(songId);
var jsonData:Dynamic = null;
try
{
jsonData = Json.parse(rawJson);
}
catch (e)
{
}
var songMetadata:SongMetadata = SongMigrator.migrateSongMetadata(jsonData, songId);
songMetadata = SongValidator.validateSongMetadata(songMetadata, songId);
if (songMetadata == null)
{
return result;
}
result.push(songMetadata);
var variations = songMetadata.playData.songVariations;
for (variation in variations)
{
var variationRawJson:String = loadSongMetadataFile(songId, variation);
var variationSongMetadata:SongMetadata = SongMigrator.migrateSongMetadata(variationRawJson, '${songId}_${variation}');
variationSongMetadata = SongValidator.validateSongMetadata(variationSongMetadata, '${songId}_${variation}');
if (variationSongMetadata != null)
{
variationSongMetadata.variation = variation;
result.push(variationSongMetadata);
}
}
return result;
}
static function loadSongMetadataFile(songPath:String, variation:String = ''):String
{
var songMetadataFilePath:String = (variation != '') ? Paths.json('$SONG_DATA_PATH$songPath/$songPath-metadata-$variation') : Paths.json('$SONG_DATA_PATH$songPath/$songPath-metadata');
var rawJson:String = Assets.getText(songMetadataFilePath).trim();
while (!rawJson.endsWith("}"))
{
rawJson = rawJson.substr(0, rawJson.length - 1);
}
return rawJson;
}
public static function parseSongChartData(songId:String, variation:String = ""):SongChartData
{
var rawJson:String = loadSongChartDataFile(songId, variation);
var jsonData:Dynamic = null;
try
{
jsonData = Json.parse(rawJson);
}
catch (e)
{
}
var songChartData:SongChartData = SongMigrator.migrateSongChartData(jsonData, songId);
songChartData = SongValidator.validateSongChartData(songChartData, songId);
if (songChartData == null)
{
trace('Failed to validate song chart data: ${songId}');
return null;
}
return songChartData;
}
static function loadSongChartDataFile(songPath:String, variation:String = ''):String
{
var songChartDataFilePath:String = (variation != '') ? Paths.json('$SONG_DATA_PATH$songPath/$songPath-chart-$variation') : Paths.json('$SONG_DATA_PATH$songPath/$songPath-chart');
var rawJson:String = Assets.getText(songChartDataFilePath).trim();
while (!rawJson.endsWith("}"))
{
rawJson = rawJson.substr(0, rawJson.length - 1);
}
return rawJson;
}
}
typedef SongMetadata =
{
/**
* A semantic versioning string for the song data format.
*
*/
var version:Version;
var songName:String;
var artist:String;
var timeFormat:SongTimeFormat;
var divisions:Int;
var timeChanges:Array<SongTimeChange>;
var loop:Bool;
var playData:SongPlayData;
var generatedBy:String;
/**
* Defaults to ''. Populated later.
*/
var variation:String;
};
typedef SongPlayData =
{
var songVariations:Array<String>;
var difficulties:Array<String>;
/**
* Keys are the player characters and the values give info on what opponent/GF/inst to use.
*/
var playableChars:DynamicAccess<SongPlayableChar>;
var stage:String;
var noteSkin:String;
}
typedef RawSongPlayableChar =
{
var g:String;
var o:String;
var i:String;
}
typedef RawSongNoteData =
{
/**
* The timestamp of the note. The timestamp is in the format of the song's time format.
*/
var t:Float;
/**
* Data for the note. Represents the index on the strumline.
* 0 = left, 1 = down, 2 = up, 3 = right
* `floor(direction / strumlineSize)` specifies which strumline the note is on.
* 0 = player, 1 = opponent, etc.
*/
var d:Int;
/**
* Length of the note, if applicable.
* Defaults to 0 for single notes.
*/
var l:Float;
/**
* The kind of the note.
* This can allow the note to include information used for custom behavior.
* Defaults to blank or `"normal"`.
*/
var k:String;
}
abstract SongNoteData(RawSongNoteData)
{
public function new(time:Float, data:Int, length:Float = 0, kind:String = "")
{
this = {
t: time,
d: data,
l: length,
k: kind
};
}
public var time(get, set):Float;
public function get_time():Float
{
return this.t;
}
public function set_time(value:Float):Float
{
return this.t = value;
}
/**
* The raw data for the note.
*/
public var data(get, set):Int;
public function get_data():Int
{
return this.d;
}
public function set_data(value:Int):Int
{
return this.d = value;
}
/**
* The direction of the note, if applicable.
* Strips the strumline index from the data.
*
* 0 = left, 1 = down, 2 = up, 3 = right
*/
public inline function getDirection(strumlineSize:Int = 4):Int
{
return this.d % strumlineSize;
}
/**
* The strumline index of the note, if applicable.
* Strips the direction from the data.
*
* 0 = player, 1 = opponent, etc.
*/
public inline function getStrumlineIndex(strumlineSize:Int = 4):Int
{
return Math.floor(this.d / strumlineSize);
}
public inline function getMustHitNote(strumlineSize:Int = 4):Bool
{
return getStrumlineIndex(strumlineSize) == 0;
}
public var length(get, set):Float;
public function get_length():Float
{
return this.l;
}
public function set_length(value:Float):Float
{
return this.l = value;
}
public var kind(get, set):String;
public function get_kind():String
{
if (this.k == null || this.k == '')
return 'normal';
return this.k;
}
public function set_kind(value:String):String
{
if (value == 'normal' || value == '')
value = null;
return this.k = value;
}
}
typedef RawSongEventData =
{
/**
* The timestamp of the event. The timestamp is in the format of the song's time format.
*/
var t:Float;
/**
* The kind of the event.
* Examples include "FocusCamera" and "PlayAnimation"
* Custom events can be added by scripts with the `ScriptedSongEvent` class.
*/
var e:String;
/**
* The data for the event.
* This can allow the event to include information used for custom behavior.
* Data type depends on the event kind. It can be anything that's JSON serializable.
*/
var v:Dynamic;
}
abstract SongEventData(RawSongEventData)
{
public function new(time:Float, event:String, value:Dynamic = null)
{
this = {
t: time,
e: event,
v: value
};
}
public var time(get, set):Float;
public function get_time():Float
{
return this.t;
}
public function set_time(value:Float):Float
{
return this.t = value;
}
public var event(get, set):String;
public function get_event():String
{
return this.e;
}
public function set_event(value:String):String
{
return this.e = value;
}
public var value(get, set):Dynamic;
public function get_value():Dynamic
{
return this.v;
}
public function set_value(value:Dynamic):Dynamic
{
return this.v = value;
}
public inline function getBool():Bool
{
return cast this.v;
}
public inline function getInt():Int
{
return cast this.v;
}
public inline function getFloat():Float
{
return cast this.v;
}
public inline function getString():String
{
return cast this.v;
}
public inline function getArray():Array<Dynamic>
{
return cast this.v;
}
public inline function getMap():DynamicAccess<Dynamic>
{
return cast this.v;
}
public inline function getBoolArray():Array<Bool>
{
return cast this.v;
}
}
abstract SongPlayableChar(RawSongPlayableChar)
{
public function new(girlfriend:String, opponent:String, inst:String = "")
{
this = {
g: girlfriend,
o: opponent,
i: inst
};
}
public var girlfriend(get, set):String;
public function get_girlfriend():String
{
return this.g;
}
public function set_girlfriend(value:String):String
{
return this.g = value;
}
public var opponent(get, set):String;
public function get_opponent():String
{
return this.o;
}
public function set_opponent(value:String):String
{
return this.o = value;
}
public var inst(get, set):String;
public function get_inst():String
{
return this.i;
}
public function set_inst(value:String):String
{
return this.i = value;
}
}
typedef RawSongChartData =
{
var version:Version;
var scrollSpeed:DynamicAccess<Float>;
var events:Array<SongEventData>;
var notes:DynamicAccess<Array<SongNoteData>>;
var generatedBy:String;
};
@:forward
abstract SongChartData(RawSongChartData)
{
public function new(scrollSpeed:DynamicAccess<Float>, events:Array<SongEventData>, notes:DynamicAccess<Array<SongNoteData>>)
{
this = {
version: SongMigrator.CHART_VERSION,
events: events,
notes: notes,
scrollSpeed: scrollSpeed,
generatedBy: SongValidator.DEFAULT_GENERATEDBY
}
}
public function getScrollSpeed(diff:String = 'default'):Float
{
var result:Float = this.scrollSpeed.get(diff);
if (result == 0.0 && diff != 'default')
return getScrollSpeed('default');
return (result == 0.0) ? 1.0 : result;
}
}
typedef RawSongTimeChange =
{
/**
* Timestamp in specified `timeFormat`.
*/
var t:Float;
/**
* Time in beats (int). The game will calculate further beat values based on this one,
* so it can do it in a simple linear fashion.
*/
var b:Int;
/**
* Quarter notes per minute (float). Cannot be empty in the first element of the list,
* but otherwise it's optional, and defaults to the value of the previous element.
*/
var bpm:Float;
/**
* Time signature numerator (int). Optional, defaults to 4.
*/
var n:Int;
/**
* Time signature denominator (int). Optional, defaults to 4. Should only ever be a power of two.
*/
var d:Int;
/**
* Beat tuplets (Array<int> or int). This defines how many steps each beat is divided into.
* It can either be an array of length `n` (see above) or a single integer number.
* Optional, defaults to `[4]`.
*/
var bt:OneOfTwo<Int, Array<Int>>;
}
/**
* Add aliases to the minimalized property names of the typedef,
* to improve readability.
*/
abstract SongTimeChange(RawSongTimeChange)
{
public function new(timeStamp:Float, beatTime:Int, bpm:Float, timeSignatureNum:Int = 4, timeSignatureDen:Int = 4, beatTuplets:Array<Int>)
{
this = {
t: timeStamp,
b: beatTime,
bpm: bpm,
n: timeSignatureNum,
d: timeSignatureDen,
bt: beatTuplets,
}
}
public var timeStamp(get, set):Float;
public function get_timeStamp():Float
{
return this.t;
}
public function set_timeStamp(value:Float):Float
{
return this.t = value;
}
public var beatTime(get, set):Int;
public function get_beatTime():Int
{
return this.b;
}
public function set_beatTime(value:Int):Int
{
return this.b = value;
}
public var bpm(get, set):Float;
public function get_bpm():Float
{
return this.bpm;
}
public function set_bpm(value:Float):Float
{
return this.bpm = value;
}
public var timeSignatureNum(get, set):Int;
public function get_timeSignatureNum():Int
{
return this.n;
}
public function set_timeSignatureNum(value:Int):Int
{
return this.n = value;
}
public var timeSignatureDen(get, set):Int;
public function get_timeSignatureDen():Int
{
return this.d;
}
public function set_timeSignatureDen(value:Int):Int
{
return this.d = value;
}
public var beatTuplets(get, set):Array<Int>;
public function get_beatTuplets():Array<Int>
{
if (Std.isOfType(this.bt, Int))
{
return [this.bt];
}
else
{
return this.bt;
}
}
public function set_beatTuplets(value:Array<Int>):Array<Int>
{
return this.bt = value;
}
}
enum abstract SongTimeFormat(String) from String to String
{
var TICKS = "ticks";
var FLOAT = "float";
var MILLISECONDS = "ms";
}