mirror of
https://github.com/ninjamuffin99/Funkin.git
synced 2025-07-22 12:21:07 +00:00
392 lines
12 KiB
Haxe
392 lines
12 KiB
Haxe
package funkin.util.macro;
|
|
|
|
import haxe.macro.Context;
|
|
import haxe.macro.Expr;
|
|
import haxe.macro.Expr.ComplexType;
|
|
import haxe.macro.Expr.Field;
|
|
import haxe.macro.Expr.TypeDefKind;
|
|
import haxe.macro.Expr.MetadataEntry;
|
|
import haxe.macro.Type;
|
|
import haxe.macro.Type.ClassType;
|
|
|
|
using Lambda;
|
|
using haxe.macro.ExprTools;
|
|
using haxe.macro.TypeTools;
|
|
using StringTools;
|
|
|
|
/**
|
|
* The type parameters for a class extending `BaseRegistry`.
|
|
*/
|
|
typedef RegistryTypeParams =
|
|
{
|
|
/**
|
|
* The class type of the entry. Must implement `IRegistryEntry`.
|
|
*/
|
|
var entryType:ClassType;
|
|
|
|
/**
|
|
* The type for the data of an entry. This is usually a typedef of a struct.
|
|
*/
|
|
var dataType:Any; // DefType or ClassType
|
|
|
|
}
|
|
|
|
/**
|
|
* A set of build macros to be applied to `Registry` classes in the `funkin.data` package.
|
|
*
|
|
* @see `funkin.data.BaseRegistry`
|
|
*/
|
|
class RegistryMacro
|
|
{
|
|
static final DATA_FILE_BASE_PATH:String = 'assets/preload/data';
|
|
|
|
/**
|
|
* Builds the registry class.
|
|
*
|
|
* @return The modified list of fields for the target class.
|
|
*/
|
|
public static macro function buildRegistry():Array<Field>
|
|
{
|
|
var cls:ClassType = Context.getLocalClass().get();
|
|
var fields:Array<Field> = Context.getBuildFields();
|
|
|
|
// Classes with the `@:funkinBase` meta or `@:funkinProcessed` meta should be ignored.
|
|
var baseMeta:Null<MetadataEntry> = cls.meta.get().find(function(m) return m.name == ':funkinBase');
|
|
if (baseMeta != null || alreadyProcessed(cls)) return fields;
|
|
|
|
var typeParams:RegistryTypeParams = getTypeParams(cls);
|
|
|
|
// Build an internal class with static functions that allow the Entry class to call functions on the Registry class.
|
|
buildEntryImpl(typeParams.entryType, cls);
|
|
|
|
fields = fields.concat(buildRegistryMethods(cls, fields, typeParams.entryType, typeParams.dataType));
|
|
|
|
// Indicate that the class has been processed so we don't process twice.
|
|
cls.meta.add(":funkinProcessed", [], cls.pos);
|
|
|
|
return fields;
|
|
}
|
|
|
|
/**
|
|
* Builds the registry entry class.
|
|
*
|
|
* @return The modified list of fields for the target class.
|
|
*/
|
|
public static macro function buildEntry():Array<Field>
|
|
{
|
|
var cls:ClassType = Context.getLocalClass().get();
|
|
var fields:Array<Field> = Context.getBuildFields();
|
|
|
|
// Classes with the `@:funkinProcessed` meta should be ignored.
|
|
if (alreadyProcessed(cls)) return fields;
|
|
|
|
// Get the type of the JSON data for an entry.
|
|
var entryData:Any = getEntryData(cls);
|
|
|
|
// Build variables and methods for the entry.
|
|
fields = fields.concat(buildEntryVariables(cls, entryData));
|
|
fields = fields.concat(buildEntryMethods(cls));
|
|
|
|
// Indicate that the class has been processed so we don't process twice.
|
|
cls.meta.add(":funkinProcessed", [], cls.pos);
|
|
|
|
return fields;
|
|
}
|
|
|
|
#if macro
|
|
/**
|
|
* Retrieve the type parameters for a class extending `BaseRegistry<T, J>`.
|
|
* @param cls The class to retrieve the type parameters for.
|
|
* @return The type parameters for the class.
|
|
*/
|
|
static function getTypeParams(cls:ClassType):RegistryTypeParams
|
|
{
|
|
switch (cls.superClass.t.get().kind)
|
|
{
|
|
case KGenericInstance(_, params):
|
|
var typeParams:Array<Any> = [];
|
|
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 {entryType: typeParams[0], dataType: typeParams[1]};
|
|
default:
|
|
throw '${cls.name}: Could not interpret type parameters of Registry class.';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Builds new static and instance methods for a registry class.
|
|
*
|
|
* @param cls The registry class to build the methods for.
|
|
* @param fields The fields of the registry class.
|
|
* @param entryType The class type of entries in the registry.
|
|
* @param dataType The type of the data for entries in the registry.
|
|
* @return The modified list of fields for the target class.
|
|
*/
|
|
static function buildRegistryMethods(cls:ClassType, fields:Array<Field>, entryType:ClassType, dataType:Dynamic):Array<Field>
|
|
{
|
|
var scriptedEntryClsName:String = entryType.pack.join('.') + '.Scripted' + entryType.name;
|
|
|
|
var getScriptedClassName:String = '${scriptedEntryClsName}';
|
|
|
|
var createScriptedEntry:String = '${scriptedEntryClsName}.init(clsName, "unknown")';
|
|
|
|
var newJsonParser:String = 'new json2object.JsonParser<${dataType.module}.${dataType.name}>()';
|
|
|
|
var dataFilePath:String = getRegistryDataFilePath(cls, fields);
|
|
var baseGameEntryIds:Array<Expr> = listBaseGameEntryIds('${DATA_FILE_BASE_PATH}/${dataFilePath}/');
|
|
|
|
return (macro class TempClass
|
|
{
|
|
public function listBaseGameEntryIds():Array<String>
|
|
{
|
|
return $a{baseGameEntryIds};
|
|
}
|
|
|
|
public function listModdedEntryIds():Array<String>
|
|
{
|
|
return listEntryIds().filter(function(id:String):Bool {
|
|
return listBaseGameEntryIds().indexOf(id) == -1;
|
|
});
|
|
}
|
|
|
|
function getScriptedClassNames()
|
|
{
|
|
return ${Context.parse(getScriptedClassName, Context.currentPos())}.listScriptClasses();
|
|
}
|
|
|
|
function createScriptedEntry(clsName:String)
|
|
{
|
|
return ${Context.parse(createScriptedEntry, Context.currentPos())};
|
|
}
|
|
|
|
public function parseEntryData(id:String)
|
|
{
|
|
var parser = ${Context.parse(newJsonParser, Context.currentPos())};
|
|
parser.ignoreUnknownVariables = false;
|
|
|
|
@:privateAccess
|
|
switch (this.loadEntryFile(id))
|
|
{
|
|
case {fileName: fileName, contents: contents}:
|
|
parser.fromJson(contents, fileName);
|
|
default:
|
|
return null;
|
|
}
|
|
|
|
if (parser.errors.length > 0)
|
|
{
|
|
@:privateAccess
|
|
this.printErrors(parser.errors, id);
|
|
return null;
|
|
}
|
|
return parser.value;
|
|
}
|
|
|
|
public function parseEntryDataRaw(contents:String, ?fileName:String)
|
|
{
|
|
var parser = ${Context.parse(newJsonParser, Context.currentPos())};
|
|
parser.ignoreUnknownVariables = false;
|
|
parser.fromJson(contents, fileName);
|
|
|
|
if (parser.errors.length > 0)
|
|
{
|
|
@:privateAccess
|
|
this.printErrors(parser.errors, fileName);
|
|
return null;
|
|
}
|
|
return parser.value;
|
|
}
|
|
}).fields.filter((field) -> return !MacroUtil.fieldAlreadyExists(field.name));
|
|
}
|
|
|
|
/**
|
|
* Retrieve the type of the JSON data for an entry.
|
|
* @param cls The entry class to retrieve the type of the JSON data for.
|
|
* @return Will be either a `DefType` or a `ClassType`.
|
|
*/
|
|
static function getEntryData(cls:ClassType):Any // DefType or ClassType
|
|
{
|
|
try
|
|
{
|
|
switch (cls.interfaces[0].params[0])
|
|
{
|
|
case Type.TInst(t, _):
|
|
return t.get();
|
|
case Type.TType(t, _):
|
|
return t.get();
|
|
default:
|
|
throw '${cls.name}: Type parameter for Entry must be a Class or typedef';
|
|
}
|
|
}
|
|
catch (e)
|
|
{
|
|
throw '${cls.name}: IRegistryEntry must be the last implemented interface';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add fields to the entry class.
|
|
* @param cls The entry class to add fields to.
|
|
* @param entryData The type of the data for the entry.
|
|
* @return The modified list of fields for the target class.
|
|
*/
|
|
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 !MacroUtil.fieldAlreadyExists(field.name));
|
|
}
|
|
|
|
/**
|
|
* Add methods to the entry class.
|
|
* @param cls The entry class to add methods to.
|
|
* @return The modified list of fields for the target class.
|
|
*/
|
|
static function buildEntryMethods(cls:ClassType):Array<Field>
|
|
{
|
|
// The internal class built by `buildEntryImpl`.
|
|
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) -> return !MacroUtil.fieldAlreadyExists(field.name));
|
|
}
|
|
|
|
/**
|
|
* Build an internal class that calls functions for an associated registry class.
|
|
* @param cls The entry class to build the internal class for.
|
|
* @param registryCls The registry class that the entry class is associated with.
|
|
*/
|
|
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
|
|
});
|
|
}
|
|
|
|
static function getRegistryDataFilePath(cls:ClassType, fields:Array<Field>):String
|
|
{
|
|
for (field in fields)
|
|
{
|
|
if (field.name == 'new')
|
|
{
|
|
// We found a field called `new`, it's probably the constructor.
|
|
switch (field.kind)
|
|
{
|
|
case FFun(f):
|
|
// Inside the function.
|
|
switch (f.expr.expr)
|
|
{
|
|
// Inside the block.
|
|
case EBlock(exprs):
|
|
var superCall:Expr = exprs[0];
|
|
switch (superCall.expr)
|
|
{
|
|
// Inside the super() call.
|
|
case ECall(_, args):
|
|
// var registryId:String = args[0].toString();
|
|
var dataPath:String = args[1].toString().replace('"', '').replace("'", '');
|
|
// var versionRule = args[2].toString();
|
|
|
|
return dataPath;
|
|
default:
|
|
Context.error('${cls.name}.new: RegistryMacro expected super call', field.pos);
|
|
}
|
|
default:
|
|
Context.error('${cls.name}.new: RegistryMacro expected super call', field.pos);
|
|
}
|
|
default:
|
|
// Continue looking for the actual constructor.
|
|
}
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
static function listBaseGameEntryIds(dataFilePath:String):Array<Expr>
|
|
{
|
|
var result:Array<Expr> = [];
|
|
var files:Array<String> = sys.FileSystem.readDirectory(dataFilePath);
|
|
|
|
for (file in files)
|
|
{
|
|
result.push(macro $v{file.replace('.json', '')});
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Check whether this class has already been processed by the RegistryMacro,
|
|
* as indicated by the `@:funkinProcessed` meta.
|
|
* @param cls The class to check.
|
|
* @return `true` if the class has already been processed, `false` otherwise.
|
|
*/
|
|
static function alreadyProcessed(cls:ClassType):Bool
|
|
{
|
|
// Check for the `@:funkinProcessed` meta.
|
|
var processedMeta:MetadataEntry = cls.meta.get().find(function(m) return m.name == ':funkinProcessed');
|
|
if (processedMeta != null) return true;
|
|
|
|
// If it's not found, check the superclass.
|
|
if (cls.superClass != null) return alreadyProcessed(cls.superClass.t.get());
|
|
return false;
|
|
}
|
|
#end
|
|
}
|