mirror of
https://github.com/ninjamuffin99/Funkin.git
synced 2024-12-25 22:47:52 +00:00
Merge remote-tracking branch 'origin/bugfix-squash/song-data-parser-rework' into HEAD
This commit is contained in:
commit
ef1cb10bb3
6
docs/troubleshooting.md
Normal file
6
docs/troubleshooting.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
# Troubleshooting Common Issues
|
||||
|
||||
- Weird macro error with a very tall call stack: Restart Visual Studio Code
|
||||
- `Get Thread Context Failed`: Turn off other expensive applications while building
|
||||
- `Type not found: T1`: This is thrown by `json2object`, make sure the data type of `@:default` is correct.
|
||||
- NOTE: `flixel.util.typeLimit.OneOfTwo` isn't supported.
|
8
hmm.json
8
hmm.json
|
@ -95,8 +95,10 @@
|
|||
},
|
||||
{
|
||||
"name": "json2object",
|
||||
"type": "haxelib",
|
||||
"version": "3.11.0"
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "429986134031cbb1980f09d0d3d642b4b4cbcd6a",
|
||||
"url": "https://github.com/elnabo/json2object"
|
||||
},
|
||||
{
|
||||
"name": "lime",
|
||||
|
@ -158,4 +160,4 @@
|
|||
"version": "0.11.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ import funkin.util.Constants;
|
|||
import flixel.util.FlxSignal;
|
||||
import flixel.math.FlxMath;
|
||||
import funkin.play.song.Song.SongDifficulty;
|
||||
import funkin.play.song.SongData.SongTimeChange;
|
||||
import funkin.data.song.SongData.SongTimeChange;
|
||||
|
||||
/**
|
||||
* A core class which handles musical timing throughout the game,
|
||||
|
|
|
@ -37,7 +37,7 @@ class DialogueBox extends FlxSpriteGroup
|
|||
{
|
||||
super();
|
||||
|
||||
switch (PlayState.instance.currentSong.songId.toLowerCase())
|
||||
switch (PlayState.instance.currentSong.id.toLowerCase())
|
||||
{
|
||||
case 'senpai':
|
||||
FlxG.sound.playMusic(Paths.music('Lunchbox'), 0);
|
||||
|
@ -78,7 +78,7 @@ class DialogueBox extends FlxSpriteGroup
|
|||
box = new FlxSprite(-20, 45);
|
||||
|
||||
var hasDialog:Bool = false;
|
||||
switch (PlayState.instance.currentSong.songId.toLowerCase())
|
||||
switch (PlayState.instance.currentSong.id.toLowerCase())
|
||||
{
|
||||
case 'senpai':
|
||||
hasDialog = true;
|
||||
|
@ -150,8 +150,8 @@ class DialogueBox extends FlxSpriteGroup
|
|||
override function update(elapsed:Float):Void
|
||||
{
|
||||
// HARD CODING CUZ IM STUPDI
|
||||
if (PlayState.instance.currentSong.songId.toLowerCase() == 'roses') portraitLeft.visible = false;
|
||||
if (PlayState.instance.currentSong.songId.toLowerCase() == 'thorns')
|
||||
if (PlayState.instance.currentSong.id.toLowerCase() == 'roses') portraitLeft.visible = false;
|
||||
if (PlayState.instance.currentSong.id.toLowerCase() == 'thorns')
|
||||
{
|
||||
portraitLeft.color = FlxColor.BLACK;
|
||||
swagDialogue.color = FlxColor.WHITE;
|
||||
|
@ -187,8 +187,8 @@ class DialogueBox extends FlxSpriteGroup
|
|||
{
|
||||
isEnding = true;
|
||||
|
||||
if (PlayState.instance.currentSong.songId.toLowerCase() == 'senpai'
|
||||
|| PlayState.instance.currentSong.songId.toLowerCase() == 'thorns') FlxG.sound.music.fadeOut(2.2, 0);
|
||||
if (PlayState.instance.currentSong.id.toLowerCase() == 'senpai'
|
||||
|| PlayState.instance.currentSong.id.toLowerCase() == 'thorns') FlxG.sound.music.fadeOut(2.2, 0);
|
||||
|
||||
new FlxTimer().start(0.2, function(tmr:FlxTimer) {
|
||||
box.alpha -= 1 / 5;
|
||||
|
|
|
@ -20,6 +20,7 @@ import flixel.text.FlxText;
|
|||
import flixel.tweens.FlxEase;
|
||||
import flixel.tweens.FlxTween;
|
||||
import flixel.util.FlxColor;
|
||||
import funkin.data.song.SongRegistry;
|
||||
import flixel.util.FlxSpriteUtil;
|
||||
import flixel.util.FlxTimer;
|
||||
import funkin.Controls.Control;
|
||||
|
@ -30,7 +31,6 @@ import funkin.freeplayStuff.LetterSort;
|
|||
import funkin.freeplayStuff.SongMenuItem;
|
||||
import funkin.play.HealthIcon;
|
||||
import funkin.play.PlayState;
|
||||
import funkin.play.song.SongData.SongDataParser;
|
||||
import funkin.shaderslmfao.AngleMask;
|
||||
import funkin.shaderslmfao.PureColor;
|
||||
import funkin.shaderslmfao.StrokeShader;
|
||||
|
@ -843,7 +843,8 @@ class FreeplayState extends MusicBeatSubState
|
|||
}*/
|
||||
|
||||
PlayStatePlaylist.isStoryMode = false;
|
||||
var targetSong:Song = SongDataParser.fetchSong(songs[curSelected].songName.toLowerCase());
|
||||
var songId:String = songs[curSelected].songName.toLowerCase();
|
||||
var targetSong:Song = SongRegistry.instance.fetchEntry(songId);
|
||||
var targetDifficulty:String = switch (curDifficulty)
|
||||
{
|
||||
case 0:
|
||||
|
|
|
@ -17,11 +17,11 @@ import funkin.play.PlayStatePlaylist;
|
|||
import openfl.display.BitmapData;
|
||||
import funkin.data.level.LevelRegistry;
|
||||
import funkin.data.notestyle.NoteStyleRegistry;
|
||||
import funkin.play.event.SongEventData.SongEventParser;
|
||||
import funkin.data.event.SongEventData.SongEventParser;
|
||||
import funkin.play.cutscene.dialogue.ConversationDataParser;
|
||||
import funkin.play.cutscene.dialogue.DialogueBoxDataParser;
|
||||
import funkin.play.cutscene.dialogue.SpeakerDataParser;
|
||||
import funkin.play.song.SongData.SongDataParser;
|
||||
import funkin.data.song.SongRegistry;
|
||||
import funkin.play.stage.StageData.StageDataParser;
|
||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||
import funkin.modding.module.ModuleHandler;
|
||||
|
@ -197,13 +197,13 @@ class InitState extends FlxState
|
|||
|
||||
// NOTE: Registries and data parsers must be imported and not referenced with fully qualified names,
|
||||
// to ensure build macros work properly.
|
||||
SongRegistry.instance.loadEntries();
|
||||
LevelRegistry.instance.loadEntries();
|
||||
NoteStyleRegistry.instance.loadEntries();
|
||||
SongEventParser.loadEventCache();
|
||||
ConversationDataParser.loadConversationCache();
|
||||
DialogueBoxDataParser.loadDialogueBoxCache();
|
||||
SpeakerDataParser.loadSpeakerCache();
|
||||
SongDataParser.loadSongCache();
|
||||
StageDataParser.loadStageCache();
|
||||
CharacterDataParser.loadCharacterCache();
|
||||
ModuleHandler.buildModuleCallbacks();
|
||||
|
@ -276,7 +276,7 @@ class InitState extends FlxState
|
|||
*/
|
||||
function startSong(songId:String, difficultyId:String = 'normal'):Void
|
||||
{
|
||||
var songData:funkin.play.song.Song = funkin.play.song.SongData.SongDataParser.fetchSong(songId);
|
||||
var songData:funkin.play.song.Song = funkin.data.song.SongRegistry.instance.fetchEntry(songId);
|
||||
|
||||
if (songData == null)
|
||||
{
|
||||
|
@ -312,7 +312,7 @@ class InitState extends FlxState
|
|||
|
||||
var targetSongId:String = PlayStatePlaylist.playlistSongIds.shift();
|
||||
|
||||
var targetSong:funkin.play.song.Song = funkin.play.song.SongData.SongDataParser.fetchSong(targetSongId);
|
||||
var targetSong:funkin.play.song.Song = SongRegistry.instance.fetchEntry(targetSongId);
|
||||
|
||||
LoadingState.loadAndSwitchState(new funkin.play.PlayState(
|
||||
{
|
||||
|
|
|
@ -159,7 +159,7 @@ class LoadingState extends MusicBeatState
|
|||
|
||||
static function getSongPath():String
|
||||
{
|
||||
return Paths.inst(PlayState.instance.currentSong.songId);
|
||||
return Paths.inst(PlayState.instance.currentSong.id);
|
||||
}
|
||||
|
||||
inline static public function loadAndSwitchState(nextState:FlxState, shouldStopMusic = false):Void
|
||||
|
|
|
@ -10,7 +10,7 @@ import flixel.tweens.FlxEase;
|
|||
import flixel.tweens.FlxTween;
|
||||
import flixel.util.FlxColor;
|
||||
import funkin.play.PlayState;
|
||||
import funkin.play.song.SongData.SongDataParser;
|
||||
import funkin.data.song.SongRegistry;
|
||||
|
||||
class PauseSubState extends MusicBeatSubState
|
||||
{
|
||||
|
@ -197,7 +197,7 @@ class PauseSubState extends MusicBeatSubState
|
|||
regenMenu();
|
||||
|
||||
case 'EASY' | 'NORMAL' | 'HARD' | 'ERECT':
|
||||
PlayState.instance.currentSong = SongDataParser.fetchSong(PlayState.instance.currentSong.songId.toLowerCase());
|
||||
PlayState.instance.currentSong = SongRegistry.instance.fetchEntry(PlayState.instance.currentSong.id.toLowerCase());
|
||||
|
||||
PlayState.instance.currentDifficulty = daSelected.toLowerCase();
|
||||
|
||||
|
|
|
@ -4,6 +4,9 @@ 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.
|
||||
|
@ -135,7 +138,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
|
|||
|
||||
public function fetchEntryVersion(id:String):Null<thx.semver.Version>
|
||||
{
|
||||
var entryStr:String = loadEntryFile(id);
|
||||
var entryStr:String = loadEntryFile(id).contents;
|
||||
var entryVersion:thx.semver.Version = VersionUtil.getVersionFromJSON(entryStr);
|
||||
return entryVersion;
|
||||
}
|
||||
|
@ -145,11 +148,14 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
|
|||
trace('[' + registryId + '] ' + message);
|
||||
}
|
||||
|
||||
function loadEntryFile(id:String):String
|
||||
function loadEntryFile(id:String):JsonFile
|
||||
{
|
||||
var entryFilePath:String = Paths.json('${dataFilePath}/${id}');
|
||||
var rawJson:String = openfl.Assets.getText(entryFilePath).trim();
|
||||
return rawJson;
|
||||
return {
|
||||
fileName: entryFilePath,
|
||||
contents: rawJson
|
||||
};
|
||||
}
|
||||
|
||||
function clearEntries():Void
|
||||
|
@ -188,7 +194,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
|
|||
}
|
||||
else
|
||||
{
|
||||
throw '[${registryId}] Entry ${id} does not support migration.';
|
||||
throw '[${registryId}] Entry ${id} does not support migration to version ${versionRule}.';
|
||||
}
|
||||
|
||||
// Example:
|
||||
|
@ -219,4 +225,85 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
|
|||
* @param clsName
|
||||
*/
|
||||
abstract function createScriptedEntry(clsName:String):Null<T>;
|
||||
|
||||
function printErrors(errors:Array<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}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typedef JsonFile =
|
||||
{
|
||||
fileName:String,
|
||||
contents:String
|
||||
};
|
||||
|
|
99
source/funkin/data/DataParse.hx
Normal file
99
source/funkin/data/DataParse.hx
Normal file
|
@ -0,0 +1,99 @@
|
|||
package funkin.data;
|
||||
|
||||
import hxjsonast.Json;
|
||||
import hxjsonast.Json.JObjectField;
|
||||
|
||||
/**
|
||||
* `json2object` has an annotation `@:jcustomparse` which allows for mutation of parsed values.
|
||||
*
|
||||
* It also allows for validation, since throwing an error in this function will cause the issue to be properly caught.
|
||||
* Parsing will fail and `parser.errors` will contain the thrown exception.
|
||||
*
|
||||
* Functions must be of the signature `(hxjsonast.Json, String) -> T`, where the String is the property name and `T` is the type of the property.
|
||||
*/
|
||||
class DataParse
|
||||
{
|
||||
/**
|
||||
* `@:jcustomparse(funkin.data.DataParse.stringNotEmpty)`
|
||||
* @param json Contains the `pos` and `value` of the property.
|
||||
* @param name The name of the property.
|
||||
* @throws If the property is not a string or is empty.
|
||||
*/
|
||||
public static function stringNotEmpty(json:Json, name:String):String
|
||||
{
|
||||
switch (json.value)
|
||||
{
|
||||
case JString(s):
|
||||
if (s == "") throw 'Expected property $name to be non-empty.';
|
||||
return s;
|
||||
default:
|
||||
throw 'Expected property $name to be a string, but it was ${json.value}.';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parser which outputs a Dynamic value, either a object or something else.
|
||||
* @param json
|
||||
* @param name
|
||||
* @return The value of the property.
|
||||
*/
|
||||
public static function dynamicValue(json:Json, name:String):Dynamic
|
||||
{
|
||||
return jsonToDynamic(json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parser which outputs a Dynamic value, which must be an object with properties.
|
||||
* @param json
|
||||
* @param name
|
||||
* @return Dynamic
|
||||
*/
|
||||
public static function dynamicObject(json:Json, name:String):Dynamic
|
||||
{
|
||||
switch (json.value)
|
||||
{
|
||||
case JObject(fields):
|
||||
return jsonFieldsToDynamicObject(fields);
|
||||
default:
|
||||
throw 'Expected property $name to be an object, but it was ${json.value}.';
|
||||
}
|
||||
}
|
||||
|
||||
static function jsonToDynamic(json:Json):Null<Dynamic>
|
||||
{
|
||||
return switch (json.value)
|
||||
{
|
||||
case JString(s): s;
|
||||
case JNumber(n): n;
|
||||
case JBool(b): b;
|
||||
case JNull: null;
|
||||
case JObject(fields): jsonFieldsToDynamicObject(fields);
|
||||
case JArray(values): jsonArrayToDynamicArray(values);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Array of JSON fields `[{key, value}, {key, value}]` to a Dynamic object `{key:value, key:value}`.
|
||||
* @param fields
|
||||
* @return Dynamic
|
||||
*/
|
||||
static function jsonFieldsToDynamicObject(fields:Array<JObjectField>):Dynamic
|
||||
{
|
||||
var result:Dynamic = {};
|
||||
for (field in fields)
|
||||
{
|
||||
Reflect.setField(result, field.name, field.value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Array of JSON elements `[Json, Json, Json]` to a Dynamic array `[String, Object, Int, Array]`
|
||||
* @param jsons
|
||||
* @return Array<Dynamic>
|
||||
*/
|
||||
static function jsonArrayToDynamicArray(jsons:Array<Json>):Array<Null<Dynamic>>
|
||||
{
|
||||
return [for (json in jsons) jsonToDynamic(json)];
|
||||
}
|
||||
}
|
8
source/funkin/data/DataWrite.hx
Normal file
8
source/funkin/data/DataWrite.hx
Normal file
|
@ -0,0 +1,8 @@
|
|||
package funkin.data;
|
||||
|
||||
/**
|
||||
* `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 {}
|
|
@ -15,5 +15,6 @@ interface IRegistryEntry<T>
|
|||
|
||||
// Can't make an interface field private I guess.
|
||||
public final _data:T;
|
||||
public function _fetchData(id:String):Null<T>;
|
||||
// Can't make a static field required by an interface I guess.
|
||||
// private static function _fetchData(id:String):Null<T>;
|
||||
}
|
||||
|
|
21
source/funkin/data/README.md
Normal file
21
source/funkin/data/README.md
Normal file
|
@ -0,0 +1,21 @@
|
|||
# funkin.data
|
||||
|
||||
Data structures are parsed using `json2object`, which uses macros to generate parser classes based on anonymous structures OR classes.
|
||||
|
||||
Parsing errors will be returned in `parser.errors`. See `json2object.Error` for an enumeration of possible parsing errors. If an error occurred, `parser.value` will be null.
|
||||
|
||||
The properties of these anonymous structures can have their behavior changed with annotations:
|
||||
|
||||
- `@:optional`: The value is optional and will not throw a parsing error if it is not present in the JSON data.
|
||||
- `@:default("test")`: If the value is optional, this value will be used instead of `null`. Replace `"test"` with a value of the property's type.
|
||||
- `@:default(auto)`: If the value is an anonymous structure with `json2object` annotations, each field will be initialized to its default value.
|
||||
- `@:jignored`: This value will be ignored by the parser. Their presence will not be checked in the JSON data and their values will not be parsed.
|
||||
- `@:alias`: Choose the name the value will use in the JSON data to be separate from the property name. Useful if the desired name is a reserved word like `public`.
|
||||
- `@:jcustomparse`: Provide a custom function for parsing from a JSON string into a value.
|
||||
- Functions must be of the signature `(hxjsonast.Json, String) -> T`, where the String is the property name and `T` is the type of the property.
|
||||
- `hxjsonast.Json` contains a `pos` and a `value`, with `value` being an enum: https://nadako.github.io/hxjsonast/hxjsonast/JsonValue.html
|
||||
- Errors thrown in this function will cause a parsing error (`CustomFunctionException`) along with a position!
|
||||
- Make sure to provide the FULLY QUALIFIED path to the custom function.
|
||||
- `@:jcustomwrite`: Provide a custom function for serializing the property into a string for storage as JSON.
|
||||
- Functions must be of the signature `(T) -> String`, where `T` is the type of the property.
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
package funkin.play.event;
|
||||
package funkin.data.event;
|
||||
|
||||
import funkin.play.event.SongEventData.SongEventSchema;
|
||||
import funkin.play.song.SongData.SongEventData;
|
||||
import funkin.play.event.SongEvent;
|
||||
import funkin.data.event.SongEventData.SongEventSchema;
|
||||
import funkin.data.song.SongData.SongEventData;
|
||||
import funkin.util.macro.ClassMacro;
|
||||
import funkin.play.event.ScriptedSongEvent;
|
||||
|
|
@ -24,6 +24,7 @@ typedef LevelData =
|
|||
/**
|
||||
* The graphic for the level, as seen in the scrolling list.
|
||||
*/
|
||||
@:jcustomparse(funkin.data.DataParse.stringNotEmpty)
|
||||
var titleAsset:String;
|
||||
|
||||
@:default([])
|
||||
|
@ -40,6 +41,7 @@ typedef LevelPropData =
|
|||
/**
|
||||
* The image to use for the prop. May optionally be a sprite sheet.
|
||||
*/
|
||||
// @:jcustomparse(funkin.data.DataParse.stringNotEmpty)
|
||||
var assetPath:String;
|
||||
|
||||
/**
|
||||
|
|
|
@ -30,17 +30,18 @@ class LevelRegistry extends BaseRegistry<Level, LevelData>
|
|||
// JsonParser does not take type parameters,
|
||||
// otherwise this function would be in BaseRegistry.
|
||||
var parser = new json2object.JsonParser<LevelData>();
|
||||
var jsonStr:String = loadEntryFile(id);
|
||||
|
||||
parser.fromJson(jsonStr);
|
||||
switch (loadEntryFile(id))
|
||||
{
|
||||
case {fileName: fileName, contents: contents}:
|
||||
parser.fromJson(contents, fileName);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
{
|
||||
trace('[${registryId}] Failed to parse entry data: ${id}');
|
||||
for (error in parser.errors)
|
||||
{
|
||||
trace(error);
|
||||
}
|
||||
printErrors(parser.errors, id);
|
||||
return null;
|
||||
}
|
||||
return parser.value;
|
||||
|
|
|
@ -34,22 +34,21 @@ class NoteStyleRegistry extends BaseRegistry<NoteStyle, NoteStyleData>
|
|||
*/
|
||||
public function parseEntryData(id:String):Null<NoteStyleData>
|
||||
{
|
||||
if (id == null) id = DEFAULT_NOTE_STYLE_ID;
|
||||
|
||||
// JsonParser does not take type parameters,
|
||||
// otherwise this function would be in BaseRegistry.
|
||||
var parser = new json2object.JsonParser<NoteStyleData>();
|
||||
var jsonStr:String = loadEntryFile(id);
|
||||
|
||||
parser.fromJson(jsonStr);
|
||||
switch (loadEntryFile(id))
|
||||
{
|
||||
case {fileName: fileName, contents: contents}:
|
||||
parser.fromJson(contents, fileName);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
{
|
||||
trace('[${registryId}] Failed to parse entry data: ${id}');
|
||||
for (error in parser.errors)
|
||||
{
|
||||
trace(error);
|
||||
}
|
||||
printErrors(parser.errors, id);
|
||||
return null;
|
||||
}
|
||||
return parser.value;
|
||||
|
|
649
source/funkin/data/song/SongData.hx
Normal file
649
source/funkin/data/song/SongData.hx
Normal file
|
@ -0,0 +1,649 @@
|
|||
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;
|
||||
|
||||
class SongMetadata
|
||||
{
|
||||
/**
|
||||
* A semantic versioning string for the song data format.
|
||||
*
|
||||
*/
|
||||
// @:default(funkin.data.song.SongRegistry.SONG_METADATA_VERSION)
|
||||
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;
|
||||
|
||||
/**
|
||||
* Data relating to the song's gameplay.
|
||||
*/
|
||||
public var playData:SongPlayData;
|
||||
|
||||
// @:default(funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY)
|
||||
public var generatedBy:String;
|
||||
|
||||
// @:default(funkin.data.song.SongData.SongTimeFormat.MILLISECONDS)
|
||||
public var timeFormat:SongTimeFormat;
|
||||
|
||||
// @:default(funkin.data.song.SongData.SongTimeChange.DEFAULT_SONGTIMECHANGES)
|
||||
public var timeChanges:Array<SongTimeChange>;
|
||||
|
||||
/**
|
||||
* Defaults to `default` or `''`. Populated later.
|
||||
*/
|
||||
@:jignored
|
||||
public var variation:String = 'default';
|
||||
|
||||
public function new(songName:String, artist:String, variation:String = 'default')
|
||||
{
|
||||
this.version = SongMigrator.CHART_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.generatedBy = SongRegistry.DEFAULT_GENERATEDBY;
|
||||
// Variation ID.
|
||||
this.variation = variation;
|
||||
}
|
||||
|
||||
public function clone(?newVariation:String = null):SongMetadata
|
||||
{
|
||||
var result:SongMetadata = new SongMetadata(this.songName, this.artist, newVariation == null ? this.variation : newVariation);
|
||||
result.version = this.version;
|
||||
result.timeFormat = this.timeFormat;
|
||||
result.divisions = this.divisions;
|
||||
result.timeChanges = this.timeChanges;
|
||||
result.looped = this.looped;
|
||||
result.playData = this.playData;
|
||||
result.generatedBy = this.generatedBy;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
enum abstract SongTimeFormat(String) from String to String
|
||||
{
|
||||
var TICKS = 'ticks';
|
||||
var FLOAT = 'float';
|
||||
var MILLISECONDS = 'ms';
|
||||
}
|
||||
|
||||
class SongTimeChange
|
||||
{
|
||||
public static final DEFAULT_SONGTIMECHANGE:SongTimeChange = new SongTimeChange(0, 100);
|
||||
|
||||
public static final DEFAULT_SONGTIMECHANGES:Array<SongTimeChange> = [DEFAULT_SONGTIMECHANGE];
|
||||
|
||||
static final DEFAULT_BEAT_TUPLETS:Array<Int> = [4, 4, 4, 4];
|
||||
static final DEFAULT_BEAT_TIME:Null<Float> = null; // Later, null gets detected and recalculated.
|
||||
|
||||
/**
|
||||
* Timestamp in specified `timeFormat`.
|
||||
*/
|
||||
@:alias("t")
|
||||
public var timeStamp: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.
|
||||
*/
|
||||
@:optional
|
||||
@:alias("b")
|
||||
// @:default(funkin.data.song.SongData.SongTimeChange.DEFAULT_BEAT_TIME)
|
||||
public var beatTime:Null<Float>;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@:alias("bpm")
|
||||
public var bpm:Float;
|
||||
|
||||
/**
|
||||
* Time signature numerator (int). Optional, defaults to 4.
|
||||
*/
|
||||
@:default(4)
|
||||
@:optional
|
||||
@:alias("n")
|
||||
public var timeSignatureNum:Int;
|
||||
|
||||
/**
|
||||
* Time signature denominator (int). Optional, defaults to 4. Should only ever be a power of two.
|
||||
*/
|
||||
@:default(4)
|
||||
@:optional
|
||||
@:alias("d")
|
||||
public var timeSignatureDen: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]`.
|
||||
*/
|
||||
@:optional
|
||||
@:alias("bt")
|
||||
public var beatTuplets:Array<Int>;
|
||||
|
||||
public function new(timeStamp:Float, bpm:Float, timeSignatureNum:Int = 4, timeSignatureDen:Int = 4, ?beatTime:Float, ?beatTuplets:Array<Int>)
|
||||
{
|
||||
this.timeStamp = timeStamp;
|
||||
this.bpm = bpm;
|
||||
|
||||
this.timeSignatureNum = timeSignatureNum;
|
||||
this.timeSignatureDen = timeSignatureDen;
|
||||
|
||||
this.beatTime = beatTime == null ? DEFAULT_BEAT_TIME : beatTime;
|
||||
this.beatTuplets = beatTuplets == null ? DEFAULT_BEAT_TUPLETS : beatTuplets;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata for a song only used for the music.
|
||||
* For example, the menu music.
|
||||
*/
|
||||
class SongMusicData
|
||||
{
|
||||
/**
|
||||
* A semantic versioning string for the song data format.
|
||||
*
|
||||
*/
|
||||
// @:default(funkin.data.song.SongRegistry.SONG_METADATA_VERSION)
|
||||
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;
|
||||
|
||||
// @:default(funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY)
|
||||
public var generatedBy:String;
|
||||
|
||||
// @:default(funkin.data.song.SongData.SongTimeFormat.MILLISECONDS)
|
||||
public var timeFormat:SongTimeFormat;
|
||||
|
||||
// @:default(funkin.data.song.SongData.SongTimeChange.DEFAULT_SONGTIMECHANGES)
|
||||
public var timeChanges:Array<SongTimeChange>;
|
||||
|
||||
/**
|
||||
* Defaults to `default` or `''`. Populated later.
|
||||
*/
|
||||
@:jignored
|
||||
public var variation:String = 'default';
|
||||
|
||||
public function new(songName:String, artist:String, variation:String = 'default')
|
||||
{
|
||||
this.version = SongMigrator.CHART_VERSION;
|
||||
this.songName = songName;
|
||||
this.artist = artist;
|
||||
this.timeFormat = 'ms';
|
||||
this.divisions = null;
|
||||
this.timeChanges = [new SongTimeChange(0, 100)];
|
||||
this.looped = false;
|
||||
this.generatedBy = SongRegistry.DEFAULT_GENERATEDBY;
|
||||
// Variation ID.
|
||||
this.variation = variation;
|
||||
}
|
||||
|
||||
public function clone(?newVariation:String = null):SongMusicData
|
||||
{
|
||||
var result:SongMusicData = new SongMusicData(this.songName, this.artist, newVariation == null ? this.variation : newVariation);
|
||||
result.version = this.version;
|
||||
result.timeFormat = this.timeFormat;
|
||||
result.divisions = this.divisions;
|
||||
result.timeChanges = this.timeChanges;
|
||||
result.looped = this.looped;
|
||||
result.generatedBy = this.generatedBy;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
typedef SongPlayData =
|
||||
{
|
||||
public var songVariations:Array<String>;
|
||||
public var difficulties:Array<String>;
|
||||
|
||||
/**
|
||||
* Keys are the player characters and the values give info on what opponent/GF/inst to use.
|
||||
*/
|
||||
public var playableChars:Map<String, SongPlayableChar>;
|
||||
|
||||
public var stage:String;
|
||||
public var noteSkin:String;
|
||||
}
|
||||
|
||||
class SongPlayableChar
|
||||
{
|
||||
@: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;
|
||||
}
|
||||
}
|
||||
|
||||
class SongChartData
|
||||
{
|
||||
@:default(funkin.data.song.SongRegistry.SONG_CHART_DATA_VERSION)
|
||||
public var version:Version;
|
||||
|
||||
public var scrollSpeed:Map<String, Float>;
|
||||
public var events:Array<SongEventData>;
|
||||
public var notes:Map<String, Array<SongNoteData>>;
|
||||
|
||||
@:default(funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY)
|
||||
public var generatedBy:String;
|
||||
|
||||
public function new(scrollSpeed:Map<String, Float>, events:Array<SongEventData>, notes:Map<String, Array<SongNoteData>>)
|
||||
{
|
||||
this.version = SongRegistry.SONG_CHART_DATA_VERSION;
|
||||
|
||||
this.events = events;
|
||||
this.notes = notes;
|
||||
this.scrollSpeed = scrollSpeed;
|
||||
|
||||
this.generatedBy = SongRegistry.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;
|
||||
}
|
||||
|
||||
public function setScrollSpeed(value:Float, diff:String = 'default'):Float
|
||||
{
|
||||
this.scrollSpeed.set(diff, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
public function getNotes(diff:String):Array<SongNoteData>
|
||||
{
|
||||
var result:Array<SongNoteData> = this.notes.get(diff);
|
||||
|
||||
if (result == null && diff != 'normal') return getNotes('normal');
|
||||
|
||||
return (result == null) ? [] : result;
|
||||
}
|
||||
|
||||
public function setNotes(value:Array<SongNoteData>, diff:String):Array<SongNoteData>
|
||||
{
|
||||
this.notes.set(diff, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
public function getEvents():Array<SongEventData>
|
||||
{
|
||||
return this.events;
|
||||
}
|
||||
|
||||
public function setEvents(value:Array<SongEventData>):Array<SongEventData>
|
||||
{
|
||||
return this.events = value;
|
||||
}
|
||||
}
|
||||
|
||||
class SongEventData
|
||||
{
|
||||
/**
|
||||
* The timestamp of the event. The timestamp is in the format of the song's time format.
|
||||
*/
|
||||
@:alias("t")
|
||||
public var time:Float;
|
||||
|
||||
/**
|
||||
* The kind of the event.
|
||||
* Examples include "FocusCamera" and "PlayAnimation"
|
||||
* Custom events can be added by scripts with the `ScriptedSongEvent` class.
|
||||
*/
|
||||
@:alias("e")
|
||||
public var event: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.
|
||||
*/
|
||||
@:alias("v")
|
||||
@:optional
|
||||
@:jcustomparse(funkin.data.DataParse.dynamicValue)
|
||||
public var value:Dynamic = null;
|
||||
|
||||
/**
|
||||
* Whether this event has been activated.
|
||||
* This is only used internally by the game. It should not be serialized.
|
||||
*/
|
||||
@:jignored
|
||||
public var activated:Bool = false;
|
||||
|
||||
public function new(time:Float, event:String, value:Dynamic = null)
|
||||
{
|
||||
this.time = time;
|
||||
this.event = event;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@:jignored
|
||||
public var stepTime(get, never):Float;
|
||||
|
||||
function get_stepTime():Float
|
||||
{
|
||||
return Conductor.getTimeInSteps(this.time);
|
||||
}
|
||||
|
||||
public inline function getDynamic(key:String):Null<Dynamic>
|
||||
{
|
||||
return value == null ? null : Reflect.field(value, key);
|
||||
}
|
||||
|
||||
public inline function getBool(key:String):Null<Bool>
|
||||
{
|
||||
return value == null ? null : cast Reflect.field(value, key);
|
||||
}
|
||||
|
||||
public inline function getInt(key:String):Null<Int>
|
||||
{
|
||||
return value == null ? null : cast Reflect.field(value, key);
|
||||
}
|
||||
|
||||
public inline function getFloat(key:String):Null<Float>
|
||||
{
|
||||
return value == null ? null : cast Reflect.field(value, key);
|
||||
}
|
||||
|
||||
public inline function getString(key:String):String
|
||||
{
|
||||
return value == null ? null : cast Reflect.field(value, key);
|
||||
}
|
||||
|
||||
public inline function getArray(key:String):Array<Dynamic>
|
||||
{
|
||||
return value == null ? null : cast Reflect.field(value, key);
|
||||
}
|
||||
|
||||
public inline function getBoolArray(key:String):Array<Bool>
|
||||
{
|
||||
return value == null ? null : cast Reflect.field(value, key);
|
||||
}
|
||||
|
||||
@:op(A == B)
|
||||
public function op_equals(other:SongEventData):Bool
|
||||
{
|
||||
return this.time == other.time && this.event == other.event && this.value == other.value;
|
||||
}
|
||||
|
||||
@:op(A != B)
|
||||
public function op_notEquals(other:SongEventData):Bool
|
||||
{
|
||||
return this.time != other.time || this.event != other.event || this.value != other.value;
|
||||
}
|
||||
|
||||
@:op(A > B)
|
||||
public function op_greaterThan(other:SongEventData):Bool
|
||||
{
|
||||
return this.time > other.time;
|
||||
}
|
||||
|
||||
@:op(A < B)
|
||||
public function op_lessThan(other:SongEventData):Bool
|
||||
{
|
||||
return this.time < other.time;
|
||||
}
|
||||
|
||||
@:op(A >= B)
|
||||
public function op_greaterThanOrEquals(other:SongEventData):Bool
|
||||
{
|
||||
return this.time >= other.time;
|
||||
}
|
||||
|
||||
@:op(A <= B)
|
||||
public function op_lessThanOrEquals(other:SongEventData):Bool
|
||||
{
|
||||
return this.time <= other.time;
|
||||
}
|
||||
}
|
||||
|
||||
class SongNoteData
|
||||
{
|
||||
/**
|
||||
* The timestamp of the note. The timestamp is in the format of the song's time format.
|
||||
*/
|
||||
@:alias("t")
|
||||
public var time: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.
|
||||
*/
|
||||
@:alias("d")
|
||||
public var data:Int;
|
||||
|
||||
/**
|
||||
* Length of the note, if applicable.
|
||||
* Defaults to 0 for single notes.
|
||||
*/
|
||||
@:alias("l")
|
||||
@:default(0)
|
||||
@:optional
|
||||
public var length:Float;
|
||||
|
||||
/**
|
||||
* The kind of the note.
|
||||
* This can allow the note to include information used for custom behavior.
|
||||
* Defaults to blank or `"normal"`.
|
||||
*/
|
||||
@:alias("k")
|
||||
@:default("normal")
|
||||
@:optional
|
||||
public var kind(get, default):String = '';
|
||||
|
||||
function get_kind():String
|
||||
{
|
||||
if (this.kind == null || this.kind == '') return 'normal';
|
||||
|
||||
return this.kind;
|
||||
}
|
||||
|
||||
public function new(time:Float, data:Int, length:Float = 0, kind:String = '')
|
||||
{
|
||||
this.time = time;
|
||||
this.data = data;
|
||||
this.length = length;
|
||||
this.kind = kind;
|
||||
}
|
||||
|
||||
/**
|
||||
* The timestamp of the note, in steps.
|
||||
*/
|
||||
@:jignored
|
||||
public var stepTime(get, never):Float;
|
||||
|
||||
function get_stepTime():Float
|
||||
{
|
||||
return Conductor.getTimeInSteps(this.time);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.data % strumlineSize;
|
||||
}
|
||||
|
||||
public function getDirectionName(strumlineSize:Int = 4):String
|
||||
{
|
||||
switch (this.data % strumlineSize)
|
||||
{
|
||||
case 0:
|
||||
return 'Left';
|
||||
case 1:
|
||||
return 'Down';
|
||||
case 2:
|
||||
return 'Up';
|
||||
case 3:
|
||||
return 'Right';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.data / strumlineSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the note is one that Boyfriend should try to hit (i.e. it's on his side).
|
||||
* TODO: The name of this function is a little misleading; what about mines?
|
||||
* @param strumlineSize Defaults to 4.
|
||||
* @return True if it's Boyfriend's note.
|
||||
*/
|
||||
public inline function getMustHitNote(strumlineSize:Int = 4):Bool
|
||||
{
|
||||
return getStrumlineIndex(strumlineSize) == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* If this is a hold note, this is the length of the hold note in steps.
|
||||
* @default 0 (not a hold note)
|
||||
*/
|
||||
public var stepLength(get, set):Float;
|
||||
|
||||
function get_stepLength():Float
|
||||
{
|
||||
return Conductor.getTimeInSteps(this.time + this.length) - this.stepTime;
|
||||
}
|
||||
|
||||
function set_stepLength(value:Float):Float
|
||||
{
|
||||
return this.length = Conductor.getStepTimeInMs(value) - this.time;
|
||||
}
|
||||
|
||||
@:jignored
|
||||
public var isHoldNote(get, never):Bool;
|
||||
|
||||
public function get_isHoldNote():Bool
|
||||
{
|
||||
return this.length > 0;
|
||||
}
|
||||
|
||||
@:op(A == B)
|
||||
public function op_equals(other:SongNoteData):Bool
|
||||
{
|
||||
if (this.kind == '')
|
||||
{
|
||||
if (other.kind != '' && other.kind != 'normal') return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (other.kind == '' || other.kind != this.kind) return false;
|
||||
}
|
||||
|
||||
return this.time == other.time && this.data == other.data && this.length == other.length;
|
||||
}
|
||||
|
||||
@:op(A != B)
|
||||
public function op_notEquals(other:SongNoteData):Bool
|
||||
{
|
||||
if (this.kind == '')
|
||||
{
|
||||
if (other.kind != '' && other.kind != 'normal') return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (other.kind == '' || other.kind != this.kind) return true;
|
||||
}
|
||||
|
||||
return this.time != other.time || this.data != other.data || this.length != other.length;
|
||||
}
|
||||
|
||||
@:op(A > B)
|
||||
public function op_greaterThan(other:SongNoteData):Bool
|
||||
{
|
||||
return this.time > other.time;
|
||||
}
|
||||
|
||||
@:op(A < B)
|
||||
public function op_lessThan(other:SongNoteData):Bool
|
||||
{
|
||||
return this.time < other.time;
|
||||
}
|
||||
|
||||
@:op(A >= B)
|
||||
public function op_greaterThanOrEquals(other:SongNoteData):Bool
|
||||
{
|
||||
return this.time >= other.time;
|
||||
}
|
||||
|
||||
@:op(A <= B)
|
||||
public function op_lessThanOrEquals(other:SongNoteData):Bool
|
||||
{
|
||||
return this.time <= other.time;
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
package funkin.play.song;
|
||||
package funkin.data.song;
|
||||
|
||||
import flixel.util.FlxSort;
|
||||
import funkin.play.song.SongData.SongEventData;
|
||||
import funkin.play.song.SongData.SongNoteData;
|
||||
import funkin.data.song.SongData.SongEventData;
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
import funkin.util.ClipboardUtil;
|
||||
import funkin.util.SerializerUtil;
|
||||
|
262
source/funkin/data/song/SongRegistry.hx
Normal file
262
source/funkin/data/song/SongRegistry.hx
Normal file
|
@ -0,0 +1,262 @@
|
|||
package funkin.data.song;
|
||||
|
||||
import funkin.data.song.SongData;
|
||||
import funkin.data.song.SongData.SongChartData;
|
||||
import funkin.data.song.SongData.SongMetadata;
|
||||
import funkin.play.song.ScriptedSong;
|
||||
import funkin.play.song.Song;
|
||||
import funkin.util.assets.DataAssets;
|
||||
import funkin.util.VersionUtil;
|
||||
|
||||
class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
||||
{
|
||||
/**
|
||||
* The current version string for the stage data format.
|
||||
* 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_RULE:thx.semver.VersionRule = "2.0.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 var DEFAULT_GENERATEDBY(get, null):String;
|
||||
|
||||
static function get_DEFAULT_GENERATEDBY():String
|
||||
{
|
||||
return '${Constants.TITLE} - ${Constants.VERSION}';
|
||||
}
|
||||
|
||||
public static final instance:SongRegistry = new SongRegistry();
|
||||
|
||||
public function new()
|
||||
{
|
||||
super('SONG', 'songs', SONG_METADATA_VERSION_RULE);
|
||||
}
|
||||
|
||||
public override function loadEntries():Void
|
||||
{
|
||||
clearEntries();
|
||||
|
||||
//
|
||||
// SCRIPTED ENTRIES
|
||||
//
|
||||
var scriptedEntryClassNames:Array<String> = getScriptedClassNames();
|
||||
log('Registering ${scriptedEntryClassNames.length} scripted entries...');
|
||||
|
||||
for (entryCls in scriptedEntryClassNames)
|
||||
{
|
||||
var entry:Song = createScriptedEntry(entryCls);
|
||||
|
||||
if (entry != null)
|
||||
{
|
||||
log('Successfully created scripted entry (${entryCls} = ${entry.id})');
|
||||
entries.set(entry.id, entry);
|
||||
}
|
||||
else
|
||||
{
|
||||
log('Failed to create scripted entry (${entryCls})');
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// UNSCRIPTED ENTRIES
|
||||
//
|
||||
var entryIdList:Array<String> = DataAssets.listDataFilesInPath('songs/', '-metadata.json').map(function(songDataPath:String):String {
|
||||
return songDataPath.split('/')[0];
|
||||
});
|
||||
var unscriptedEntryIds:Array<String> = entryIdList.filter(function(entryId:String):Bool {
|
||||
return !entries.exists(entryId);
|
||||
});
|
||||
log('Fetching data for ${unscriptedEntryIds.length} unscripted entries...');
|
||||
for (entryId in unscriptedEntryIds)
|
||||
{
|
||||
try
|
||||
{
|
||||
var entry:Song = createEntry(entryId);
|
||||
if (entry != null)
|
||||
{
|
||||
trace(' Loaded entry data: ${entry}');
|
||||
entries.set(entry.id, entry);
|
||||
}
|
||||
}
|
||||
catch (e:Dynamic)
|
||||
{
|
||||
// Print the error.
|
||||
trace(' Failed to load entry data: ${entryId}');
|
||||
trace(e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read, parse, and validate the JSON data and produce the corresponding data object.
|
||||
*/
|
||||
public function parseEntryData(id:String):Null<SongMetadata>
|
||||
{
|
||||
return parseEntryMetadata(id);
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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.
|
||||
if (SONG_METADATA_VERSION_RULE == null || VersionUtil.validateVersion(version, SONG_METADATA_VERSION_RULE))
|
||||
{
|
||||
return parseEntryMetadata(id);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw '[${registryId}] Metadata entry ${id}:${variation == '' ? 'default' : variation} does not support migration to version ${SONG_METADATA_VERSION_RULE}.';
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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.
|
||||
if (SONG_CHART_DATA_VERSION_RULE == null || VersionUtil.validateVersion(version, SONG_CHART_DATA_VERSION_RULE))
|
||||
{
|
||||
return parseEntryChartData(id, variation);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw '[${registryId}] Chart entry ${id}:${variation == '' ? 'default' : variation} does not support migration to version ${SONG_CHART_DATA_VERSION_RULE}.';
|
||||
}
|
||||
}
|
||||
|
||||
function createScriptedEntry(clsName:String):Song
|
||||
{
|
||||
return ScriptedSong.init(clsName, "unknown");
|
||||
}
|
||||
|
||||
function getScriptedClassNames():Array<String>
|
||||
{
|
||||
return ScriptedSong.listScriptClasses();
|
||||
}
|
||||
|
||||
function loadEntryMetadataFile(id:String, variation:String = ''):BaseRegistry.JsonFile
|
||||
{
|
||||
var entryFilePath:String = Paths.json('$dataFilePath/$id/$id${variation == '' ? '' : '-$variation'}-metadata');
|
||||
var rawJson:String = openfl.Assets.getText(entryFilePath).trim();
|
||||
return {fileName: entryFilePath, contents: rawJson};
|
||||
}
|
||||
|
||||
function loadMusicDataFile(id:String, variation:String = ''):BaseRegistry.JsonFile
|
||||
{
|
||||
var entryFilePath:String = Paths.file('music/$id/$id${variation == '' ? '' : '-$variation'}-metadata.json');
|
||||
var rawJson:String = openfl.Assets.getText(entryFilePath).trim();
|
||||
return {fileName: entryFilePath, contents: rawJson};
|
||||
}
|
||||
|
||||
function loadEntryChartFile(id:String, variation:String = ''):BaseRegistry.JsonFile
|
||||
{
|
||||
var entryFilePath:String = Paths.json('$dataFilePath/$id/$id${variation == '' ? '' : '-$variation'}-chart');
|
||||
var rawJson:String = openfl.Assets.getText(entryFilePath).trim();
|
||||
return {fileName: entryFilePath, contents: rawJson};
|
||||
}
|
||||
|
||||
public function fetchEntryMetadataVersion(id:String, variation:String = ''):Null<thx.semver.Version>
|
||||
{
|
||||
var entryStr:String = loadEntryMetadataFile(id, variation).contents;
|
||||
var entryVersion:thx.semver.Version = VersionUtil.getVersionFromJSON(entryStr);
|
||||
return entryVersion;
|
||||
}
|
||||
|
||||
public function fetchEntryChartVersion(id:String, variation:String = ''):Null<thx.semver.Version>
|
||||
{
|
||||
var entryStr:String = loadEntryChartFile(id, variation).contents;
|
||||
var entryVersion:thx.semver.Version = VersionUtil.getVersionFromJSON(entryStr);
|
||||
return entryVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of all the story weeks from the base game, in order.
|
||||
* TODO: Should this be hardcoded?
|
||||
*/
|
||||
public function listBaseGameSongIds():Array<String>
|
||||
{
|
||||
return [
|
||||
"tutorial", "bopeebo", "fresh", "dadbattle", "spookeez", "south", "monster", "pico", "philly-nice", "blammed", "satin-panties", "high", "milf", "cocoa",
|
||||
"eggnog", "winter-horrorland", "senpai", "roses", "thorns", "ugh", "guns", "stress", "darnell", "lit-up", "2hot", "blazin"
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of all installed story weeks that are not from the base game.
|
||||
*/
|
||||
public function listModdedSongIds():Array<String>
|
||||
{
|
||||
return listEntryIds().filter(function(id:String):Bool {
|
||||
return listBaseGameSongIds().indexOf(id) == -1;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -3,18 +3,19 @@ package funkin.modding;
|
|||
import funkin.util.macro.ClassMacro;
|
||||
import funkin.modding.module.ModuleHandler;
|
||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||
import funkin.play.song.SongData;
|
||||
import funkin.data.song.SongData;
|
||||
import funkin.play.stage.StageData;
|
||||
import polymod.Polymod;
|
||||
import polymod.backends.PolymodAssets.PolymodAssetType;
|
||||
import polymod.format.ParseRules.TextFileFormat;
|
||||
import funkin.play.event.SongEventData.SongEventParser;
|
||||
import funkin.data.event.SongEventData.SongEventParser;
|
||||
import funkin.util.FileUtil;
|
||||
import funkin.data.level.LevelRegistry;
|
||||
import funkin.data.notestyle.NoteStyleRegistry;
|
||||
import funkin.play.cutscene.dialogue.ConversationDataParser;
|
||||
import funkin.play.cutscene.dialogue.DialogueBoxDataParser;
|
||||
import funkin.play.cutscene.dialogue.SpeakerDataParser;
|
||||
import funkin.data.song.SongRegistry;
|
||||
|
||||
class PolymodHandler
|
||||
{
|
||||
|
@ -290,13 +291,13 @@ class PolymodHandler
|
|||
|
||||
// These MUST be imported at the top of the file and not referred to by fully qualified name,
|
||||
// to ensure build macros work properly.
|
||||
SongRegistry.instance.loadEntries();
|
||||
LevelRegistry.instance.loadEntries();
|
||||
NoteStyleRegistry.instance.loadEntries();
|
||||
SongEventParser.loadEventCache();
|
||||
ConversationDataParser.loadConversationCache();
|
||||
DialogueBoxDataParser.loadDialogueBoxCache();
|
||||
SpeakerDataParser.loadSpeakerCache();
|
||||
SongDataParser.loadSongCache();
|
||||
StageDataParser.loadStageCache();
|
||||
CharacterDataParser.loadCharacterCache();
|
||||
ModuleHandler.loadModuleCache();
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package funkin.modding.events;
|
||||
|
||||
import funkin.play.song.SongData.SongNoteData;
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
import flixel.FlxState;
|
||||
import flixel.FlxSubState;
|
||||
import funkin.play.notes.NoteSprite;
|
||||
|
@ -435,9 +435,9 @@ class SongEventScriptEvent extends ScriptEvent
|
|||
* The note associated with this event.
|
||||
* You cannot replace it, but you can edit it.
|
||||
*/
|
||||
public var event(default, null):funkin.play.song.SongData.SongEventData;
|
||||
public var event(default, null):funkin.data.song.SongData.SongEventData;
|
||||
|
||||
public function new(event:funkin.play.song.SongData.SongEventData):Void
|
||||
public function new(event:funkin.data.song.SongData.SongEventData):Void
|
||||
{
|
||||
super(ScriptEvent.SONG_EVENT, true);
|
||||
this.event = event;
|
||||
|
|
|
@ -35,7 +35,7 @@ import funkin.play.cutscene.dialogue.Conversation;
|
|||
import funkin.play.cutscene.dialogue.ConversationDataParser;
|
||||
import funkin.play.cutscene.VanillaCutscenes;
|
||||
import funkin.play.cutscene.VideoCutscene;
|
||||
import funkin.play.event.SongEventData.SongEventParser;
|
||||
import funkin.data.event.SongEventData.SongEventParser;
|
||||
import funkin.play.notes.NoteSprite;
|
||||
import funkin.play.notes.NoteDirection;
|
||||
import funkin.play.notes.Strumline;
|
||||
|
@ -43,10 +43,10 @@ import funkin.play.notes.SustainTrail;
|
|||
import funkin.play.scoring.Scoring;
|
||||
import funkin.NoteSplash;
|
||||
import funkin.play.song.Song;
|
||||
import funkin.play.song.SongData.SongDataParser;
|
||||
import funkin.play.song.SongData.SongEventData;
|
||||
import funkin.play.song.SongData.SongNoteData;
|
||||
import funkin.play.song.SongData.SongPlayableChar;
|
||||
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.play.stage.Stage;
|
||||
import funkin.play.stage.StageData.StageDataParser;
|
||||
import funkin.ui.PopUpStuff;
|
||||
|
@ -630,7 +630,7 @@ class PlayState extends MusicBeatSubState
|
|||
startingSong = true;
|
||||
|
||||
// TODO: We hardcoded the transition into Winter Horrorland. Do this with a ScriptedSong instead.
|
||||
if ((currentSong?.songId ?? '').toLowerCase() == 'winter-horrorland')
|
||||
if ((currentSong?.id ?? '').toLowerCase() == 'winter-horrorland')
|
||||
{
|
||||
// VanillaCutscenes will call startCountdown later.
|
||||
VanillaCutscenes.playHorrorStartCutscene();
|
||||
|
@ -2495,9 +2495,9 @@ class PlayState extends MusicBeatSubState
|
|||
if (currentSong != null && currentSong.validScore)
|
||||
{
|
||||
// crackhead double thingie, sets whether was new highscore, AND saves the song!
|
||||
Highscore.tallies.isNewHighscore = Highscore.saveScoreForDifficulty(currentSong.songId, songScore, currentDifficulty);
|
||||
Highscore.tallies.isNewHighscore = Highscore.saveScoreForDifficulty(currentSong.id, songScore, currentDifficulty);
|
||||
|
||||
Highscore.saveCompletionForDifficulty(currentSong.songId, Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes, currentDifficulty);
|
||||
Highscore.saveCompletionForDifficulty(currentSong.id, Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes, currentDifficulty);
|
||||
}
|
||||
|
||||
if (PlayStatePlaylist.isStoryMode)
|
||||
|
@ -2549,7 +2549,7 @@ class PlayState extends MusicBeatSubState
|
|||
vocals.stop();
|
||||
|
||||
// TODO: Softcode this cutscene.
|
||||
if (currentSong.songId == 'eggnog')
|
||||
if (currentSong.id == 'eggnog')
|
||||
{
|
||||
var blackShit:FlxSprite = new FlxSprite(-FlxG.width * FlxG.camera.zoom,
|
||||
-FlxG.height * FlxG.camera.zoom).makeGraphic(FlxG.width * 3, FlxG.height * 3, FlxColor.BLACK);
|
||||
|
@ -2560,7 +2560,7 @@ class PlayState extends MusicBeatSubState
|
|||
|
||||
FlxG.sound.play(Paths.sound('Lights_Shut_off'), function() {
|
||||
// no camFollow so it centers on horror tree
|
||||
var targetSong:Song = SongDataParser.fetchSong(targetSongId);
|
||||
var targetSong:Song = SongRegistry.instance.fetchEntry(targetSongId);
|
||||
// Load and cache the song's charts.
|
||||
// TODO: Do this in the loading state.
|
||||
targetSong.cacheCharts(true);
|
||||
|
@ -2577,7 +2577,7 @@ class PlayState extends MusicBeatSubState
|
|||
}
|
||||
else
|
||||
{
|
||||
var targetSong:Song = SongDataParser.fetchSong(targetSongId);
|
||||
var targetSong:Song = SongRegistry.instance.fetchEntry(targetSongId);
|
||||
// Load and cache the song's charts.
|
||||
// TODO: Do this in the loading state.
|
||||
targetSong.cacheCharts(true);
|
||||
|
|
|
@ -143,7 +143,7 @@ class ResultState extends MusicBeatSubState
|
|||
}
|
||||
else
|
||||
{
|
||||
songName.text += PlayState.instance.currentSong.songId;
|
||||
songName.text += PlayState.instance.currentSong.id;
|
||||
}
|
||||
|
||||
songName.letterSpacing = -15;
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
package funkin.play.event;
|
||||
|
||||
import funkin.play.song.SongData;
|
||||
// Data from the chart
|
||||
import funkin.data.song.SongData;
|
||||
import funkin.data.song.SongData.SongEventData;
|
||||
// Data from the event schema
|
||||
import funkin.play.event.SongEvent;
|
||||
import funkin.play.event.SongEventData.SongEventFieldType;
|
||||
import funkin.play.event.SongEventData.SongEventSchema;
|
||||
import funkin.data.event.SongEventData.SongEventSchema;
|
||||
import funkin.data.event.SongEventData.SongEventFieldType;
|
||||
|
||||
/**
|
||||
* This class represents a handler for a type of song event.
|
||||
|
|
|
@ -2,10 +2,13 @@ package funkin.play.event;
|
|||
|
||||
import flixel.FlxSprite;
|
||||
import funkin.play.character.BaseCharacter;
|
||||
// Data from the chart
|
||||
import funkin.data.song.SongData;
|
||||
import funkin.data.song.SongData.SongEventData;
|
||||
// Data from the event schema
|
||||
import funkin.play.event.SongEvent;
|
||||
import funkin.play.event.SongEventData.SongEventFieldType;
|
||||
import funkin.play.event.SongEventData.SongEventSchema;
|
||||
import funkin.play.song.SongData;
|
||||
import funkin.data.event.SongEventData.SongEventSchema;
|
||||
import funkin.data.event.SongEventData.SongEventFieldType;
|
||||
|
||||
class PlayAnimationSongEvent extends SongEvent
|
||||
{
|
||||
|
|
|
@ -3,10 +3,13 @@ package funkin.play.event;
|
|||
import flixel.tweens.FlxTween;
|
||||
import flixel.FlxCamera;
|
||||
import flixel.tweens.FlxEase;
|
||||
// Data from the chart
|
||||
import funkin.data.song.SongData;
|
||||
import funkin.data.song.SongData.SongEventData;
|
||||
// Data from the event schema
|
||||
import funkin.play.event.SongEvent;
|
||||
import funkin.play.song.SongData;
|
||||
import funkin.play.event.SongEventData;
|
||||
import funkin.play.event.SongEventData.SongEventFieldType;
|
||||
import funkin.data.event.SongEventData.SongEventSchema;
|
||||
import funkin.data.event.SongEventData.SongEventFieldType;
|
||||
|
||||
/**
|
||||
* This class represents a handler for configuring camera bop intensity and rate.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package funkin.play.event;
|
||||
|
||||
import funkin.play.song.SongData.SongEventData;
|
||||
import funkin.play.event.SongEventData.SongEventSchema;
|
||||
import funkin.data.song.SongData.SongEventData;
|
||||
import funkin.data.event.SongEventData.SongEventSchema;
|
||||
|
||||
/**
|
||||
* This class represents a handler for a type of song event.
|
||||
|
|
|
@ -3,10 +3,13 @@ package funkin.play.event;
|
|||
import flixel.tweens.FlxTween;
|
||||
import flixel.FlxCamera;
|
||||
import flixel.tweens.FlxEase;
|
||||
// Data from the chart
|
||||
import funkin.data.song.SongData;
|
||||
import funkin.data.song.SongData.SongEventData;
|
||||
// Data from the event schema
|
||||
import funkin.play.event.SongEvent;
|
||||
import funkin.play.song.SongData;
|
||||
import funkin.play.event.SongEventData;
|
||||
import funkin.play.event.SongEventData.SongEventFieldType;
|
||||
import funkin.data.event.SongEventData.SongEventFieldType;
|
||||
import funkin.data.event.SongEventData.SongEventSchema;
|
||||
|
||||
/**
|
||||
* This class represents a handler for camera zoom events.
|
||||
|
@ -76,8 +79,7 @@ class ZoomCameraSongEvent extends SongEvent
|
|||
return;
|
||||
}
|
||||
|
||||
FlxTween.tween(PlayState.instance, {defaultCameraZoom: zoom * FlxCamera.defaultZoom}, (Conductor.stepLengthMs * duration / 1000),
|
||||
{ease: easeFunction});
|
||||
FlxTween.tween(PlayState.instance, {defaultCameraZoom: zoom * FlxCamera.defaultZoom}, (Conductor.stepLengthMs * duration / 1000), {ease: easeFunction});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package funkin.play.notes;
|
||||
|
||||
import funkin.play.song.SongData.SongNoteData;
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
import funkin.play.notes.notestyle.NoteStyle;
|
||||
import flixel.graphics.frames.FlxAtlasFrames;
|
||||
import flixel.FlxSprite;
|
||||
|
|
|
@ -11,7 +11,7 @@ import funkin.play.notes.NoteHoldCover;
|
|||
import funkin.play.notes.NoteSplash;
|
||||
import funkin.play.notes.NoteSprite;
|
||||
import funkin.play.notes.SustainTrail;
|
||||
import funkin.play.song.SongData.SongNoteData;
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
import funkin.ui.PreferencesMenu;
|
||||
import funkin.util.SortUtil;
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ package funkin.play.notes;
|
|||
|
||||
import funkin.play.notes.notestyle.NoteStyle;
|
||||
import funkin.play.notes.NoteDirection;
|
||||
import funkin.play.song.SongData.SongNoteData;
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
import flixel.util.FlxDirectionFlags;
|
||||
import flixel.FlxSprite;
|
||||
import flixel.graphics.FlxGraphic;
|
||||
|
|
|
@ -104,7 +104,8 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
|
|||
|
||||
noteFrames = Paths.getSparrowAtlas(getNoteAssetPath(), getNoteAssetLibrary());
|
||||
|
||||
if (noteFrames == null) {
|
||||
if (noteFrames == null)
|
||||
{
|
||||
throw 'Could not load note frames for note style: $id';
|
||||
}
|
||||
|
||||
|
@ -139,13 +140,13 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
|
|||
function buildNoteAnimations(target:NoteSprite):Void
|
||||
{
|
||||
var leftData:AnimationData = fetchNoteAnimationData(LEFT);
|
||||
target.animation.addByPrefix('purpleScroll', leftData.prefix);
|
||||
target.animation.addByPrefix('purpleScroll', leftData.prefix, leftData.frameRate, leftData.looped, leftData.flipX, leftData.flipY);
|
||||
var downData:AnimationData = fetchNoteAnimationData(DOWN);
|
||||
target.animation.addByPrefix('blueScroll', downData.prefix);
|
||||
target.animation.addByPrefix('blueScroll', downData.prefix, downData.frameRate, downData.looped, downData.flipX, downData.flipY);
|
||||
var upData:AnimationData = fetchNoteAnimationData(UP);
|
||||
target.animation.addByPrefix('greenScroll', upData.prefix);
|
||||
target.animation.addByPrefix('greenScroll', upData.prefix, upData.frameRate, upData.looped, upData.flipX, upData.flipY);
|
||||
var rightData:AnimationData = fetchNoteAnimationData(RIGHT);
|
||||
target.animation.addByPrefix('redScroll', rightData.prefix);
|
||||
target.animation.addByPrefix('redScroll', rightData.prefix, rightData.frameRate, rightData.looped, rightData.flipX, rightData.flipY);
|
||||
}
|
||||
|
||||
function fetchNoteAnimationData(dir:NoteDirection):AnimationData
|
||||
|
@ -302,7 +303,7 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
|
|||
return 'NoteStyle($id)';
|
||||
}
|
||||
|
||||
public function _fetchData(id:String):Null<NoteStyleData>
|
||||
static function _fetchData(id:String):Null<NoteStyleData>
|
||||
{
|
||||
return NoteStyleRegistry.instance.parseEntryDataWithMigration(id, NoteStyleRegistry.instance.fetchEntryVersion(id));
|
||||
}
|
||||
|
|
|
@ -5,14 +5,16 @@ import openfl.utils.Assets;
|
|||
import funkin.modding.events.ScriptEvent;
|
||||
import funkin.modding.IScriptedClass;
|
||||
import funkin.audio.VoicesGroup;
|
||||
import funkin.play.song.SongData.SongChartData;
|
||||
import funkin.play.song.SongData.SongDataParser;
|
||||
import funkin.play.song.SongData.SongEventData;
|
||||
import funkin.play.song.SongData.SongMetadata;
|
||||
import funkin.play.song.SongData.SongNoteData;
|
||||
import funkin.play.song.SongData.SongPlayableChar;
|
||||
import funkin.play.song.SongData.SongTimeChange;
|
||||
import funkin.play.song.SongData.SongTimeFormat;
|
||||
import funkin.data.song.SongRegistry;
|
||||
import funkin.data.song.SongData.SongChartData;
|
||||
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.SongTimeChange;
|
||||
import funkin.data.song.SongData.SongTimeFormat;
|
||||
import funkin.data.IRegistryEntry;
|
||||
|
||||
/**
|
||||
* This is a data structure managing information about the current song.
|
||||
|
@ -23,9 +25,26 @@ import funkin.play.song.SongData.SongTimeFormat;
|
|||
* It also receives script events; scripted classes which extend this class
|
||||
* can be used to perform custom gameplay behaviors only on specific songs.
|
||||
*/
|
||||
class Song implements IPlayStateScriptedClass
|
||||
@:nullSafety
|
||||
class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMetadata>
|
||||
{
|
||||
public final songId:String;
|
||||
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 final id:String;
|
||||
|
||||
/**
|
||||
* Song metadata as parsed from the JSON file.
|
||||
* This is the data for the `default` variation specifically,
|
||||
* and is needed for the IRegistryEntry interface.
|
||||
* Will only be null if the song data could not be loaded.
|
||||
*/
|
||||
public final _data:Null<SongMetadata>;
|
||||
|
||||
final _metadata:Array<SongMetadata>;
|
||||
|
||||
|
@ -39,33 +58,56 @@ class Song implements IPlayStateScriptedClass
|
|||
|
||||
var difficultyIds:Array<String>;
|
||||
|
||||
public var songName(get, never):String;
|
||||
|
||||
function get_songName():String
|
||||
{
|
||||
if (_data != null) return _data?.songName ?? DEFAULT_SONGNAME;
|
||||
if (_metadata.length > 0) return _metadata[0]?.songName ?? DEFAULT_SONGNAME;
|
||||
return DEFAULT_SONGNAME;
|
||||
}
|
||||
|
||||
public var songArtist(get, never):String;
|
||||
|
||||
function get_songArtist():String
|
||||
{
|
||||
if (_data != null) return _data?.artist ?? DEFAULT_ARTIST;
|
||||
if (_metadata.length > 0) return _metadata[0]?.artist ?? DEFAULT_ARTIST;
|
||||
return DEFAULT_ARTIST;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id The ID of the song to load.
|
||||
* @param ignoreErrors If false, an exception will be thrown if the song data could not be loaded.
|
||||
*/
|
||||
public function new(id:String, ignoreErrors:Bool = false)
|
||||
public function new(id:String)
|
||||
{
|
||||
this.songId = id;
|
||||
this.id = id;
|
||||
|
||||
variations = [];
|
||||
difficultyIds = [];
|
||||
difficulties = new Map<String, SongDifficulty>();
|
||||
|
||||
try
|
||||
_data = _fetchData(id);
|
||||
|
||||
_metadata = _data == null ? [] : [_data];
|
||||
|
||||
for (meta in fetchVariationMetadata(id))
|
||||
_metadata.push(meta);
|
||||
|
||||
if (_metadata.length == 0)
|
||||
{
|
||||
_metadata = SongDataParser.loadSongMetadata(songId);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
_metadata = [];
|
||||
trace('[WARN] Could not find song data for songId: $id');
|
||||
return;
|
||||
}
|
||||
|
||||
if (_metadata.length == 0 && !ignoreErrors)
|
||||
{
|
||||
throw 'Could not find song data for songId: $songId';
|
||||
}
|
||||
else
|
||||
variations.clear();
|
||||
variations.push('default');
|
||||
if (_data != null && _data.playData != null)
|
||||
{
|
||||
for (vari in _data.playData.songVariations)
|
||||
variations.push(vari);
|
||||
|
||||
populateFromMetadata();
|
||||
}
|
||||
}
|
||||
|
@ -74,7 +116,7 @@ class Song implements IPlayStateScriptedClass
|
|||
public static function buildRaw(songId:String, metadata:Array<SongMetadata>, variations:Array<String>, charts:Map<String, SongChartData>,
|
||||
validScore:Bool = false):Song
|
||||
{
|
||||
var result:Song = new Song(songId, true);
|
||||
var result:Song = new Song(songId);
|
||||
|
||||
result._metadata.clear();
|
||||
for (meta in metadata)
|
||||
|
@ -112,6 +154,8 @@ class Song implements IPlayStateScriptedClass
|
|||
// Variations may have different artist, time format, generatedBy, etc.
|
||||
for (metadata in _metadata)
|
||||
{
|
||||
if (metadata == null || metadata.playData == null) continue;
|
||||
|
||||
// There may be more difficulties in the chart file than in the metadata,
|
||||
// (i.e. non-playable charts like the one used for Pico on the speaker in Stress)
|
||||
// but all the difficulties in the metadata must be in the chart file.
|
||||
|
@ -134,15 +178,16 @@ class Song implements IPlayStateScriptedClass
|
|||
difficulty.stage = metadata.playData.stage;
|
||||
// difficulty.noteSkin = 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 = metadata.playData.playableChars.get(charId);
|
||||
|
||||
var char:Null<SongPlayableChar> = metadata.playData.playableChars.get(charId);
|
||||
if (char == null) continue;
|
||||
difficulty.chars.set(charId, char);
|
||||
}
|
||||
|
||||
difficulties.set(diffId, difficulty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -157,11 +202,14 @@ class Song implements IPlayStateScriptedClass
|
|||
clearCharts();
|
||||
}
|
||||
|
||||
trace('Caching ${variations.length} chart files for song $songId');
|
||||
trace('Caching ${variations.length} chart files for song $id');
|
||||
for (variation in variations)
|
||||
{
|
||||
var chartData:SongChartData = SongDataParser.parseSongChartData(songId, variation);
|
||||
applyChartData(chartData, variation);
|
||||
var version:Null<thx.semver.Version> = SongRegistry.instance.fetchEntryChartVersion(id, variation);
|
||||
if (version == null) continue;
|
||||
var chart:Null<SongChartData> = SongRegistry.instance.parseEntryChartDataWithMigration(id, version, variation);
|
||||
if (chart == null) continue;
|
||||
applyChartData(chart, variation);
|
||||
}
|
||||
trace('Done caching charts.');
|
||||
}
|
||||
|
@ -181,8 +229,8 @@ class Song implements IPlayStateScriptedClass
|
|||
difficulties.set(diffId, difficulty);
|
||||
}
|
||||
// Add the chart data to the difficulty.
|
||||
difficulty.notes = chartData.notes.get(diffId);
|
||||
difficulty.scrollSpeed = chartData.getScrollSpeed(diffId);
|
||||
difficulty.notes = chartNotes.get(diffId) ?? [];
|
||||
difficulty.scrollSpeed = chartData.getScrollSpeed(diffId) ?? 1.0;
|
||||
|
||||
difficulty.events = chartData.events;
|
||||
}
|
||||
|
@ -193,7 +241,7 @@ class Song implements IPlayStateScriptedClass
|
|||
* @param diffId The difficulty ID, such as `easy` or `hard`.
|
||||
* @return The difficulty data.
|
||||
*/
|
||||
public inline function getDifficulty(diffId:String = null):SongDifficulty
|
||||
public inline function getDifficulty(?diffId:String):Null<SongDifficulty>
|
||||
{
|
||||
if (diffId == null) diffId = difficulties.keys().array()[0];
|
||||
|
||||
|
@ -223,9 +271,11 @@ class Song implements IPlayStateScriptedClass
|
|||
|
||||
public function toString():String
|
||||
{
|
||||
return 'Song($songId)';
|
||||
return 'Song($id)';
|
||||
}
|
||||
|
||||
public function destroy():Void {}
|
||||
|
||||
public function onPause(event:PauseScriptEvent):Void {};
|
||||
|
||||
public function onResume(event:ScriptEvent):Void {};
|
||||
|
@ -265,6 +315,27 @@ class Song implements IPlayStateScriptedClass
|
|||
public function onDestroy(event:ScriptEvent):Void {};
|
||||
|
||||
public function onUpdate(event:UpdateScriptEvent):Void {};
|
||||
|
||||
static function _fetchData(id:String):Null<SongMetadata>
|
||||
{
|
||||
trace('Fetching song metadata for $id');
|
||||
var version:Null<thx.semver.Version> = SongRegistry.instance.fetchEntryMetadataVersion(id);
|
||||
if (version == null) return null;
|
||||
return SongRegistry.instance.parseEntryMetadataWithMigration(id, '', version);
|
||||
}
|
||||
|
||||
function fetchVariationMetadata(id:String):Array<SongMetadata>
|
||||
{
|
||||
var result:Array<SongMetadata> = [];
|
||||
for (vari in variations)
|
||||
{
|
||||
var version:Null<thx.semver.Version> = SongRegistry.instance.fetchEntryMetadataVersion(id, vari);
|
||||
if (version == null) continue;
|
||||
var meta:Null<SongMetadata> = SongRegistry.instance.parseEntryMetadataWithMigration(id, vari, version);
|
||||
if (meta != null) result.push(meta);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class SongDifficulty
|
||||
|
@ -299,7 +370,7 @@ class SongDifficulty
|
|||
public var timeFormat:SongTimeFormat = SongValidator.DEFAULT_TIMEFORMAT;
|
||||
public var divisions:Null<Int> = SongValidator.DEFAULT_DIVISIONS;
|
||||
public var looped:Bool = SongValidator.DEFAULT_LOOPED;
|
||||
public var generatedBy:String = SongValidator.DEFAULT_GENERATEDBY;
|
||||
public var generatedBy:String = SongRegistry.DEFAULT_GENERATEDBY;
|
||||
|
||||
public var timeChanges:Array<SongTimeChange> = [];
|
||||
|
||||
|
@ -351,18 +422,18 @@ class SongDifficulty
|
|||
var currentPlayer:Null<SongPlayableChar> = getPlayableChar(currentPlayerId);
|
||||
if (currentPlayer != null)
|
||||
{
|
||||
FlxG.sound.cache(Paths.inst(this.song.songId, currentPlayer.inst));
|
||||
FlxG.sound.cache(Paths.inst(this.song.id, currentPlayer.inst));
|
||||
}
|
||||
else
|
||||
{
|
||||
FlxG.sound.cache(Paths.inst(this.song.songId));
|
||||
FlxG.sound.cache(Paths.inst(this.song.id));
|
||||
}
|
||||
}
|
||||
|
||||
public inline function playInst(volume:Float = 1.0, looped:Bool = false):Void
|
||||
{
|
||||
var suffix:String = (variation != null && variation != '' && variation != 'default') ? '-$variation' : '';
|
||||
FlxG.sound.playMusic(Paths.inst(this.song.songId, suffix), volume, looped);
|
||||
FlxG.sound.playMusic(Paths.inst(this.song.id, suffix), volume, looped);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -388,7 +459,7 @@ class SongDifficulty
|
|||
var playableCharData:SongPlayableChar = getPlayableChar(id);
|
||||
if (playableCharData == null)
|
||||
{
|
||||
trace('Could not find playable char $id for song ${this.song.songId}');
|
||||
trace('Could not find playable char $id for song ${this.song.id}');
|
||||
return [];
|
||||
}
|
||||
|
||||
|
@ -398,24 +469,24 @@ class SongDifficulty
|
|||
// 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.songId, '-$id$suffix');
|
||||
var voicePlayer:String = Paths.voices(this.song.id, '-$id$suffix');
|
||||
while (voicePlayer != null && !Assets.exists(voicePlayer))
|
||||
{
|
||||
// Remove the last suffix.
|
||||
// For example, bf-car becomes bf.
|
||||
playerId = playerId.split('-').slice(0, -1).join('-');
|
||||
// Try again.
|
||||
voicePlayer = playerId == '' ? null : Paths.voices(this.song.songId, '-${playerId}$suffix');
|
||||
voicePlayer = playerId == '' ? null : Paths.voices(this.song.id, '-${playerId}$suffix');
|
||||
}
|
||||
|
||||
var opponentId:String = playableCharData.opponent;
|
||||
var voiceOpponent:String = Paths.voices(this.song.songId, '-${opponentId}$suffix');
|
||||
var voiceOpponent:String = Paths.voices(this.song.id, '-${opponentId}$suffix');
|
||||
while (voiceOpponent != null && !Assets.exists(voiceOpponent))
|
||||
{
|
||||
// Remove the last suffix.
|
||||
opponentId = opponentId.split('-').slice(0, -1).join('-');
|
||||
// Try again.
|
||||
voiceOpponent = opponentId == '' ? null : Paths.voices(this.song.songId, '-${opponentId}$suffix');
|
||||
voiceOpponent = opponentId == '' ? null : Paths.voices(this.song.id, '-${opponentId}$suffix');
|
||||
}
|
||||
|
||||
var result:Array<String> = [];
|
||||
|
@ -424,7 +495,7 @@ class SongDifficulty
|
|||
if (voicePlayer == null && voiceOpponent == null)
|
||||
{
|
||||
// Try to use `Voices.ogg` if no other voices are found.
|
||||
if (Assets.exists(Paths.voices(this.song.songId, ''))) result.push(Paths.voices(this.song.songId, '$suffix'));
|
||||
if (Assets.exists(Paths.voices(this.song.id, ''))) result.push(Paths.voices(this.song.id, '$suffix'));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
@ -442,7 +513,7 @@ class SongDifficulty
|
|||
|
||||
if (voiceList.length == 0)
|
||||
{
|
||||
trace('Could not find any voices for song ${this.song.songId}');
|
||||
trace('Could not find any voices for song ${this.song.id}');
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,11 +1,11 @@
|
|||
package funkin.play.song;
|
||||
|
||||
import funkin.play.song.formats.FNFLegacy;
|
||||
import funkin.play.song.SongData.SongChartData;
|
||||
import funkin.play.song.SongData.SongEventData;
|
||||
import funkin.play.song.SongData.SongMetadata;
|
||||
import funkin.play.song.SongData.SongNoteData;
|
||||
import funkin.play.song.SongData.SongPlayableChar;
|
||||
import funkin.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
|
||||
|
@ -176,7 +176,7 @@ class SongMigrator
|
|||
songMetadata.playData.songVariations = [];
|
||||
|
||||
// Set the song's song variations.
|
||||
songMetadata.playData.playableChars = {};
|
||||
songMetadata.playData.playableChars = [];
|
||||
try
|
||||
{
|
||||
Reflect.setField(songMetadata.playData.playableChars, songData.song.player1, new SongPlayableChar('', songData.song.player2));
|
||||
|
@ -203,7 +203,7 @@ class SongMigrator
|
|||
|
||||
var songData:FNFLegacy = cast jsonData;
|
||||
|
||||
var songChartData:SongChartData = new SongChartData(1.0, [], []);
|
||||
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));
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package funkin.play.song;
|
||||
|
||||
import funkin.play.song.SongData.SongChartData;
|
||||
import funkin.play.song.SongData.SongMetadata;
|
||||
import funkin.data.song.SongData.SongChartData;
|
||||
import funkin.data.song.SongData.SongMetadata;
|
||||
import funkin.util.SerializerUtil;
|
||||
import lime.utils.Bytes;
|
||||
import openfl.events.Event;
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
package funkin.play.song;
|
||||
|
||||
import funkin.play.song.SongData.SongChartData;
|
||||
import funkin.play.song.SongData.SongMetadata;
|
||||
import funkin.play.song.SongData.SongPlayData;
|
||||
import funkin.play.song.SongData.SongTimeChange;
|
||||
import funkin.play.song.SongData.SongTimeFormat;
|
||||
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,
|
||||
|
@ -59,7 +60,7 @@ class SongValidator
|
|||
}
|
||||
if (input.generatedBy == null)
|
||||
{
|
||||
input.generatedBy = DEFAULT_GENERATEDBY;
|
||||
input.generatedBy = SongRegistry.DEFAULT_GENERATEDBY;
|
||||
}
|
||||
|
||||
input.timeChanges = validateTimeChanges(input.timeChanges, songId);
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package funkin.ui.debug.charting;
|
||||
|
||||
import funkin.play.song.SongData.SongEventData;
|
||||
import funkin.play.song.SongData.SongNoteData;
|
||||
import funkin.play.song.SongDataUtils;
|
||||
import funkin.data.song.SongData.SongEventData;
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
import funkin.data.song.SongDataUtils;
|
||||
|
||||
using Lambda;
|
||||
|
||||
|
|
|
@ -3,8 +3,8 @@ package funkin.ui.debug.charting;
|
|||
import funkin.play.character.CharacterData;
|
||||
import funkin.util.Constants;
|
||||
import funkin.util.SerializerUtil;
|
||||
import funkin.play.song.SongData.SongChartData;
|
||||
import funkin.play.song.SongData.SongMetadata;
|
||||
import funkin.data.song.SongData.SongChartData;
|
||||
import funkin.data.song.SongData.SongMetadata;
|
||||
import flixel.util.FlxTimer;
|
||||
import funkin.util.SortUtil;
|
||||
import funkin.input.Cursor;
|
||||
|
@ -13,9 +13,9 @@ import funkin.play.character.CharacterData.CharacterDataParser;
|
|||
import funkin.play.song.Song;
|
||||
import funkin.play.song.SongMigrator;
|
||||
import funkin.play.song.SongValidator;
|
||||
import funkin.play.song.SongData.SongDataParser;
|
||||
import funkin.play.song.SongData.SongPlayableChar;
|
||||
import funkin.play.song.SongData.SongTimeChange;
|
||||
import funkin.data.song.SongRegistry;
|
||||
import funkin.data.song.SongData.SongPlayableChar;
|
||||
import funkin.data.song.SongData.SongTimeChange;
|
||||
import funkin.util.FileUtil;
|
||||
import haxe.io.Path;
|
||||
import haxe.ui.components.Button;
|
||||
|
@ -112,23 +112,17 @@ class ChartEditorDialogHandler
|
|||
var splashTemplateContainer:Null<VBox> = dialog.findComponent('splashTemplateContainer', VBox);
|
||||
if (splashTemplateContainer == null) throw 'Could not locate splashTemplateContainer in Welcome dialog';
|
||||
|
||||
var songList:Array<String> = SongDataParser.listSongIds();
|
||||
var songList:Array<String> = SongRegistry.instance.listEntryIds();
|
||||
songList.sort(SortUtil.alphabetically);
|
||||
|
||||
for (targetSongId in songList)
|
||||
{
|
||||
var songData:Null<Song> = SongDataParser.fetchSong(targetSongId);
|
||||
|
||||
var songData:Null<Song> = SongRegistry.instance.fetchEntry(targetSongId);
|
||||
if (songData == null) continue;
|
||||
|
||||
var diffNormal:Null<SongDifficulty> = songData.getDifficulty('normal');
|
||||
var songName:Null<String> = diffNormal?.songName;
|
||||
if (songName == null)
|
||||
{
|
||||
var diffDefault:Null<SongDifficulty> = songData.getDifficulty();
|
||||
songName = diffDefault?.songName;
|
||||
}
|
||||
if (songName == null)
|
||||
var songName:Null<String> = songData.getDifficulty('normal')?.songName;
|
||||
if (songName == null) songName = songData.getDifficulty()?.songName;
|
||||
if (songName == null) // Still null?
|
||||
{
|
||||
trace('[WARN] Could not fetch song name for ${targetSongId}');
|
||||
continue;
|
||||
|
@ -508,9 +502,9 @@ class ChartEditorDialogHandler
|
|||
if (dialogNoteSkin == null) throw 'Could not locate dialogNoteSkin DropDown in Song Metadata dialog';
|
||||
dialogNoteSkin.onChange = function(event:UIEvent) {
|
||||
if (event.data.id == null) return;
|
||||
state.currentSongMetadata.playData.noteSkin = event.data.id;
|
||||
state.currentSongNoteSkin = event.data.id;
|
||||
};
|
||||
state.currentSongMetadata.playData.noteSkin = 'funkin';
|
||||
state.currentSongNoteSkin = 'funkin';
|
||||
|
||||
var dialogBPM:Null<NumberStepper> = dialog.findComponent('dialogBPM', NumberStepper);
|
||||
if (dialogBPM == null) throw 'Could not locate dialogBPM NumberStepper in Song Metadata dialog';
|
||||
|
@ -520,7 +514,7 @@ class ChartEditorDialogHandler
|
|||
var timeChanges:Array<SongTimeChange> = state.currentSongMetadata.timeChanges;
|
||||
if (timeChanges == null || timeChanges.length == 0)
|
||||
{
|
||||
timeChanges = [new SongTimeChange(-1, 0, event.value, 4, 4, [4, 4, 4, 4])];
|
||||
timeChanges = [new SongTimeChange(0, event.value)];
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -543,7 +537,7 @@ class ChartEditorDialogHandler
|
|||
};
|
||||
|
||||
// Empty the character list.
|
||||
state.currentSongMetadata.playData.playableChars = {};
|
||||
state.currentSongMetadata.playData.playableChars = [];
|
||||
// Add at least one character group with no Remove button.
|
||||
dialogCharGrid.addComponent(buildCharGroup(state, 'bf'));
|
||||
|
||||
|
@ -558,7 +552,8 @@ class ChartEditorDialogHandler
|
|||
{
|
||||
var groupKey:String = key;
|
||||
|
||||
var getCharData:Void->SongPlayableChar = function() {
|
||||
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);
|
||||
|
@ -570,46 +565,53 @@ class ChartEditorDialogHandler
|
|||
return result;
|
||||
}
|
||||
|
||||
var moveCharGroup:String->Void = function(target:String) {
|
||||
var charData = getCharData();
|
||||
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() {
|
||||
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:SongPlayableChar = getCharData();
|
||||
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) {
|
||||
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) {
|
||||
charGroupOpponent.onChange = function(event:UIEvent):Void {
|
||||
if (charData == null) return;
|
||||
charData.opponent = event.data.id;
|
||||
};
|
||||
charGroupOpponent.value = getCharData().opponent;
|
||||
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) {
|
||||
charGroupGirlfriend.onChange = function(event:UIEvent):Void {
|
||||
if (charData == null) return;
|
||||
charData.girlfriend = event.data.id;
|
||||
};
|
||||
charGroupGirlfriend.value = getCharData().girlfriend;
|
||||
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) {
|
||||
charGroupRemove.onClick = function(event:UIEvent):Void {
|
||||
removeGroup();
|
||||
};
|
||||
|
||||
|
@ -630,7 +632,8 @@ class ChartEditorDialogHandler
|
|||
|
||||
for (charKey in state.currentSongMetadata.playData.playableChars.keys())
|
||||
{
|
||||
var charData:SongPlayableChar = state.currentSongMetadata.playData.playableChars.get(charKey);
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package funkin.ui.debug.charting;
|
||||
|
||||
import funkin.play.event.SongEventData.SongEventParser;
|
||||
import funkin.data.event.SongEventData.SongEventParser;
|
||||
import flixel.graphics.frames.FlxAtlasFrames;
|
||||
import openfl.display.BitmapData;
|
||||
import openfl.utils.Assets;
|
||||
|
@ -10,7 +10,7 @@ import flixel.FlxSprite;
|
|||
import flixel.graphics.frames.FlxFramesCollection;
|
||||
import flixel.graphics.frames.FlxTileFrames;
|
||||
import flixel.math.FlxPoint;
|
||||
import funkin.play.song.SongData.SongEventData;
|
||||
import funkin.data.song.SongData.SongEventData;
|
||||
|
||||
/**
|
||||
* A event sprite that can be used to display a song event in a chart.
|
||||
|
|
|
@ -8,7 +8,7 @@ import flixel.graphics.frames.FlxFramesCollection;
|
|||
import flixel.graphics.frames.FlxTileFrames;
|
||||
import flixel.math.FlxPoint;
|
||||
import funkin.play.notes.SustainTrail;
|
||||
import funkin.play.song.SongData.SongNoteData;
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
|
||||
/**
|
||||
* A hold note sprite that can be used to display a note in a chart.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package funkin.ui.debug.charting;
|
||||
|
||||
import funkin.play.song.SongData.SongEventData;
|
||||
import funkin.play.song.SongData.SongNoteData;
|
||||
import funkin.data.song.SongData.SongEventData;
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
import flixel.math.FlxMath;
|
||||
import flixel.FlxSprite;
|
||||
import flixel.util.FlxColor;
|
||||
|
|
|
@ -7,7 +7,7 @@ import flixel.graphics.frames.FlxAtlasFrames;
|
|||
import flixel.graphics.frames.FlxFrame;
|
||||
import flixel.graphics.frames.FlxTileFrames;
|
||||
import flixel.math.FlxPoint;
|
||||
import funkin.play.song.SongData.SongNoteData;
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
|
||||
/**
|
||||
* A note sprite that can be used to display a note in a chart.
|
||||
|
|
|
@ -36,13 +36,13 @@ import funkin.play.notes.NoteSprite;
|
|||
import funkin.play.notes.Strumline;
|
||||
import funkin.play.PlayState;
|
||||
import funkin.play.song.Song;
|
||||
import funkin.play.song.SongData.SongChartData;
|
||||
import funkin.play.song.SongData.SongDataParser;
|
||||
import funkin.play.song.SongData.SongEventData;
|
||||
import funkin.play.song.SongData.SongMetadata;
|
||||
import funkin.play.song.SongData.SongNoteData;
|
||||
import funkin.play.song.SongData.SongPlayableChar;
|
||||
import funkin.play.song.SongDataUtils;
|
||||
import funkin.data.song.SongData.SongChartData;
|
||||
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.SongDataUtils;
|
||||
import funkin.ui.debug.charting.ChartEditorCommand;
|
||||
import funkin.ui.debug.charting.ChartEditorCommand;
|
||||
import funkin.ui.debug.charting.ChartEditorThemeHandler.ChartEditorTheme;
|
||||
|
@ -865,7 +865,7 @@ class ChartEditorState extends HaxeUIState
|
|||
var result:Null<SongChartData> = songChartData.get(selectedVariation);
|
||||
if (result == null)
|
||||
{
|
||||
result = new SongChartData(1.0, [], []);
|
||||
result = new SongChartData(["normal" => 1.0], [], ["normal" => []]);
|
||||
songChartData.set(selectedVariation, result);
|
||||
}
|
||||
return result;
|
||||
|
@ -4003,7 +4003,7 @@ class ChartEditorState extends HaxeUIState
|
|||
*/
|
||||
public function loadSongAsTemplate(songId:String):Void
|
||||
{
|
||||
var song:Null<Song> = SongDataParser.fetchSong(songId);
|
||||
var song:Null<Song> = SongRegistry.instance.fetchEntry(songId);
|
||||
|
||||
if (song == null) return;
|
||||
|
||||
|
@ -4021,7 +4021,7 @@ class ChartEditorState extends HaxeUIState
|
|||
var metadataClone = Reflect.copy(metadata);
|
||||
if (metadataClone != null) songMetadata.set(variation, metadataClone);
|
||||
|
||||
songChartData.set(variation, SongDataParser.parseSongChartData(songId, metadata.variation));
|
||||
songChartData.set(variation, SongRegistry.instance.parseEntryChartData(songId, metadata.variation));
|
||||
}
|
||||
|
||||
loadSong(songMetadata, songChartData);
|
||||
|
|
|
@ -4,8 +4,8 @@ import haxe.ui.containers.TreeView;
|
|||
import haxe.ui.containers.TreeViewNode;
|
||||
import funkin.play.character.BaseCharacter.CharacterType;
|
||||
import funkin.play.event.SongEvent;
|
||||
import funkin.play.event.SongEventData;
|
||||
import funkin.play.song.SongData.SongTimeChange;
|
||||
import funkin.data.event.SongEventData;
|
||||
import funkin.data.song.SongData.SongTimeChange;
|
||||
import funkin.play.song.SongSerializer;
|
||||
import funkin.ui.haxeui.components.CharacterPlayer;
|
||||
import haxe.ui.components.Button;
|
||||
|
@ -569,9 +569,9 @@ class ChartEditorToolboxHandler
|
|||
if (inputNoteSkin == null) throw 'ChartEditorToolboxHandler.buildToolboxMetadataLayout() - Could not find inputNoteSkin component.';
|
||||
inputNoteSkin.onChange = function(event:UIEvent) {
|
||||
if ((event?.data?.id ?? null) == null) return;
|
||||
state.currentSongMetadata.playData.noteSkin = event.data.id;
|
||||
state.currentSongNoteSkin = event.data.id;
|
||||
};
|
||||
inputNoteSkin.value = state.currentSongMetadata.playData.noteSkin;
|
||||
inputNoteSkin.value = state.currentSongNoteSkin;
|
||||
|
||||
var inputBPM:Null<NumberStepper> = toolbox.findComponent('inputBPM', NumberStepper);
|
||||
if (inputBPM == null) throw 'ChartEditorToolboxHandler.buildToolboxMetadataLayout() - Could not find inputBPM component.';
|
||||
|
@ -581,7 +581,7 @@ class ChartEditorToolboxHandler
|
|||
var timeChanges:Array<SongTimeChange> = state.currentSongMetadata.timeChanges;
|
||||
if (timeChanges == null || timeChanges.length == 0)
|
||||
{
|
||||
timeChanges = [new SongTimeChange(-1, 0, event.value, 4, 4, [4, 4, 4, 4])];
|
||||
timeChanges = [new SongTimeChange(0, event.value)];
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
@ -4,6 +4,7 @@ import flixel.FlxSprite;
|
|||
import flixel.util.FlxColor;
|
||||
import funkin.play.song.Song;
|
||||
import funkin.data.IRegistryEntry;
|
||||
import funkin.data.song.SongRegistry;
|
||||
import funkin.data.level.LevelRegistry;
|
||||
import funkin.data.level.LevelData;
|
||||
|
||||
|
@ -70,17 +71,20 @@ class Level implements IRegistryEntry<LevelData>
|
|||
public function getSongDisplayNames(difficulty:String):Array<String>
|
||||
{
|
||||
var songList:Array<String> = getSongs() ?? [];
|
||||
var songNameList:Array<String> = songList.map(function(songId) {
|
||||
var song:Song = funkin.play.song.SongData.SongDataParser.fetchSong(songId);
|
||||
if (song == null) return 'Unknown';
|
||||
var songDifficulty:SongDifficulty = song.getDifficulty(difficulty);
|
||||
if (songDifficulty == null) songDifficulty = song.getDifficulty();
|
||||
var songName:String = songDifficulty?.songName;
|
||||
return songName ?? 'Unknown';
|
||||
var songNameList:Array<String> = songList.map(function(songId:String) {
|
||||
return getSongDisplayName(songId, difficulty);
|
||||
});
|
||||
return songNameList;
|
||||
}
|
||||
|
||||
static function getSongDisplayName(songId:String, difficulty:String):String
|
||||
{
|
||||
var song:Null<Song> = SongRegistry.instance.fetchEntry(songId);
|
||||
if (song == null) return 'Unknown';
|
||||
|
||||
return song.songName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this level is unlocked. If not, it will be greyed out on the menu and have a lock icon.
|
||||
* TODO: Change this behavior in a later release.
|
||||
|
@ -120,7 +124,7 @@ class Level implements IRegistryEntry<LevelData>
|
|||
var songList = getSongs();
|
||||
|
||||
var firstSongId:String = songList[0];
|
||||
var firstSong:Song = funkin.play.song.SongData.SongDataParser.fetchSong(firstSongId);
|
||||
var firstSong:Song = SongRegistry.instance.fetchEntry(firstSongId);
|
||||
|
||||
if (firstSong != null)
|
||||
{
|
||||
|
@ -134,7 +138,7 @@ class Level implements IRegistryEntry<LevelData>
|
|||
for (songIndex in 1...songList.length)
|
||||
{
|
||||
var songId:String = songList[songIndex];
|
||||
var song:Song = funkin.play.song.SongData.SongDataParser.fetchSong(songId);
|
||||
var song:Song = SongRegistry.instance.fetchEntry(songId);
|
||||
|
||||
if (song == null) continue;
|
||||
|
||||
|
@ -179,7 +183,7 @@ class Level implements IRegistryEntry<LevelData>
|
|||
return 'Level($id)';
|
||||
}
|
||||
|
||||
public function _fetchData(id:String):Null<LevelData>
|
||||
static function _fetchData(id:String):Null<LevelData>
|
||||
{
|
||||
return LevelRegistry.instance.parseEntryDataWithMigration(id, LevelRegistry.instance.fetchEntryVersion(id));
|
||||
}
|
||||
|
|
|
@ -16,8 +16,8 @@ import funkin.modding.events.ScriptEventDispatcher;
|
|||
import funkin.play.PlayState;
|
||||
import funkin.play.PlayStatePlaylist;
|
||||
import funkin.play.song.Song;
|
||||
import funkin.play.song.SongData.SongMetadata;
|
||||
import funkin.play.song.SongData.SongDataParser;
|
||||
import funkin.data.song.SongData.SongMusicData;
|
||||
import funkin.data.song.SongRegistry;
|
||||
|
||||
class StoryMenuState extends MusicBeatState
|
||||
{
|
||||
|
@ -201,8 +201,11 @@ class StoryMenuState extends MusicBeatState
|
|||
{
|
||||
if (FlxG.sound.music == null || !FlxG.sound.music.playing)
|
||||
{
|
||||
var freakyMenuMetadata:SongMetadata = SongDataParser.parseMusicMetadata('freakyMenu');
|
||||
Conductor.mapTimeChanges(freakyMenuMetadata.timeChanges);
|
||||
var freakyMenuMetadata:Null<SongMusicData> = SongRegistry.instance.parseMusicData('freakyMenu');
|
||||
if (freakyMenuMetadata != null)
|
||||
{
|
||||
Conductor.mapTimeChanges(freakyMenuMetadata.timeChanges);
|
||||
}
|
||||
|
||||
FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'), 0);
|
||||
FlxG.sound.music.fadeIn(4, 0, 0.7);
|
||||
|
@ -509,7 +512,7 @@ class StoryMenuState extends MusicBeatState
|
|||
|
||||
var targetSongId:String = PlayStatePlaylist.playlistSongIds.shift();
|
||||
|
||||
var targetSong:Song = SongDataParser.fetchSong(targetSongId);
|
||||
var targetSong:Song = SongRegistry.instance.fetchEntry(targetSongId);
|
||||
|
||||
PlayStatePlaylist.campaignId = currentLevel.id;
|
||||
PlayStatePlaylist.campaignTitle = currentLevel.getTitle();
|
||||
|
|
|
@ -12,8 +12,8 @@ import flixel.util.FlxTimer;
|
|||
import funkin.audiovis.SpectogramSprite;
|
||||
import funkin.shaderslmfao.ColorSwap;
|
||||
import funkin.shaderslmfao.LeftMaskShader;
|
||||
import funkin.play.song.SongData.SongDataParser;
|
||||
import funkin.play.song.SongData.SongMetadata;
|
||||
import funkin.data.song.SongRegistry;
|
||||
import funkin.data.song.SongData.SongMusicData;
|
||||
import funkin.shaderslmfao.TitleOutline;
|
||||
import funkin.ui.AtlasText;
|
||||
import openfl.Assets;
|
||||
|
@ -216,9 +216,11 @@ class TitleState extends MusicBeatState
|
|||
{
|
||||
if (FlxG.sound.music == null || !FlxG.sound.music.playing)
|
||||
{
|
||||
var freakyMenuMetadata:SongMetadata = SongDataParser.parseMusicMetadata('freakyMenu');
|
||||
Conductor.mapTimeChanges(freakyMenuMetadata.timeChanges);
|
||||
|
||||
var freakyMenuMetadata:Null<SongMusicData> = SongRegistry.instance.parseMusicData('freakyMenu');
|
||||
if (freakyMenuMetadata != null)
|
||||
{
|
||||
Conductor.mapTimeChanges(freakyMenuMetadata.timeChanges);
|
||||
}
|
||||
FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'), 0);
|
||||
FlxG.sound.music.fadeIn(4, 0, 0.7);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package funkin.util;
|
||||
|
||||
import flixel.graphics.frames.FlxFrame;
|
||||
#if !macro
|
||||
import flixel.FlxBasic;
|
||||
import flixel.util.FlxSort;
|
||||
|
@ -41,6 +42,16 @@ class SortUtil
|
|||
return FlxSort.byValues(order, a.noteData.time, b.noteData.time);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given two FlxFrames, sort their names alphabetically.
|
||||
*
|
||||
* @param order Either `FlxSort.ASCENDING` or `FlxSort.DESCENDING`
|
||||
*/
|
||||
public static inline function byFrameName(a:FlxFrame, b:FlxFrame)
|
||||
{
|
||||
return alphabetically(a.name, b.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort predicate for sorting strings alphabetically.
|
||||
* @param a The first string to compare.
|
||||
|
|
|
@ -13,4 +13,22 @@ class IteratorTools
|
|||
{
|
||||
return [for (i in iterator) i];
|
||||
}
|
||||
|
||||
public static function count<T>(iterator:Iterator<T>, ?predicate:(item:T) -> Bool):Int
|
||||
{
|
||||
var n = 0;
|
||||
|
||||
if (predicate == null)
|
||||
{
|
||||
for (_ in iterator)
|
||||
n++;
|
||||
}
|
||||
else
|
||||
{
|
||||
for (x in iterator)
|
||||
if (predicate(x)) n++;
|
||||
}
|
||||
|
||||
return n;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"note": {
|
||||
"assetPath": "shared:arrows",
|
||||
"scale": 1.0,
|
||||
"isPixel": true,
|
||||
"isPixel": false,
|
||||
"data": {
|
||||
"left": { "prefix": "noteLeft" },
|
||||
"down": { "prefix": "noteDown" },
|
||||
|
@ -19,7 +19,7 @@
|
|||
"assetPath": "shared:arrows",
|
||||
"scale": 1.0,
|
||||
"offsets": [28, 32],
|
||||
"isPixel": true,
|
||||
"isPixel": false,
|
||||
"data": {
|
||||
"leftStatic": { "prefix": "staticLeft0" },
|
||||
"leftPress": { "prefix": "pressedLeft0" },
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"songName": "Bopeebo",
|
||||
"artist": "Kawai Sprite",
|
||||
"timeFormat": "ms",
|
||||
"timeChanges": [{ "t": 0, "bpm": 100, "n": 4, "d": 4, "bt": [4, 4, 4, 4] }],
|
||||
"playData": {
|
||||
"songVariations": [],
|
||||
"difficulties": ["easy", "normal", "hard"],
|
||||
"playableChars": { "bf": { "g": "gf", "o": "dad" } },
|
||||
"stage": "mainStage",
|
||||
"noteSkin": "Normal"
|
||||
},
|
||||
"generatedBy": "MasterEric (by hand)"
|
||||
}
|
File diff suppressed because it is too large
Load diff
1738
tests/unit/assets/preload/data/songs/bopeebo/bopeebo-chart.json
Normal file
1738
tests/unit/assets/preload/data/songs/bopeebo/bopeebo-chart.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"songName": "Bopeebo",
|
||||
"artist": "Kawai Sprite",
|
||||
"timeFormat": "ms",
|
||||
"timeChanges": [{ "t": 0, "bpm": 100, "n": 4, "d": 4, "bt": [4, 4, 4, 4] }],
|
||||
"playData": {
|
||||
"songVariations": [],
|
||||
"difficulties": ["easy", "normal", "hard"],
|
||||
"playableChars": { "bf": { "g": "gf", "o": "dad" } },
|
||||
"stage": "mainStage",
|
||||
"noteSkin": "Normal"
|
||||
},
|
||||
"generatedBy": "MasterEric (by hand)"
|
||||
}
|
|
@ -44,11 +44,12 @@
|
|||
<!-- Assets -->
|
||||
<assets path="assets/preload" rename="assets" exclude="*.ogg" if="web" />
|
||||
<assets path="assets/preload" rename="assets" exclude="*.mp3" unless="web" />
|
||||
<assets path="assets/shared" library="shared" exclude="*.fla|*.ogg" if="web" />
|
||||
<assets path="assets/shared" library="shared" exclude="*.fla|*.mp3" unless="web" />
|
||||
<library name="shared" preload="true" />
|
||||
<!--
|
||||
<assets path="assets/songs" library="songs" exclude="*.fla|*.ogg" if="web" />
|
||||
<assets path="assets/songs" library="songs" exclude="*.fla|*.mp3" unless="web" />
|
||||
<assets path="assets/shared" library="shared" exclude="*.fla|*.ogg" if="web" />
|
||||
<assets path="assets/shared" library="shared" exclude="*.fla|*.mp3" unless="web" />
|
||||
<assets path="assets/tutorial" library="tutorial" exclude="*.fla|*.ogg" if="web" />
|
||||
<assets path="assets/tutorial" library="tutorial" exclude="*.fla|*.mp3" unless="web" />
|
||||
<assets path="assets/week1" library="week1" exclude="*.fla|*.ogg" if="web" />
|
||||
|
@ -68,7 +69,6 @@
|
|||
<assets path="assets/weekend1" library="weekend1" exclude="*.fla|*.ogg" if="web" />
|
||||
<assets path="assets/weekend1" library="weekend1" exclude="*.fla|*.mp3" unless="web" />
|
||||
<library name="songs" preload="true" />
|
||||
<library name="shared" preload="true" />
|
||||
<library name="tutorial" preload="true" />
|
||||
<library name="week1" preload="true" />
|
||||
<library name="week2" preload="true" />
|
||||
|
@ -87,8 +87,8 @@
|
|||
<haxedef name="FLX_RECORD" />
|
||||
|
||||
<!-- Clean up the output -->
|
||||
<haxedef name="no-traces" />
|
||||
<!--
|
||||
<haxedef name="echo-traces" />
|
||||
-->
|
||||
<haxedef name="ignore-inline" />
|
||||
<haxeflag name="-w" value="-WDeprecated" />
|
||||
|
|
|
@ -127,4 +127,54 @@ class FunkinAssert
|
|||
};
|
||||
validateThrows(targetFunc, predicate, info);
|
||||
}
|
||||
|
||||
static var capturedTraces:Array<String> = [];
|
||||
|
||||
public static function initAssertTrace():Void
|
||||
{
|
||||
var oldTrace = haxe.Log.trace;
|
||||
haxe.Log.trace = function(v:Dynamic, ?infos:haxe.PosInfos) {
|
||||
onTrace(v, infos);
|
||||
// oldTrace(v, infos);
|
||||
};
|
||||
}
|
||||
|
||||
public static function clearTraces():Void
|
||||
{
|
||||
capturedTraces = [];
|
||||
}
|
||||
|
||||
@:nullSafety(Off) // Why isn't haxe.std null-safe?
|
||||
static function onTrace(v:Dynamic, ?infos:haxe.PosInfos)
|
||||
{
|
||||
// var str:String = haxe.Log.formatOutput(v, infos);
|
||||
var str:String = Std.string(v);
|
||||
capturedTraces.push(str);
|
||||
|
||||
#if (sys && echo_traces)
|
||||
Sys.println('[TESTLOG] $str');
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the first string that was traced and validate it.
|
||||
* @param expected
|
||||
*/
|
||||
public static inline function assertTrace(expected:String):Void
|
||||
{
|
||||
var actual:Null<String> = capturedTraces.shift();
|
||||
Assert.isNotNull(actual);
|
||||
Assert.areEqual(expected, actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the first string that was traced and validate it.
|
||||
* @param expected
|
||||
*/
|
||||
public static inline function assertLastTrace(expected:String):Void
|
||||
{
|
||||
var actual:Null<String> = capturedTraces.pop();
|
||||
Assert.isNotNull(actual);
|
||||
Assert.areEqual(expected, actual);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,8 +30,8 @@ class MockTest extends FunkinTest
|
|||
{
|
||||
// Test that mocking works.
|
||||
|
||||
var mockSprite = mockatoo.Mockatoo.mock(flixel.FlxSprite);
|
||||
var mockAnim = mockatoo.Mockatoo.mock(flixel.animation.FlxAnimationController);
|
||||
var mockSprite = Mockatoo.mock(flixel.FlxSprite);
|
||||
var mockAnim = Mockatoo.mock(flixel.animation.FlxAnimationController);
|
||||
mockSprite.animation = mockAnim;
|
||||
|
||||
var animData:funkin.data.animation.AnimationData =
|
||||
|
@ -44,12 +44,12 @@ class MockTest extends FunkinTest
|
|||
|
||||
// Verify that the method was called once.
|
||||
// If not, a VerificationException will be thrown and the test will fail.
|
||||
mockatoo.Mockatoo.verify(mockAnim.addByPrefix("testAnim", "blablabla", 24, false, false, false), times(1));
|
||||
mockAnim.addByPrefix("testAnim", "blablabla", 24, false, false, false).verify(times(1));
|
||||
|
||||
FunkinAssert.validateThrows(function() {
|
||||
// Attempt to verify the method was called.
|
||||
// This should FAIL, since we didn't call the method.
|
||||
mockatoo.Mockatoo.verify(mockAnim.addByPrefix("testAnim", "blablabla", 24, false, false, false), times(1));
|
||||
mockAnim.addByPrefix("testAnim", "blablabla", 24, false, false, false).verify(times(1));
|
||||
}, function(err) {
|
||||
return Std.isOfType(err, mockatoo.exception.VerificationException);
|
||||
});
|
||||
|
|
|
@ -3,7 +3,7 @@ package funkin;
|
|||
import flixel.FlxG;
|
||||
import flixel.FlxState;
|
||||
import funkin.Conductor;
|
||||
import funkin.play.song.SongData.SongTimeChange;
|
||||
import funkin.data.song.SongData.SongTimeChange;
|
||||
import funkin.util.Constants;
|
||||
import massive.munit.Assert;
|
||||
|
||||
|
@ -16,6 +16,8 @@ class ConductorTest extends FunkinTest
|
|||
@Before
|
||||
function before()
|
||||
{
|
||||
FunkinAssert.initAssertTrace();
|
||||
|
||||
resetGame();
|
||||
|
||||
// The ConductorState will advance the conductor when step() is called.
|
||||
|
@ -193,16 +195,7 @@ class ConductorTest extends FunkinTest
|
|||
function testSingleTimeChange():Void
|
||||
{
|
||||
// Start the song with a BPM of 120.
|
||||
var songTimeChanges:Array<SongTimeChange> = [
|
||||
{
|
||||
t: 0,
|
||||
b: 0,
|
||||
bpm: 120,
|
||||
n: 4,
|
||||
d: 4,
|
||||
bt: [4, 4, 4, 4]
|
||||
}, // 120 bpm starting 0 sec/0 beats
|
||||
];
|
||||
var songTimeChanges:Array<SongTimeChange> = [new SongTimeChange(0, 120)];
|
||||
Conductor.mapTimeChanges(songTimeChanges);
|
||||
|
||||
// All should be at 0.
|
||||
|
@ -253,24 +246,7 @@ class ConductorTest extends FunkinTest
|
|||
function testDoubleTimeChange():Void
|
||||
{
|
||||
// Start the song with a BPM of 120.
|
||||
var songTimeChanges:Array<SongTimeChange> = [
|
||||
{
|
||||
t: 0,
|
||||
b: 0,
|
||||
bpm: 120,
|
||||
n: 4,
|
||||
d: 4,
|
||||
bt: [4, 4, 4, 4]
|
||||
}, // 120 bpm starting 0 sec/0 beats
|
||||
{
|
||||
t: 3000,
|
||||
b: 6,
|
||||
bpm: 90,
|
||||
n: 4,
|
||||
d: 4,
|
||||
bt: [4, 4, 4, 4]
|
||||
} // 90 bpm starting 3 sec/6 beats
|
||||
];
|
||||
var songTimeChanges:Array<SongTimeChange> = [new SongTimeChange(0, 120), new SongTimeChange(3000, 90)];
|
||||
Conductor.mapTimeChanges(songTimeChanges);
|
||||
|
||||
// All should be at 0.
|
||||
|
@ -354,30 +330,9 @@ class ConductorTest extends FunkinTest
|
|||
{
|
||||
// Start the song with a BPM of 120, then move to 90, then move to 180.
|
||||
var songTimeChanges:Array<SongTimeChange> = [
|
||||
{
|
||||
t: 0,
|
||||
b: null,
|
||||
bpm: 120,
|
||||
n: 4,
|
||||
d: 4,
|
||||
bt: [4, 4, 4, 4]
|
||||
}, // 120 bpm starting 0 sec/0 beats
|
||||
{
|
||||
t: 3000,
|
||||
b: null,
|
||||
bpm: 90,
|
||||
n: 4,
|
||||
d: 4,
|
||||
bt: [4, 4, 4, 4]
|
||||
}, // 90 bpm starting 3 sec/6 beats
|
||||
{
|
||||
t: 6000,
|
||||
b: null,
|
||||
bpm: 180,
|
||||
n: 4,
|
||||
d: 4,
|
||||
bt: [4, 4, 4, 4]
|
||||
} // 90 bpm starting 3 sec/6 beats
|
||||
new SongTimeChange(0, 120),
|
||||
new SongTimeChange(3000, 90),
|
||||
new SongTimeChange(6000, 180)
|
||||
];
|
||||
Conductor.mapTimeChanges(songTimeChanges);
|
||||
|
||||
|
|
|
@ -17,7 +17,10 @@ class BaseRegistryTest extends FunkinTest
|
|||
}
|
||||
|
||||
@BeforeClass
|
||||
public function beforeClass() {}
|
||||
public function beforeClass()
|
||||
{
|
||||
FunkinAssert.initAssertTrace();
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public function afterClass() {}
|
||||
|
@ -118,7 +121,7 @@ class MyType implements IRegistryEntry<MyTypeData>
|
|||
return 'MyType($id)';
|
||||
}
|
||||
|
||||
public function _fetchData(id:String):Null<MyTypeData>
|
||||
static function _fetchData(id:String):Null<MyTypeData>
|
||||
{
|
||||
return MyTypeRegistry.instance.parseEntryDataWithMigration(id, MyTypeRegistry.instance.fetchEntryVersion(id));
|
||||
}
|
||||
|
@ -153,17 +156,18 @@ class MyTypeRegistry extends BaseRegistry<MyType, MyTypeData>
|
|||
// JsonParser does not take type parameters,
|
||||
// otherwise this function would be in BaseRegistry.
|
||||
var parser = new json2object.JsonParser<MyTypeData>();
|
||||
var jsonStr:String = loadEntryFile(id);
|
||||
|
||||
parser.fromJson(jsonStr);
|
||||
switch (loadEntryFile(id))
|
||||
{
|
||||
case {fileName: fileName, contents: contents}:
|
||||
parser.fromJson(contents, fileName);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
{
|
||||
trace('[${registryId}] Failed to parse entry data: ${id}');
|
||||
for (error in parser.errors)
|
||||
{
|
||||
trace(error);
|
||||
}
|
||||
printErrors(parser.errors, id);
|
||||
return null;
|
||||
}
|
||||
return parser.value;
|
||||
|
@ -177,31 +181,33 @@ class MyTypeRegistry extends BaseRegistry<MyType, MyTypeData>
|
|||
// JsonParser does not take type parameters,
|
||||
// otherwise this function would be in BaseRegistry.
|
||||
var parser = new json2object.JsonParser<MyTypeData_v0_1_x>();
|
||||
var jsonStr:String = loadEntryFile(id);
|
||||
|
||||
parser.fromJson(jsonStr);
|
||||
switch (loadEntryFile(id))
|
||||
{
|
||||
case {fileName: fileName, contents: contents}:
|
||||
parser.fromJson(contents, fileName);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
{
|
||||
trace('[${registryId}] Failed to parse entry data: ${id}');
|
||||
for (error in parser.errors)
|
||||
{
|
||||
trace(error);
|
||||
}
|
||||
printErrors(parser.errors, id);
|
||||
return null;
|
||||
}
|
||||
|
||||
var oldData:MyTypeData_v0_1_x = parser.value;
|
||||
return migrateData_v0_1_x(oldData);
|
||||
}
|
||||
|
||||
var result:MyTypeData =
|
||||
{
|
||||
version: DATA_VERSION,
|
||||
id: '${oldData.id}',
|
||||
name: oldData.name,
|
||||
data: []
|
||||
};
|
||||
|
||||
return result;
|
||||
function migrateData_v0_1_x(input:MyTypeData_v0_1_x):MyTypeData
|
||||
{
|
||||
return {
|
||||
version: DATA_VERSION,
|
||||
id: '${input.id}',
|
||||
name: input.name,
|
||||
data: []
|
||||
};
|
||||
}
|
||||
|
||||
public override function parseEntryDataWithMigration(id:String, version:thx.semver.Version):Null<MyTypeData>
|
||||
|
|
|
@ -123,9 +123,12 @@ class LevelRegistryTest extends FunkinTest
|
|||
}
|
||||
|
||||
@Test
|
||||
@Ignore("Requires redoing validation.")
|
||||
public function testCreateEntryBlankPath():Void
|
||||
{
|
||||
// Using @:jcustomparse, `titleAsset` has a validation function that ensures it is not blank.
|
||||
// This test makes sure that the validation function is being called, and that the error
|
||||
// results in the level failing to parse.
|
||||
|
||||
FunkinAssert.validateThrows(function() {
|
||||
var result:Null<Level> = LevelRegistry.instance.createEntry("blankpathtest");
|
||||
}, function(err) {
|
||||
|
@ -134,7 +137,6 @@ class LevelRegistryTest extends FunkinTest
|
|||
}
|
||||
|
||||
@Test
|
||||
@Ignore("Requires redoing validation.")
|
||||
public function testFetchBadEntry():Void
|
||||
{
|
||||
var result:Null<Level> = LevelRegistry.instance.fetchEntry("blablabla");
|
||||
|
|
|
@ -86,7 +86,7 @@ class NoteStyleRegistryTest extends FunkinTest
|
|||
@Test
|
||||
public function testFetchDefault():Void
|
||||
{
|
||||
var nsrMock:NoteStyleRegistry = mock(NoteStyleRegistry);
|
||||
var nsrMock = Mockatoo.mock(NoteStyleRegistry);
|
||||
|
||||
nsrMock.fetchDefault().callsRealMethod();
|
||||
|
||||
|
|
133
tests/unit/source/funkin/data/song/SongRegistryTest.hx
Normal file
133
tests/unit/source/funkin/data/song/SongRegistryTest.hx
Normal file
|
@ -0,0 +1,133 @@
|
|||
package funkin.data.song;
|
||||
|
||||
import funkin.data.song.SongData;
|
||||
import funkin.data.song.SongRegistry;
|
||||
import funkin.play.song.Song;
|
||||
import massive.munit.Assert;
|
||||
import massive.munit.async.AsyncFactory;
|
||||
import massive.munit.util.Timer;
|
||||
|
||||
@:nullSafety
|
||||
@:access(funkin.play.song.Song)
|
||||
@:access(funkin.data.song.SongRegistry)
|
||||
class SongRegistryTest extends FunkinTest
|
||||
{
|
||||
public function new()
|
||||
{
|
||||
super();
|
||||
}
|
||||
|
||||
@BeforeClass
|
||||
public function beforeClass():Void
|
||||
{
|
||||
FunkinAssert.initAssertTrace();
|
||||
SongRegistry.instance.loadEntries();
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public function afterClass():Void {}
|
||||
|
||||
@Before
|
||||
public function setup():Void {}
|
||||
|
||||
@After
|
||||
public function tearDown():Void {}
|
||||
|
||||
@Test
|
||||
public function testValid():Void
|
||||
{
|
||||
Assert.isNotNull(SongRegistry.instance);
|
||||
}
|
||||
|
||||
@Test
|
||||
public function testParseMetadata():Void
|
||||
{
|
||||
var result:Null<SongData.SongMetadata> = SongRegistry.instance.parseEntryMetadata("bopeebo");
|
||||
|
||||
Assert.isNotNull(result);
|
||||
|
||||
var expectedVersion:thx.semver.Version = "2.0.0";
|
||||
Assert.areEqual(expectedVersion, result.version);
|
||||
Assert.areEqual("Bopeebo", result.songName);
|
||||
Assert.areEqual("Kawai Sprite", result.artist);
|
||||
Assert.areEqual(SongData.SongTimeFormat.MILLISECONDS, result.timeFormat);
|
||||
Assert.areEqual("MasterEric (by hand)", result.generatedBy);
|
||||
}
|
||||
|
||||
@Test
|
||||
public function testParseChartData():Void
|
||||
{
|
||||
var result:Null<SongData.SongChartData> = SongRegistry.instance.parseEntryChartData("bopeebo");
|
||||
|
||||
Assert.isNotNull(result);
|
||||
|
||||
var expectedVersion:thx.semver.Version = "2.0.0";
|
||||
Assert.areEqual(expectedVersion, result.version);
|
||||
}
|
||||
|
||||
/**
|
||||
* A test validating an error is thrown when attempting to parse chart data as metadata.
|
||||
*/
|
||||
@Test
|
||||
public function testParseMetadataSwapped():Void
|
||||
{
|
||||
// Arrange
|
||||
FunkinAssert.clearTraces();
|
||||
|
||||
// Act
|
||||
var result:Null<SongData.SongMetadata> = SongRegistry.instance.parseEntryMetadata("bopeebo-swapped");
|
||||
|
||||
// Assert
|
||||
Assert.isNull(result);
|
||||
FunkinAssert.assertTrace("[SONG] Failed to parse entry data: bopeebo-swapped");
|
||||
FunkinAssert.assertTrace(" Unknown variable \"scrollSpeed\"");
|
||||
FunkinAssert.assertTrace(" at assets/data/songs/bopeebo-swapped/bopeebo-swapped-metadata.json:3");
|
||||
FunkinAssert.assertTrace(" Unknown variable \"events\"");
|
||||
FunkinAssert.assertTrace(" at assets/data/songs/bopeebo-swapped/bopeebo-swapped-metadata.json:7");
|
||||
FunkinAssert.assertTrace(" Unknown variable \"notes\"");
|
||||
FunkinAssert.assertTrace(" at assets/data/songs/bopeebo-swapped/bopeebo-swapped-metadata.json:185");
|
||||
FunkinAssert.assertTrace(" Uninitialized variable \"songName\"");
|
||||
FunkinAssert.assertTrace(" at assets/data/songs/bopeebo-swapped/bopeebo-swapped-metadata.json:1738");
|
||||
FunkinAssert.assertTrace(" Uninitialized variable \"timeFormat\"");
|
||||
FunkinAssert.assertTrace(" at assets/data/songs/bopeebo-swapped/bopeebo-swapped-metadata.json:1738");
|
||||
FunkinAssert.assertTrace(" Uninitialized variable \"timeChanges\"");
|
||||
FunkinAssert.assertTrace(" at assets/data/songs/bopeebo-swapped/bopeebo-swapped-metadata.json:1738");
|
||||
FunkinAssert.assertTrace(" Uninitialized variable \"playData\"");
|
||||
FunkinAssert.assertTrace(" at assets/data/songs/bopeebo-swapped/bopeebo-swapped-metadata.json:1738");
|
||||
FunkinAssert.assertTrace(" Uninitialized variable \"artist\"");
|
||||
FunkinAssert.assertTrace(" at assets/data/songs/bopeebo-swapped/bopeebo-swapped-metadata.json:1738");
|
||||
}
|
||||
|
||||
/**
|
||||
* A test validating an error is thrown when attempting to parse metadata as chart data.
|
||||
*/
|
||||
@Test
|
||||
public function testParseChartDataSwapped():Void
|
||||
{
|
||||
// Arrange
|
||||
FunkinAssert.clearTraces();
|
||||
|
||||
// Act
|
||||
var result:Null<SongData.SongChartData> = SongRegistry.instance.parseEntryChartData("bopeebo-swapped");
|
||||
|
||||
// Assert
|
||||
Assert.isNull(result);
|
||||
FunkinAssert.assertTrace("[SONG] Failed to parse entry data: bopeebo-swapped");
|
||||
FunkinAssert.assertTrace(" Unknown variable \"songName\"");
|
||||
FunkinAssert.assertTrace(" at assets/data/songs/bopeebo-swapped/bopeebo-swapped-chart.json:3");
|
||||
FunkinAssert.assertTrace(" Unknown variable \"artist\"");
|
||||
FunkinAssert.assertTrace(" at assets/data/songs/bopeebo-swapped/bopeebo-swapped-chart.json:4");
|
||||
FunkinAssert.assertTrace(" Unknown variable \"timeFormat\"");
|
||||
FunkinAssert.assertTrace(" at assets/data/songs/bopeebo-swapped/bopeebo-swapped-chart.json:5");
|
||||
FunkinAssert.assertTrace(" Unknown variable \"timeChanges\"");
|
||||
FunkinAssert.assertTrace(" at assets/data/songs/bopeebo-swapped/bopeebo-swapped-chart.json:6");
|
||||
FunkinAssert.assertTrace(" Unknown variable \"playData\"");
|
||||
FunkinAssert.assertTrace(" at assets/data/songs/bopeebo-swapped/bopeebo-swapped-chart.json:7");
|
||||
FunkinAssert.assertTrace(" Uninitialized variable \"notes\"");
|
||||
FunkinAssert.assertTrace(" at assets/data/songs/bopeebo-swapped/bopeebo-swapped-chart.json:15");
|
||||
FunkinAssert.assertTrace(" Uninitialized variable \"events\"");
|
||||
FunkinAssert.assertTrace(" at assets/data/songs/bopeebo-swapped/bopeebo-swapped-chart.json:15");
|
||||
FunkinAssert.assertTrace(" Uninitialized variable \"scrollSpeed\"");
|
||||
FunkinAssert.assertTrace(" at assets/data/songs/bopeebo-swapped/bopeebo-swapped-chart.json:15");
|
||||
}
|
||||
}
|
|
@ -1,11 +1,18 @@
|
|||
package funkin.play.notes.notestyle;
|
||||
|
||||
import flixel.util.FlxSort;
|
||||
import funkin.util.SortUtil;
|
||||
import flixel.graphics.FlxGraphic;
|
||||
import flixel.graphics.frames.FlxFrame;
|
||||
import flixel.graphics.frames.FlxFramesCollection;
|
||||
import massive.munit.util.Timer;
|
||||
import massive.munit.Assert;
|
||||
import massive.munit.async.AsyncFactory;
|
||||
import funkin.data.notestyle.NoteStyleRegistry;
|
||||
import funkin.play.notes.notestyle.NoteStyle;
|
||||
import flixel.animation.FlxAnimationController;
|
||||
import openfl.utils.Assets;
|
||||
import flixel.math.FlxPoint;
|
||||
|
||||
@:access(funkin.play.notes.notestyle.NoteStyle)
|
||||
class NoteStyleTest extends FunkinTest
|
||||
|
@ -31,20 +38,142 @@ class NoteStyleTest extends FunkinTest
|
|||
public function tearDown() {}
|
||||
|
||||
@Test
|
||||
@Ignore("This test doesn't work, crashes when the project has 2 mocks of the same class???")
|
||||
public function testBuildNoteSprite()
|
||||
{
|
||||
var target:Null<NoteStyle> = NoteStyleRegistry.instance.fetchEntry("funkin");
|
||||
|
||||
Assert.isNotNull(target);
|
||||
|
||||
var mockNoteSprite:NoteSprite = mock(NoteSprite);
|
||||
// var mockAnim = mock(FlxAnimationController);
|
||||
// mockNoteSprite.animation = mockAnim;
|
||||
// Arrange
|
||||
var mockNoteSprite = Mockatoo.mock(NoteSprite);
|
||||
var mockAnim = Mockatoo.mock(FlxAnimationController);
|
||||
var scale = new FlxPoint(1, 1); // handle sprite.scale.x on the mock
|
||||
|
||||
mockNoteSprite.animation = mockAnim; // Tell the mock to forward calls to the animation controller mock.
|
||||
mockNoteSprite.scale.returns(scale); // Redirect this final variable to a local variable.
|
||||
mockNoteSprite.antialiasing.callsRealMethod(); // Tell the mock to treat this like a normal property.
|
||||
mockNoteSprite.frames.callsRealMethod(); // Tell the mock to treat this like a normal property.
|
||||
|
||||
// Act
|
||||
target.buildNoteSprite(mockNoteSprite);
|
||||
|
||||
Assert.areEqual(mockNoteSprite.frames, []);
|
||||
var expectedGraphic:FlxGraphic = FlxG.bitmap.add("shared:assets/shared/images/arrows.png");
|
||||
|
||||
// Assert
|
||||
Assert.isNotNull(mockNoteSprite.frames);
|
||||
mockNoteSprite.frames.frames.sort(SortUtil.byFrameName);
|
||||
var frameCount:Int = mockNoteSprite.frames.frames.length;
|
||||
Assert.areEqual(24, frameCount);
|
||||
|
||||
// Validate each frame.
|
||||
for (i in 0...frameCount)
|
||||
{
|
||||
var currentFrame:FlxFrame = mockNoteSprite.frames.frames[i];
|
||||
switch (i)
|
||||
{
|
||||
case 0:
|
||||
Assert.areEqual("confirmDown0001", currentFrame.name);
|
||||
Assert.areEqual(expectedGraphic, currentFrame.parent);
|
||||
case 1:
|
||||
Assert.areEqual("confirmDown0002", currentFrame.name);
|
||||
Assert.areEqual(expectedGraphic, currentFrame.parent);
|
||||
case 2:
|
||||
Assert.areEqual("confirmLeft0001", currentFrame.name);
|
||||
Assert.areEqual(expectedGraphic, currentFrame.parent);
|
||||
case 3:
|
||||
Assert.areEqual("confirmLeft0002", currentFrame.name);
|
||||
Assert.areEqual(expectedGraphic, currentFrame.parent);
|
||||
case 4:
|
||||
Assert.areEqual("confirmRight0001", currentFrame.name);
|
||||
Assert.areEqual(expectedGraphic, currentFrame.parent);
|
||||
case 5:
|
||||
Assert.areEqual("confirmRight0002", currentFrame.name);
|
||||
Assert.areEqual(expectedGraphic, currentFrame.parent);
|
||||
case 6:
|
||||
Assert.areEqual("confirmUp0001", currentFrame.name);
|
||||
Assert.areEqual(expectedGraphic, currentFrame.parent);
|
||||
case 7:
|
||||
Assert.areEqual("confirmUp0002", currentFrame.name);
|
||||
Assert.areEqual(expectedGraphic, currentFrame.parent);
|
||||
case 8:
|
||||
Assert.areEqual("noteDown0001", currentFrame.name);
|
||||
Assert.areEqual(expectedGraphic, currentFrame.parent);
|
||||
case 9:
|
||||
Assert.areEqual("noteLeft0001", currentFrame.name);
|
||||
Assert.areEqual(expectedGraphic, currentFrame.parent);
|
||||
case 10:
|
||||
Assert.areEqual("noteRight0001", currentFrame.name);
|
||||
Assert.areEqual(expectedGraphic, currentFrame.parent);
|
||||
case 11:
|
||||
Assert.areEqual("noteUp0001", currentFrame.name);
|
||||
Assert.areEqual(expectedGraphic, currentFrame.parent);
|
||||
case 12:
|
||||
Assert.areEqual("pressedDown0001", currentFrame.name);
|
||||
Assert.areEqual(expectedGraphic, currentFrame.parent);
|
||||
case 13:
|
||||
Assert.areEqual("pressedDown0002", currentFrame.name);
|
||||
Assert.areEqual(expectedGraphic, currentFrame.parent);
|
||||
case 14:
|
||||
Assert.areEqual("pressedLeft0001", currentFrame.name);
|
||||
Assert.areEqual(expectedGraphic, currentFrame.parent);
|
||||
case 15:
|
||||
Assert.areEqual("pressedLeft0002", currentFrame.name);
|
||||
Assert.areEqual(expectedGraphic, currentFrame.parent);
|
||||
case 16:
|
||||
Assert.areEqual("pressedRight0001", currentFrame.name);
|
||||
Assert.areEqual(expectedGraphic, currentFrame.parent);
|
||||
case 17:
|
||||
Assert.areEqual("pressedRight0002", currentFrame.name);
|
||||
Assert.areEqual(expectedGraphic, currentFrame.parent);
|
||||
case 18:
|
||||
Assert.areEqual("pressedUp0001", currentFrame.name);
|
||||
Assert.areEqual(expectedGraphic, currentFrame.parent);
|
||||
case 19:
|
||||
Assert.areEqual("pressedUp0002", currentFrame.name);
|
||||
Assert.areEqual(expectedGraphic, currentFrame.parent);
|
||||
case 20:
|
||||
Assert.areEqual("staticDown0001", currentFrame.name);
|
||||
Assert.areEqual(expectedGraphic, currentFrame.parent);
|
||||
case 21:
|
||||
Assert.areEqual("staticLeft0001", currentFrame.name);
|
||||
Assert.areEqual(expectedGraphic, currentFrame.parent);
|
||||
case 22:
|
||||
Assert.areEqual("staticRight0001", currentFrame.name);
|
||||
Assert.areEqual(expectedGraphic, currentFrame.parent);
|
||||
case 23:
|
||||
Assert.areEqual("staticUp0001", currentFrame.name);
|
||||
Assert.areEqual(expectedGraphic, currentFrame.parent);
|
||||
default:
|
||||
Assert.fail('Got unexpected frame number ${i}');
|
||||
}
|
||||
}
|
||||
|
||||
// Verify animations were applied.
|
||||
@:privateAccess {
|
||||
mockAnim.addByPrefix('purpleScroll', 'noteLeft', 24, false, false, false).verify(times(1));
|
||||
mockAnim.addByPrefix('blueScroll', 'noteDown', 24, false, false, false).verify(times(1));
|
||||
mockAnim.addByPrefix('greenScroll', 'noteUp', 24, false, false, false).verify(times(1));
|
||||
mockAnim.addByPrefix('redScroll', 'noteRight', 24, false, false, false).verify(times(1));
|
||||
mockAnim.destroyAnimations().verify(times(1));
|
||||
mockAnim.set_frameIndex(0).verify(times(1));
|
||||
// Verify there were no other functions called.
|
||||
mockAnim.verifyZeroInteractions();
|
||||
}
|
||||
|
||||
// Verify sprite was initialized.
|
||||
@:privateAccess {
|
||||
mockNoteSprite.set_graphic(expectedGraphic).verify(times(1));
|
||||
mockNoteSprite.graphicLoaded().verify(times(1));
|
||||
mockNoteSprite.set_antialiasing(true).verify(times(1));
|
||||
mockNoteSprite.set_frames(mockNoteSprite.frames).verify(times(1));
|
||||
mockNoteSprite.set_frame(mockNoteSprite.frames.frames[21]).verify(times(1));
|
||||
mockNoteSprite.resetHelpers().verify(times(1));
|
||||
|
||||
Assert.areEqual(1, mockNoteSprite.scale.x);
|
||||
Assert.areEqual(1, mockNoteSprite.scale.y);
|
||||
// Verify there were no other functions called.
|
||||
mockNoteSprite.verifyZeroInteractions();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -3,7 +3,7 @@ package funkin.util;
|
|||
import flixel.FlxObject;
|
||||
import flixel.FlxSprite;
|
||||
import flixel.util.FlxSort;
|
||||
import funkin.play.song.SongData.SongNoteData;
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
import massive.munit.util.Timer;
|
||||
import massive.munit.Assert;
|
||||
import massive.munit.async.AsyncFactory;
|
||||
|
|
|
@ -34,8 +34,8 @@ class FlxAnimationUtilTest extends FunkinTest
|
|||
public function testAddAtlasAnimation()
|
||||
{
|
||||
// Build a mock child class of FlxSprite
|
||||
var mockSprite = mock(FlxSprite);
|
||||
var mockAnim = mock(FlxAnimationController);
|
||||
var mockSprite = Mockatoo.mock(FlxSprite);
|
||||
var mockAnim = Mockatoo.mock(FlxAnimationController);
|
||||
mockSprite.animation = mockAnim;
|
||||
|
||||
var animData:AnimationData =
|
||||
|
@ -85,8 +85,8 @@ class FlxAnimationUtilTest extends FunkinTest
|
|||
public function testAddAtlasAnimations()
|
||||
{
|
||||
// Build a mock child class of FlxSprite
|
||||
var mockSprite = mock(FlxSprite);
|
||||
var mockAnim = mock(FlxAnimationController);
|
||||
var mockSprite = Mockatoo.mock(FlxSprite);
|
||||
var mockAnim = Mockatoo.mock(FlxAnimationController);
|
||||
mockSprite.animation = mockAnim;
|
||||
|
||||
var animData:Array<AnimationData> = [
|
||||
|
|
Loading…
Reference in a new issue