This commit is contained in:
lemz 2024-11-08 23:45:23 +00:00 committed by GitHub
commit 5479b51577
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 401 additions and 305 deletions

View file

@ -3,6 +3,7 @@ package funkin.data.dialogue.conversation;
import funkin.play.cutscene.dialogue.Conversation;
import funkin.play.cutscene.dialogue.ScriptedConversation;
@:build(funkin.util.macro.RegistryMacro.buildRegistry())
class ConversationRegistry extends BaseRegistry<Conversation, ConversationData>
{
/**
@ -14,74 +15,8 @@ class ConversationRegistry extends BaseRegistry<Conversation, ConversationData>
public static final CONVERSATION_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x";
public static var instance(get, never):ConversationRegistry;
static var _instance:Null<ConversationRegistry> = null;
static function get_instance():ConversationRegistry
{
if (_instance == null) _instance = new ConversationRegistry();
return _instance;
}
public function new()
{
super('CONVERSATION', 'dialogue/conversations', CONVERSATION_DATA_VERSION_RULE);
}
/**
* Read, parse, and validate the JSON data and produce the corresponding data object.
*/
public function parseEntryData(id:String):Null<ConversationData>
{
// JsonParser does not take type parameters,
// otherwise this function would be in BaseRegistry.
var parser = new json2object.JsonParser<ConversationData>();
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<ConversationData>
{
var parser = new json2object.JsonParser<ConversationData>();
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):Conversation
{
return ScriptedConversation.init(clsName, "unknown");
}
function getScriptedClassNames():Array<String>
{
return ScriptedConversation.listScriptClasses();
}
}

View file

@ -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.buildRegistry())
class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData>
{
/**
@ -16,15 +17,6 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData>
public static final PLAYER_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x";
public static var instance(get, never):PlayerRegistry;
static var _instance:Null<PlayerRegistry> = 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<PlayableCharacter, PlayerData>
return ownedCharacterIds.exists(characterId);
}
/**
* Read, parse, and validate the JSON data and produce the corresponding data object.
*/
public function parseEntryData(id:String):Null<PlayerData>
{
// JsonParser does not take type parameters,
// otherwise this function would be in BaseRegistry.
var parser = new json2object.JsonParser<PlayerData>();
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<PlayerData>
{
var parser = new json2object.JsonParser<PlayerData>();
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<String>
{
return ScriptedPlayableCharacter.listScriptClasses();
}
/**
* A list of all the playable characters from the base game, in order.
*/

View file

@ -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.buildRegistry())
class NoteStyleRegistry extends BaseRegistry<NoteStyle, NoteStyleData>
{
/**
@ -15,15 +16,6 @@ class NoteStyleRegistry extends BaseRegistry<NoteStyle, NoteStyleData>
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<NoteStyleRegistry> = 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<NoteStyle, NoteStyleData>
{
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<NoteStyleData>
{
// JsonParser does not take type parameters,
// otherwise this function would be in BaseRegistry.
var parser = new json2object.JsonParser<NoteStyleData>();
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<NoteStyleData>
{
var parser = new json2object.JsonParser<NoteStyleData>();
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<String>
{
return ScriptedNoteStyle.listScriptClasses();
}
}

View file

@ -13,6 +13,7 @@ import funkin.util.VersionUtil;
using funkin.data.song.migrator.SongDataMigrator;
@:nullSafety
@:build(funkin.util.macro.RegistryMacro.buildRegistry())
class SongRegistry extends BaseRegistry<Song, SongMetadata>
{
/**
@ -39,19 +40,6 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
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<SongRegistry> = null;
static function get_instance():SongRegistry
{
if (_instance == null) _instance = new SongRegistry();
return _instance;
}
public function new()
{
super('SONG', 'songs', SONG_METADATA_VERSION_RULE);
@ -417,16 +405,6 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
}
}
function createScriptedEntry(clsName:String):Song
{
return ScriptedSong.init(clsName, "unknown");
}
function getScriptedClassNames():Array<String>
{
return ScriptedSong.listScriptClasses();
}
function loadEntryMetadataFile(id:String, ?variation:String):Null<JsonFile>
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
@ -508,8 +486,32 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
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"
"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"
];
}

View file

@ -30,23 +30,14 @@ import funkin.util.EaseUtil;
*
* This shit is great for modders but it's pretty elaborate for how much it'll actually be used, lolol. -Eric
*/
@:build(funkin.util.macro.RegistryMacro.buildEntry())
class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass implements IRegistryEntry<ConversationData>
{
/**
* The ID of the conversation.
*/
public final id:String;
/**
* The current state of the conversation.
*/
var state:ConversationState = ConversationState.Start;
/**
* Conversation data as parsed from the JSON file.
*/
public final _data:ConversationData;
/**
* The current entry in the dialogue.
*/
@ -631,16 +622,6 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
outroTween = null;
}
}
public override function toString():String
{
return 'Conversation($id)';
}
static function _fetchData(id:String):Null<ConversationData>
{
return ConversationRegistry.instance.parseEntryDataWithMigration(id, ConversationRegistry.instance.fetchEntryVersion(id));
}
}
// Managing things with a single enum is a lot easier than a multitude of flags.

View file

@ -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.RegistryMacro.buildEntry())
class NoteStyle implements IRegistryEntry<NoteStyleData>
{
/**
* The ID of the note style.
*/
public final id:String;
/**
* Note style data as parsed from the JSON file.
*/
@ -36,7 +32,8 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
*/
var fallback(get, never):Null<NoteStyle>;
function get_fallback():Null<NoteStyle> {
function get_fallback():Null<NoteStyle>
{
if (_data == null || _data.fallback == null) return null;
return NoteStyleRegistry.instance.fetchEntry(_data.fallback);
}
@ -880,13 +877,6 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
}
}
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));

