diff --git a/source/funkin/data/freeplay/player/PlayerRegistry.hx b/source/funkin/data/freeplay/player/PlayerRegistry.hx index 76b1c25c1..c14849bc4 100644 --- a/source/funkin/data/freeplay/player/PlayerRegistry.hx +++ b/source/funkin/data/freeplay/player/PlayerRegistry.hx @@ -5,6 +5,7 @@ import funkin.ui.freeplay.charselect.PlayableCharacter; import funkin.ui.freeplay.charselect.ScriptedPlayableCharacter; import funkin.save.Save; +@:build(funkin.util.macro.RegistryMacro.build()) class PlayerRegistry extends BaseRegistry { /** @@ -16,15 +17,6 @@ class PlayerRegistry extends BaseRegistry public static final PLAYER_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x"; - public static var instance(get, never):PlayerRegistry; - static var _instance:Null = null; - - static function get_instance():PlayerRegistry - { - if (_instance == null) _instance = new PlayerRegistry(); - return _instance; - } - /** * A mapping between stage character IDs and Freeplay playable character IDs. */ @@ -131,63 +123,6 @@ class PlayerRegistry extends BaseRegistry return ownedCharacterIds.exists(characterId); } - /** - * Read, parse, and validate the JSON data and produce the corresponding data object. - */ - public function parseEntryData(id:String):Null - { - // JsonParser does not take type parameters, - // otherwise this function would be in BaseRegistry. - var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - - switch (loadEntryFile(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; - } - - /** - * Parse and validate the JSON data and produce the corresponding data object. - * - * NOTE: Must be implemented on the implementation class. - * @param contents The JSON as a string. - * @param fileName An optional file name for error reporting. - */ - public function parseEntryDataRaw(contents:String, ?fileName:String):Null - { - var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - parser.fromJson(contents, fileName); - - if (parser.errors.length > 0) - { - printErrors(parser.errors, fileName); - return null; - } - return parser.value; - } - - function createScriptedEntry(clsName:String):PlayableCharacter - { - return ScriptedPlayableCharacter.init(clsName, "unknown"); - } - - function getScriptedClassNames():Array - { - return ScriptedPlayableCharacter.listScriptClasses(); - } - /** * A list of all the playable characters from the base game, in order. */ diff --git a/source/funkin/data/notestyle/NoteStyleRegistry.hx b/source/funkin/data/notestyle/NoteStyleRegistry.hx index 36d1b9200..43dbd41f7 100644 --- a/source/funkin/data/notestyle/NoteStyleRegistry.hx +++ b/source/funkin/data/notestyle/NoteStyleRegistry.hx @@ -4,6 +4,7 @@ import funkin.play.notes.notestyle.NoteStyle; import funkin.play.notes.notestyle.ScriptedNoteStyle; import funkin.data.notestyle.NoteStyleData; +@:build(funkin.util.macro.RegistryMacro.build()) class NoteStyleRegistry extends BaseRegistry { /** @@ -15,15 +16,6 @@ class NoteStyleRegistry extends BaseRegistry public static final NOTE_STYLE_DATA_VERSION_RULE:thx.semver.VersionRule = "1.1.x"; - public static var instance(get, never):NoteStyleRegistry; - static var _instance:Null = null; - - static function get_instance():NoteStyleRegistry - { - if (_instance == null) _instance = new NoteStyleRegistry(); - return _instance; - } - public function new() { super('NOTESTYLE', 'notestyles', NOTE_STYLE_DATA_VERSION_RULE); @@ -33,61 +25,4 @@ class NoteStyleRegistry extends BaseRegistry { return fetchEntry(Constants.DEFAULT_NOTE_STYLE); } - - /** - * Read, parse, and validate the JSON data and produce the corresponding data object. - */ - public function parseEntryData(id:String):Null - { - // JsonParser does not take type parameters, - // otherwise this function would be in BaseRegistry. - var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - - switch (loadEntryFile(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; - } - - /** - * Parse and validate the JSON data and produce the corresponding data object. - * - * NOTE: Must be implemented on the implementation class. - * @param contents The JSON as a string. - * @param fileName An optional file name for error reporting. - */ - public function parseEntryDataRaw(contents:String, ?fileName:String):Null - { - var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - parser.fromJson(contents, fileName); - - if (parser.errors.length > 0) - { - printErrors(parser.errors, fileName); - return null; - } - return parser.value; - } - - function createScriptedEntry(clsName:String):NoteStyle - { - return ScriptedNoteStyle.init(clsName, "unknown"); - } - - function getScriptedClassNames():Array - { - return ScriptedNoteStyle.listScriptClasses(); - } } diff --git a/source/funkin/data/song/SongRegistry.hx b/source/funkin/data/song/SongRegistry.hx index e7cab246c..3e9f88453 100644 --- a/source/funkin/data/song/SongRegistry.hx +++ b/source/funkin/data/song/SongRegistry.hx @@ -13,6 +13,7 @@ import funkin.util.VersionUtil; using funkin.data.song.migrator.SongDataMigrator; @:nullSafety +@:build(funkin.util.macro.RegistryMacro.build()) class SongRegistry extends BaseRegistry { /** @@ -39,19 +40,6 @@ class SongRegistry extends BaseRegistry return '${Constants.TITLE} - ${Constants.VERSION}'; } - /** - * TODO: What if there was a Singleton macro which automatically created the property for us? - */ - public static var instance(get, never):SongRegistry; - - static var _instance:Null = null; - - static function get_instance():SongRegistry - { - if (_instance == null) _instance = new SongRegistry(); - return _instance; - } - public function new() { super('SONG', 'songs', SONG_METADATA_VERSION_RULE); @@ -69,7 +57,7 @@ class SongRegistry extends BaseRegistry for (entryCls in scriptedEntryClassNames) { - var entry:Song = createScriptedEntry(entryCls); + var entry:Null = createScriptedEntry(entryCls); if (entry != null) { @@ -417,16 +405,6 @@ class SongRegistry extends BaseRegistry } } - function createScriptedEntry(clsName:String):Song - { - return ScriptedSong.init(clsName, "unknown"); - } - - function getScriptedClassNames():Array - { - return ScriptedSong.listScriptClasses(); - } - function loadEntryMetadataFile(id:String, ?variation:String):Null { variation = variation == null ? Constants.DEFAULT_VARIATION : variation; diff --git a/source/funkin/play/notes/notestyle/NoteStyle.hx b/source/funkin/play/notes/notestyle/NoteStyle.hx index dd0885751..0bdc507e8 100644 --- a/source/funkin/play/notes/notestyle/NoteStyle.hx +++ b/source/funkin/play/notes/notestyle/NoteStyle.hx @@ -18,13 +18,9 @@ using funkin.data.animation.AnimationData.AnimationDataUtil; * and provides convenience methods for building sprites based on them. */ @:nullSafety +@:build(funkin.util.macro.EntryMacro.build(funkin.data.notestyle.NoteStyleRegistry)) class NoteStyle implements IRegistryEntry { - /** - * The ID of the note style. - */ - public final id:String; - /** * Note style data as parsed from the JSON file. */ @@ -36,9 +32,10 @@ class NoteStyle implements IRegistryEntry */ var fallback(get, never):Null; - function get_fallback():Null { + function get_fallback():Null + { if (_data == null || _data.fallback == null) return null; - return NoteStyleRegistry.instance.fetchEntry(_data.fallback); + return registryInstance.fetchEntry(_data.fallback); } /** @@ -880,16 +877,9 @@ class NoteStyle implements IRegistryEntry } } - public function destroy():Void {} - - public function toString():String - { - return 'NoteStyle($id)'; - } - static function _fetchData(id:String):NoteStyleData { - var result = NoteStyleRegistry.instance.parseEntryDataWithMigration(id, NoteStyleRegistry.instance.fetchEntryVersion(id)); + var result = registryInstance.parseEntryDataWithMigration(id, registryInstance.fetchEntryVersion(id)); if (result == null) { diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index 20d2f75a4..40d4cb789 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -28,6 +28,7 @@ import funkin.util.SortUtil; * can be used to perform custom gameplay behaviors only on specific songs. */ @:nullSafety +@:build(funkin.util.macro.EntryMacro.build(funkin.data.song.SongRegistry)) class Song implements IPlayStateScriptedClass implements IRegistryEntry { /** @@ -65,19 +66,6 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry; - // key = variation id, value = metadata final _metadata:Map; @@ -624,13 +612,6 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry { - /** - * The ID of the playable character. - */ - public final id:String; - - /** - * Playable character data as parsed from the JSON file. - */ - public final _data:Null; - /** * @param id The ID of the JSON file to parse. */ @@ -156,25 +147,4 @@ class PlayableCharacter implements IRegistryEntry { return _data?.unlocked ?? true; } - - /** - * Called when the character is destroyed. - * TODO: Document when this gets called - */ - public function destroy():Void {} - - public function toString():String - { - return 'PlayableCharacter($id)'; - } - - /** - * Retrieve and parse the JSON data for a playable character by ID. - * @param id The ID of the character - * @return The parsed player data, or null if not found or invalid - */ - static function _fetchData(id:String):Null - { - return PlayerRegistry.instance.parseEntryDataWithMigration(id, PlayerRegistry.instance.fetchEntryVersion(id)); - } } diff --git a/source/funkin/util/macro/EntryMacro.hx b/source/funkin/util/macro/EntryMacro.hx new file mode 100644 index 000000000..3e4937292 --- /dev/null +++ b/source/funkin/util/macro/EntryMacro.hx @@ -0,0 +1,238 @@ +package funkin.util.macro; + +import haxe.macro.Context; +import haxe.macro.Expr; +import haxe.macro.Type; + +using StringTools; + +class EntryMacro +{ + public static macro function build(registryExpr:ExprOf>):Array + { + var fields = Context.getBuildFields(); + + var cls = Context.getLocalClass().get(); + + var entryData = getEntryData(cls); + + buildIdField(fields); + + buildDataField(entryData, fields); + + buildRegistryInstanceField(registryExpr, fields); + + buildFetchDataField(entryData, fields); + + buildToStringField(cls, fields); + + buildDestroyField(cls, fields); + + return fields; + } + + #if macro + static function shouldBuildField(name:String, fields:Array):Bool // fields can be Array or Array + { + for (field in fields) + { + if (field.name == name) + { + return false; + } + } + return true; + } + + static function getEntryData(cls:ClassType):Dynamic // DefType or ClassType + { + switch (cls.interfaces[0].params[0]) + { + case Type.TInst(t, _): + return t.get(); + case Type.TType(t, _): + return t.get(); + default: + throw 'Entry Data is not a class or typedef'; + } + } + + static function buildIdField(fields:Array):Void + { + if (!shouldBuildField('id', fields)) + { + return; + } + + fields.push( + { + name: 'id', + access: [Access.APublic, Access.AFinal], + kind: FieldType.FVar((macro :String)), + pos: Context.currentPos() + }); + } + + static function buildDataField(entryData:Dynamic, fields:Array):Void + { + if (!shouldBuildField('_data', fields)) + { + return; + } + + fields.push( + { + name: '_data', + access: [Access.APublic, Access.AFinal], + kind: FieldType.FVar(ComplexType.TPath( + { + pack: [], + name: 'Null', + params: [ + TypeParam.TPType(ComplexType.TPath( + { + pack: entryData.pack, + name: entryData.name + })) + ] + })), + pos: Context.currentPos() + }); + } + + static function buildRegistryInstanceField(registryExpr:ExprOf>, fields:Array):Void + { + if (!shouldBuildField('registryInstance', fields)) + { + return; + } + + var registryCls = MacroUtil.getClassTypeFromExpr(registryExpr); + + fields.push( + { + name: 'registryInstance', + access: [Access.APrivate, Access.AStatic], + kind: FieldType.FProp("get", "never", ComplexType.TPath( + { + pack: registryCls.pack, + name: registryCls.name, + params: [] + })), + pos: Context.currentPos() + }); + + fields.push( + { + name: 'get_registryInstance', + access: [Access.APrivate, Access.AStatic], + kind: FFun( + { + args: [], + expr: macro + { + return ${registryExpr}.instance; + }, + params: [], + ret: ComplexType.TPath( + { + pack: registryCls.pack, + name: registryCls.name, + params: [] + }) + }), + pos: Context.currentPos() + }); + } + + static function buildFetchDataField(entryData:Dynamic, fields:Array):Void + { + if (!shouldBuildField('_fetchData', fields)) + { + return; + } + + fields.push( + { + name: '_fetchData', + access: [Access.AStatic, Access.APrivate], + kind: FieldType.FFun( + { + args: [ + { + name: 'id', + type: (macro :String) + } + ], + expr: macro + { + return registryInstance.parseEntryDataWithMigration(id, registryInstance.fetchEntryVersion(id)); + }, + params: [], + ret: ComplexType.TPath( + { + pack: [], + name: 'Null', + params: [ + TypeParam.TPType(ComplexType.TPath( + { + pack: entryData.pack, + name: entryData.name + })) + ] + }) + }), + pos: Context.currentPos() + }); + } + + static function buildToStringField(cls:ClassType, fields:Array):Void + { + if (!shouldBuildField('toString', fields)) + { + return; + } + + fields.push( + { + name: 'toString', + access: [Access.APublic], + kind: FieldType.FFun( + { + args: [], + expr: macro + { + return $v{cls.name} + '(' + id + ')'; + }, + params: [], + ret: (macro :String) + }), + pos: Context.currentPos() + }); + } + + static function buildDestroyField(cls:ClassType, fields:Array):Void + { + if (!shouldBuildField('destroy', fields) || !shouldBuildField('destroy', cls.superClass?.t.get().fields.get() ?? [])) + { + return; + } + + fields.push( + { + name: 'destroy', + access: [Access.APublic], + kind: FieldType.FFun( + { + args: [], + expr: macro + { + return; + }, + params: [] + }), + pos: Context.currentPos() + }); + } + #end +} diff --git a/source/funkin/util/macro/RegistryMacro.hx b/source/funkin/util/macro/RegistryMacro.hx new file mode 100644 index 000000000..5aba301bc --- /dev/null +++ b/source/funkin/util/macro/RegistryMacro.hx @@ -0,0 +1,351 @@ +package funkin.util.macro; + +import haxe.macro.Context; +import haxe.macro.Expr; +import haxe.macro.Type; + +using StringTools; + +class RegistryMacro +{ + public static macro function build():Array + { + var fields = Context.getBuildFields(); + + var cls = Context.getLocalClass().get(); + + var typeParams = getTypeParams(cls); + var entryCls = typeParams.entryCls; + var jsonCls = typeParams.jsonCls; + var scriptedEntryCls = getScriptedEntryClass(entryCls); + + buildInstanceField(cls, fields); + + buildGetScriptedClassNamesField(scriptedEntryCls, fields); + + buildCreateScriptedEntryField(entryCls, scriptedEntryCls, fields); + + buildParseEntryDataField(jsonCls, fields); + + buildParseEntryDataRawField(jsonCls, fields); + + return fields; + } + + #if macro + static function shouldBuildField(name:String, fields:Array):Bool // fields can be Array or Array + { + for (field in fields) + { + if (field.name == name) + { + return false; + } + } + return true; + } + + static function getTypeParams(cls:ClassType):RegistryTypeParams + { + switch (cls.superClass.t.get().kind) + { + case KGenericInstance(_, params): + var typeParams:Array = []; + for (param in params) + { + switch (param) + { + case TInst(t, _): + typeParams.push(t.get()); + case TType(t, _): + typeParams.push(t.get()); + default: + throw 'Not a class'; + } + } + return {entryCls: typeParams[0], jsonCls: typeParams[1]}; + default: + throw 'Not in the correct format'; + } + } + + static function getScriptedEntryClass(entryCls:ClassType):ClassType + { + var scriptedEntryClsName = entryCls.pack.join('.') + '.Scripted' + entryCls.name; + switch (Context.getType(scriptedEntryClsName)) + { + case Type.TInst(t, _): + return t.get(); + default: + throw 'Not A Class (${scriptedEntryClsName})'; + }; + } + + static function buildInstanceField(cls:ClassType, fields:Array):Void + { + if (!shouldBuildField('instance', fields)) + { + return; + } + + fields.push( + { + name: '_instance', + access: [Access.APrivate, Access.AStatic], + kind: FieldType.FVar(ComplexType.TPath( + { + pack: [], + name: 'Null', + params: [ + TypeParam.TPType(ComplexType.TPath( + { + pack: cls.pack, + name: cls.name, + params: [] + })) + ] + })), + pos: Context.currentPos() + }); + + fields.push( + { + name: 'instance', + access: [Access.APublic, Access.AStatic], + kind: FieldType.FProp("get", "never", ComplexType.TPath( + { + pack: cls.pack, + name: cls.name, + params: [] + })), + pos: Context.currentPos() + }); + + var newStrExpr = 'new ${cls.pack.join('.')}.${cls.name}()'; + var newExpr = Context.parse(newStrExpr, Context.currentPos()); + + fields.push( + { + name: 'get_instance', + access: [Access.APrivate, Access.AStatic], + kind: FFun( + { + args: [], + expr: macro + { + if (_instance == null) + { + _instance = ${newExpr}; + } + return _instance; + }, + params: [], + ret: ComplexType.TPath( + { + pack: cls.pack, + name: cls.name, + params: [] + }) + }), + pos: Context.currentPos() + }); + } + + static function buildGetScriptedClassNamesField(scriptedEntryCls:ClassType, fields:Array):Void + { + if (!shouldBuildField('getScriptedClassNames', fields)) + { + return; + } + + var scriptedEntryExpr = Context.parse('${scriptedEntryCls.pack.join('.')}.${scriptedEntryCls.name}', Context.currentPos()); + + fields.push( + { + name: 'getScriptedClassNames', + access: [Access.APrivate], + kind: FieldType.FFun( + { + args: [], + expr: macro + { + return ${scriptedEntryExpr}.listScriptClasses(); + }, + params: [], + ret: (macro :Array) + }), + pos: Context.currentPos() + }); + } + + static function buildCreateScriptedEntryField(entryCls:ClassType, scriptedEntryCls:ClassType, fields:Array):Void + { + if (!shouldBuildField('createScriptedEntry', fields)) + { + return; + } + + var scriptedStrExpr = '${scriptedEntryCls.pack.join('.')}.${scriptedEntryCls.name}.init(clsName, \'unknown\')'; + var scriptedInitExpr = Context.parse(scriptedStrExpr, Context.currentPos()); + + fields.push( + { + name: 'createScriptedEntry', + access: [Access.APrivate], + kind: FieldType.FFun( + { + args: [ + { + name: 'clsName', + type: (macro :String) + } + ], + expr: macro + { + return ${scriptedInitExpr}; + }, + params: [], + ret: ComplexType.TPath( + { + pack: [], + name: 'Null', + params: [ + TypeParam.TPType(ComplexType.TPath( + { + pack: entryCls.pack, + name: entryCls.name + })) + ] + }) + }), + pos: Context.currentPos() + }); + } + + static function buildParseEntryDataField(jsonCls:Dynamic, fields:Array):Void + { + if (!shouldBuildField('parseEntryData', fields)) + { + return; + } + + var jsonParserNewStrExpr = 'new json2object.JsonParser<${jsonCls.pack.join('.')}.${jsonCls.name}>()'; + var jsonParserNewExpr = Context.parse(jsonParserNewStrExpr, Context.currentPos()); + + fields.push( + { + name: 'parseEntryData', + access: [Access.APublic], + kind: FieldType.FFun( + { + args: [ + { + name: 'id', + type: (macro :String) + } + ], + expr: macro + { + var parser = ${jsonParserNewExpr}; + parser.ignoreUnknownVariables = false; + + switch (loadEntryFile(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; + }, + params: [], + ret: ComplexType.TPath( + { + pack: [], + name: 'Null', + params: [ + TypeParam.TPType(ComplexType.TPath( + { + pack: jsonCls.pack, + name: jsonCls.name + })) + ] + }) + }), + pos: Context.currentPos() + }); + } + + static function buildParseEntryDataRawField(jsonCls:Dynamic, fields:Array):Void + { + if (!shouldBuildField('parseEntryDataRaw', fields)) + { + return; + } + + var jsonParserNewStrExpr = 'new json2object.JsonParser<${jsonCls.pack.join('.')}.${jsonCls.name}>()'; + var jsonParserNewExpr = Context.parse(jsonParserNewStrExpr, Context.currentPos()); + + fields.push( + { + name: 'parseEntryDataRaw', + access: [Access.APublic], + kind: FieldType.FFun( + { + args: [ + { + name: 'contents', + type: (macro :String) + }, + { + name: 'fileName', + type: (macro :Null), + opt: true + } + ], + expr: macro + { + var parser = ${jsonParserNewExpr}; + parser.ignoreUnknownVariables = false; + parser.fromJson(contents, fileName); + + if (parser.errors.length > 0) + { + printErrors(parser.errors, fileName); + return null; + } + return parser.value; + }, + params: [], + ret: ComplexType.TPath( + { + pack: [], + name: 'Null', + params: [ + TypeParam.TPType(ComplexType.TPath( + { + pack: jsonCls.pack, + name: jsonCls.name + })) + ] + }) + }), + pos: Context.currentPos() + }); + } + #end +} + +#if macro +typedef RegistryTypeParams = +{ + var entryCls:ClassType; + var jsonCls:Dynamic; // DefType or ClassType +} +#end