Error
```
source/Prebuild.hx:15: Building...
source/funkin/util/macro/RegistryMacro.hx:46: ENTRY: Song
source/funkin/util/macro/RegistryMacro.hx:24: REGISTRY: SongRegistry
source/funkin/util/macro/RegistryMacro.hx:46: ENTRY: PlayableCharacter
source/funkin/util/macro/RegistryMacro.hx:24: REGISTRY: PlayerRegistry
source/funkin/util/macro/RegistryMacro.hx:24: REGISTRY: AlbumRegistry
source/funkin/util/macro/RegistryMacro.hx:46: ENTRY: Album
source/funkin/util/macro/RegistryMacro.hx:46: ENTRY: Conversation
source/funkin/util/macro/RegistryMacro.hx:24: REGISTRY: ConversationRegistry
source/funkin/util/macro/RegistryMacro.hx:46: ENTRY: NoteStyle
source/funkin/util/macro/RegistryMacro.hx:24: REGISTRY: NoteStyleRegistry
ERROR source/funkin/ui/freeplay/Album.hx:99: characters 26-34
99 | return AlbumRegistry.instance.parseEntryDataWithMigration(id, AlbumRegistry.instance.fetchEntryVersion(id));
| ^^^^^^^^
| Class has no field instance
```
Album
```haxe
package funkin.ui.freeplay;
import funkin.data.freeplay.album.AlbumData;
import funkin.data.freeplay.album.AlbumRegistry;
import funkin.data.animation.AnimationData;
import funkin.data.IRegistryEntry;
import flixel.graphics.FlxGraphic;
/**
* A class representing the data for an album as displayed in Freeplay.
*/
@:build(funkin.util.macro.RegistryMacro.buildEntry())
class Album implements IRegistryEntry
{
/**
* The internal ID for this album.
*/
public final id:String;
/**
* The full data for an album.
*/
public final _data:AlbumData;
public function new(id:String)
{
this.id = id;
this._data = _fetchData(id);
if (_data == null)
{
throw 'Could not parse album data for id: $id';
}
}
/**
* Return the name of the album.
* @
*/
public function getAlbumName():String
{
return _data.name;
}
/**
* Return the artists of the album.
* @return The list of artists
*/
public function getAlbumArtists():Array
{
return _data.artists;
}
/**
* Get the asset key for the album art.
* @return The asset key
*/
public function getAlbumArtAssetKey():String
{
return _data.albumArtAsset;
}
/**
* Get the album art as a graphic, ready to apply to a sprite.
* @return The built graphic
*/
public function getAlbumArtGraphic():FlxGraphic
{
return FlxG.bitmap.add(Paths.image(getAlbumArtAssetKey()));
}
/**
* Get the asset key for the album title.
*/
public function getAlbumTitleAssetKey():String
{
return _data.albumTitleAsset;
}
public function hasAlbumTitleAnimations()
{
return _data.albumTitleAnimations.length > 0;
}
public function getAlbumTitleAnimations():Array
{
return _data.albumTitleAnimations;
}
public function toString():String
{
return 'Album($id)';
}
public function destroy():Void {}
static function _fetchData(id:String):Null
{
return AlbumRegistry.instance.parseEntryDataWithMigration(id, AlbumRegistry.instance.fetchEntryVersion(id));
}
}
```
AlbumRegistry
```haxe
package funkin.data.freeplay.album;
import funkin.ui.freeplay.Album;
import funkin.data.freeplay.album.AlbumData;
import funkin.ui.freeplay.ScriptedAlbum;
@:build(funkin.util.macro.RegistryMacro.buildRegistry())
class AlbumRegistry extends BaseRegistry
{
/**
* The current version string for the album data format.
* Handle breaking changes by incrementing this value
* and adding migration to the `migrateAlbumData()` function.
*/
public static final ALBUM_DATA_VERSION:thx.semver.Version = '1.0.0';
public static final ALBUM_DATA_VERSION_RULE:thx.semver.VersionRule = '1.0.x';
public static final instance:AlbumRegistry = new AlbumRegistry();
public function new()
{
super('ALBUM', 'ui/freeplay/albums', ALBUM_DATA_VERSION_RULE);
}
/**
* Read, parse, and validate the JSON data and produce the corresponding data object.
* @param id The ID of the entry to load.
* @return The parsed data object.
*/
public function parseEntryData(id:String):Null
{
// JsonParser does not take type parameters,
// otherwise this function would be in BaseRegistry.
var parser:json2object.JsonParser = 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.
* @return The parsed data object.
*/
public function parseEntryDataRaw(contents:String, ?fileName:String):Null
{
var parser:json2object.JsonParser = 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):Album
{
return ScriptedAlbum.init(clsName, 'unknown');
}
function getScriptedClassNames():Array
{
return ScriptedAlbum.listScriptClasses();
}
}
```
RegistryMacro
```haxe
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
{
var fields = Context.getBuildFields();
var cls = Context.getLocalClass().get();
if (!cls.name.endsWith('Registry'))
{
throw '${cls.module}.${cls.name} needs to end with "Registry"';
}
trace('REGISTRY: ${cls.name}');
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
{
var fields = Context.getBuildFields();
var cls = Context.getLocalClass().get();
trace('ENTRY: ${cls.name}');
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
// doing this if statement fixes the order
// for the song build macros
if (cls.name == 'Song')
{
MacroUtil.getClassTypeFromExpr(macro funkin.data.song.SongRegistry);
return;
}
var registries:Array = [];
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)
{
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 = [];
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
{
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
{
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
{
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
{
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
```
Information
Haxe: 4.3.4
Platform: Windows
Target: cpp
the RegistryMacro creates fields for an Entry or Registry if not yet defined
this works fine if the order of the build macro is Entry -> Registry (atleast i assume)
as you can see in the error when the Album and AlbumRegistry gets built, it builds the AlbumRegistry first and then Album, and i assume this is the problem
so my question is if there is a way to make the build order not matter,
or if there is a way to determine the build order
Reference Repository
https://github.com/lemz1/Funkin/tree/feature/registry-marco-broken
Error and Relevant Classes
Error
``` source/Prebuild.hx:15: Building... source/funkin/util/macro/RegistryMacro.hx:46: ENTRY: Song source/funkin/util/macro/RegistryMacro.hx:24: REGISTRY: SongRegistry source/funkin/util/macro/RegistryMacro.hx:46: ENTRY: PlayableCharacter source/funkin/util/macro/RegistryMacro.hx:24: REGISTRY: PlayerRegistry source/funkin/util/macro/RegistryMacro.hx:24: REGISTRY: AlbumRegistry source/funkin/util/macro/RegistryMacro.hx:46: ENTRY: Album source/funkin/util/macro/RegistryMacro.hx:46: ENTRY: Conversation source/funkin/util/macro/RegistryMacro.hx:24: REGISTRY: ConversationRegistry source/funkin/util/macro/RegistryMacro.hx:46: ENTRY: NoteStyle source/funkin/util/macro/RegistryMacro.hx:24: REGISTRY: NoteStyleRegistry ERROR source/funkin/ui/freeplay/Album.hx:99: characters 26-34 99 | return AlbumRegistry.instance.parseEntryDataWithMigration(id, AlbumRegistry.instance.fetchEntryVersion(id)); | ^^^^^^^^ | ClassAlbum
```haxe package funkin.ui.freeplay; import funkin.data.freeplay.album.AlbumData; import funkin.data.freeplay.album.AlbumRegistry; import funkin.data.animation.AnimationData; import funkin.data.IRegistryEntry; import flixel.graphics.FlxGraphic; /** * A class representing the data for an album as displayed in Freeplay. */ @:build(funkin.util.macro.RegistryMacro.buildEntry()) class Album implements IRegistryEntryAlbumRegistry
```haxe package funkin.data.freeplay.album; import funkin.ui.freeplay.Album; import funkin.data.freeplay.album.AlbumData; import funkin.ui.freeplay.ScriptedAlbum; @:build(funkin.util.macro.RegistryMacro.buildRegistry()) class AlbumRegistry extends BaseRegistryRegistryMacro
```haxe 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():ArrayInformation
Haxe: 4.3.4 Platform: Windows Target: cpp
the
RegistryMacro
creates fields for an Entry or Registry if not yet definedthis works fine if the order of the build macro is
Entry -> Registry
(atleast i assume)as you can see in the error when the
Album
andAlbumRegistry
gets built, it builds theAlbumRegistry
first and thenAlbum
, and i assume this is the problemso my question is if there is a way to make the build order not matter, or if there is a way to determine the build order