View file

@ -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.RegistryMacro.buildEntry())
class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMetadata>
{
/**
@ -65,19 +66,6 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
*/
public static final DEFAULT_SCROLLSPEED:Float = 1.0;
/**
* The internal ID of the song.
*/
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>;
// key = variation id, value = metadata
final _metadata:Map<String, SongMetadata>;
@ -624,13 +612,6 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
}
}
public function toString():String
{
return 'Song($id)';
}
public function destroy():Void {}
public function onPause(event:PauseScriptEvent):Void {};
public function onResume(event:ScriptEvent):Void {};

View file

@ -10,18 +10,11 @@ import funkin.play.scoring.Scoring.ScoringRank;
* Can be scripted to override each function, for custom behavior.
*/
@:nullSafety
@:build(funkin.util.macro.RegistryMacro.buildEntry())
class PlayableCharacter implements IRegistryEntry<PlayerData>
{
/**
* The ID of the playable character.
*/
public final id:String;
/**
* Playable character data as parsed from the JSON file.
*/
public final _data:Null<PlayerData>;
/**
* @param id The ID of the JSON file to parse.
*/
@ -156,25 +149,4 @@ class PlayableCharacter implements IRegistryEntry<PlayerData>
{
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<PlayerData>
{
return PlayerRegistry.instance.parseEntryDataWithMigration(id, PlayerRegistry.instance.fetchEntryVersion(id));
}
}

View file

@ -0,0 +1,365 @@
package funkin.util.macro;
import haxe.macro.Context;
import haxe.macro.Expr;
import haxe.macro.Type;
using haxe.macro.ExprTools;
using haxe.macro.TypeTools;
using StringTools;
class RegistryMacro
{
public static macro function buildRegistry():Array<Field>
{
var fields = Context.getBuildFields();
var cls = Context.getLocalClass().get();
if (!cls.name.endsWith('Registry'))
{
throw '${cls.module}.${cls.name} needs to end with "Registry"';
}
var typeParams = getTypeParams(cls);
var entryCls = typeParams.entryCls;
var jsonCls = typeParams.jsonCls;
var scriptedEntryCls = getScriptedEntryClass(entryCls);
fields = fields.concat(buildRegistryVariables(cls));
fields = fields.concat(buildRegistryMethods(cls));
buildEntryImpl(entryCls, cls);
buildRegistryImpl(cls, entryCls, scriptedEntryCls, jsonCls);
return fields;
}
public static macro function buildEntry():Array<Field>
{
var fields = Context.getBuildFields();
var cls = Context.getLocalClass().get();
var entryData = getEntryData(cls);
// since the registries also use a build macro
// the fields aren't callable unless we first get
// the class type of the registry
makeFieldsCallable(cls);
fields = fields.concat(buildEntryVariables(cls, entryData));
fields = fields.concat(buildEntryMethods(cls));
return fields;
}
#if macro
static function makeFieldsCallable(cls:ClassType)
{
// TODO: lets not have this if statement
// like what the hell is wrong with this
if (cls.name == 'Song')
{
MacroUtil.getClassTypeFromExpr(macro funkin.data.song.SongRegistry);
return;
}
var registries:Array<String> = [];
for (localImport in Context.getLocalImports())
{
var names = [];
for (path in localImport.path)
{
names.push(path.name);
}
var fullName = names.join('.');
if (fullName.endsWith('Registry'))
{
registries.push(fullName);
}
}
for (registry in registries)
{
MacroUtil.getClassTypeFromExpr(Context.parse(registry, Context.currentPos()));
}
}
static function fieldAlreadyExists(name:String):Bool
{
for (field in Context.getBuildFields())
{
if (field.name == name && !((field.access ?? []).contains(Access.AAbstract)))
{
return true;
}
}
function fieldAlreadyExistsSuper(name:String, superClass:Null<ClassType>)
{
if (superClass == null)
{
return false;
}
for (field in superClass.fields.get())
{
if (field.name == name && !field.isAbstract)
{
return true;
}
}
// recursively check superclasses
return fieldAlreadyExistsSuper(name, superClass.superClass?.t.get());
}
return fieldAlreadyExistsSuper(name, Context.getLocalClass().get().superClass?.t.get());
}
static function getTypeParams(cls:ClassType):RegistryTypeParamsNew
{
switch (cls.superClass.t.get().kind)
{
case KGenericInstance(_, params):
var typeParams:Array<Dynamic> = [];
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 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 buildRegistryVariables(cls:ClassType):Array<Field>
{
var clsType:ComplexType = Context.getType('${cls.module}.${cls.name}').toComplexType();
var newInstance:String = 'new ${cls.module}.${cls.name}()';
return (macro class TempClass
{
static var _instance:Null<$clsType>;
public static var instance(get, never):$clsType;
static function get_instance():$clsType
{
if (_instance == null)
{
_instance = ${Context.parse(newInstance, Context.currentPos())};
}
return _instance;
}
}).fields.filter((field) -> return !fieldAlreadyExists(field.name));
}
static function buildRegistryMethods(cls:ClassType):Array<Field>
{
var impl:String = 'funkin.macro.impl._${cls.name}_Impl';
return (macro class TempClass
{
function getScriptedClassNames()
{
return ${Context.parse(impl, Context.currentPos())}.getScriptedClassNames(this);
}
function createScriptedEntry(clsName:String)
{
return ${Context.parse(impl, Context.currentPos())}.createScriptedEntry(this, clsName);
}
public function parseEntryData(id:String)
{
return ${Context.parse(impl, Context.currentPos())}.parseEntryData(this, id);
}
public function parseEntryDataRaw(contents:String, ?fileName:String)
{
return ${Context.parse(impl, Context.currentPos())}.parseEntryDataRaw(this, contents, fileName);
}
}).fields.filter((field) -> return !fieldAlreadyExists(field.name));
}
static function buildEntryVariables(cls:ClassType, entryData:Dynamic):Array<Field>
{
var entryDataType:ComplexType = Context.getType('${entryData.module}.${entryData.name}').toComplexType();
return (macro class TempClass
{
public final id:String;
public final _data:Null<$entryDataType>;
}).fields.filter((field) -> return !fieldAlreadyExists(field.name));
}
static function buildEntryMethods(cls:ClassType):Array<Field>
{
var impl:String = 'funkin.macro.impl._${cls.name}_Impl';
return (macro class TempClass
{
public function _fetchData(id:String)
{
return ${Context.parse(impl, Context.currentPos())}._fetchData(this, id);
}
public function toString()
{
return ${Context.parse(impl, Context.currentPos())}.toString(this);
}
public function destroy()
{
${Context.parse(impl, Context.currentPos())}.destroy(this);
}
}).fields.filter((field) -> !fieldAlreadyExists(field.name));
}
static function buildRegistryImpl(cls:ClassType, entryCls:ClassType, scriptedEntryCls:ClassType, jsonCls:Dynamic):Void
{
var clsType:ComplexType = Context.getType('${cls.module}.${cls.name}').toComplexType();
var getScriptedClassName:String = '${scriptedEntryCls.module}.${scriptedEntryCls.name}';
var createScriptedEntry:String = '${scriptedEntryCls.module}.${scriptedEntryCls.name}.init(clsName, "unknown")';
var newJsonParser:String = 'new json2object.JsonParser<${jsonCls.module}.${jsonCls.name}>()';
Context.defineType(
{
pos: Context.currentPos(),
pack: ['funkin', 'macro', 'impl'],
name: '_${cls.name}_Impl',
kind: TypeDefKind.TDClass(null, [], false, false, false),
fields: (macro class TempClass
{
public static inline function getScriptedClassNames(me:$clsType)
{
return ${Context.parse(getScriptedClassName, Context.currentPos())}.listScriptClasses();
}
public static inline function createScriptedEntry(me:$clsType, clsName:String)
{
return ${Context.parse(createScriptedEntry, Context.currentPos())};
}
public static inline function parseEntryData(me:$clsType, id:String)
{
var parser = ${Context.parse(newJsonParser, Context.currentPos())};
parser.ignoreUnknownVariables = false;
@:privateAccess
switch (me.loadEntryFile(id))
{
case {fileName: fileName, contents: contents}:
parser.fromJson(contents, fileName);
default:
return null;
}
if (parser.errors.length > 0)
{
@:privateAccess
me.printErrors(parser.errors, id);
return null;
}
return parser.value;
}
public static inline function parseEntryDataRaw(me:$clsType, contents:String, ?fileName:String)
{
var parser = ${Context.parse(newJsonParser, Context.currentPos())};
parser.ignoreUnknownVariables = false;
parser.fromJson(contents, fileName);
if (parser.errors.length > 0)
{
@:privateAccess
me.printErrors(parser.errors, fileName);
return null;
}
return parser.value;
}
}).fields
});
}
static function buildEntryImpl(cls:ClassType, registryCls:ClassType):Void
{
var clsType:ComplexType = Context.getType('${cls.module}.${cls.name}').toComplexType();
var registry:String = '${registryCls.module}.${registryCls.name}';
Context.defineType(
{
pos: Context.currentPos(),
pack: ['funkin', 'macro', 'impl'],
name: '_${cls.name}_Impl',
kind: TypeDefKind.TDClass(null, [], false, false, false),
fields: (macro class TempClass
{
public static inline function _fetchData(me:$clsType, id:String)
{
return $
{
Context.parse(registry, Context.currentPos())
}.instance.parseEntryDataWithMigration(id, ${Context.parse(registry, Context.currentPos())}.instance.fetchEntryVersion(id));
}
public static inline function toString(me:$clsType)
{
return $v{cls.name} + '(' + me.id + ')';
}
public static inline function destroy(me:$clsType) {}
}).fields
});
}
#end
}
#if macro
typedef RegistryTypeParamsNew =
{
var entryCls:ClassType;
var jsonCls:Dynamic; // DefType or ClassType
}
#end