diff --git a/project.hxp b/project.hxp index 2ec8ba4458..d0559d08e5 100644 --- a/project.hxp +++ b/project.hxp @@ -588,6 +588,15 @@ class Project extends HXProject { function configureCustomMacros() { // This macro allows addition of new functionality to existing Flixel. --> addHaxeMacro("addMetadata('@:build(funkin.util.macro.FlxMacro.buildFlxBasic())', 'flixel.FlxBasic')"); + + var abstracts:Array = [ + '.*', + '!thx.Set', + '!cpp.*', + '!lime.*', + '!openfl.*' + ]; + addHaxeMacro("funkin.util.macro.PolymodMacro.buildPolymodAbstracts(['" + abstracts.join("', '") + "'])"); } function configureOutputDir() { diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx index e5bcae0f55..14906faba9 100644 --- a/source/funkin/modding/PolymodHandler.hx +++ b/source/funkin/modding/PolymodHandler.hx @@ -250,6 +250,11 @@ class PolymodHandler Polymod.addImportAlias('lime.utils.Assets', funkin.Assets); Polymod.addImportAlias('openfl.utils.Assets', funkin.Assets); + for (key => value in funkin.util.macro.PolymodMacro.aliases) + { + Polymod.addImportAlias(key, Type.resolveClass(value)); + } + // Add blacklisting for prohibited classes and packages. // `Sys` @@ -335,8 +340,19 @@ class PolymodHandler { return { assetLibraryPaths: [ - 'default' => 'preload', 'shared' => 'shared', 'songs' => 'songs', 'videos' => 'videos', 'tutorial' => 'tutorial', 'week1' => 'week1', - 'week2' => 'week2', 'week3' => 'week3', 'week4' => 'week4', 'week5' => 'week5', 'week6' => 'week6', 'week7' => 'week7', 'weekend1' => 'weekend1', + 'default' => 'preload', + 'shared' => 'shared', + 'songs' => 'songs', + 'videos' => 'videos', + 'tutorial' => 'tutorial', + 'week1' => 'week1', + 'week2' => 'week2', + 'week3' => 'week3', + 'week4' => 'week4', + 'week5' => 'week5', + 'week6' => 'week6', + 'week7' => 'week7', + 'weekend1' => 'weekend1', ], coreAssetRedirect: CORE_FOLDER, } diff --git a/source/funkin/util/macro/PolymodMacro.hx b/source/funkin/util/macro/PolymodMacro.hx new file mode 100644 index 0000000000..c1c23253a6 --- /dev/null +++ b/source/funkin/util/macro/PolymodMacro.hx @@ -0,0 +1,415 @@ +package funkin.util.macro; + +import haxe.macro.Context; +import haxe.macro.Expr; +import haxe.macro.Type; + +using haxe.macro.ExprTools; +using StringTools; + +/** + * This macro creates aliases for abstracts. + * That way we can use abstracts in hscript + */ +@SuppressWarnings(['checkstyle:CodeSimilarity', 'checkstyle:Dynamic']) +class PolymodMacro +{ + /** + * The abstracts and their corresponding aliases + * `key` => original class path + * `value` => alias class path + */ + public static var aliases(get, never):Map; + + static function get_aliases():Map + { + // truly a sight to behold + return Reflect.callMethod(null, Reflect.field(Type.resolveClass('funkin.util.macro.AbstractAliases'), 'get'), []); + } + + /** + * Function that builds all the alias classes + * @param abstractClasses An array of packages and classes + * (`!` infront of the name will exclude classes from being built) + */ + public static macro function buildPolymodAbstracts(abstractClasses:Array):Void + { + Context.onAfterTyping((types) -> { + if (alreadyCalled) + { + return; + } + + var sortedAbstractClasses:Array = []; + for (abstractCls in abstractClasses) + { + if (abstractCls.startsWith('!')) + { + sortedAbstractClasses.insert(0, abstractCls); + } + else + { + sortedAbstractClasses.push(abstractCls); + } + } + + var abstractAliases:Map = new Map(); + + for (type in types) + { + switch (type) + { + case ModuleType.TAbstract(a): + var cls:AbstractType = a.get(); + if (cls.isPrivate) + { + continue; + } + + for (abstractCls in sortedAbstractClasses) + { + var negate:Bool = abstractCls.startsWith('!'); + var name:String = abstractCls.replace('!', '').replace('.*', ''); + if (!negate && !cls.module.startsWith(name) && moduleTypePath(cls) != name && packTypePath(cls) != name) + { + continue; + } + else if (negate) + { + if (cls.module.startsWith(name) || moduleTypePath(cls) == name || packTypePath(cls) == name) + { + break; + } + else + { + continue; + } + } + + abstractAliases.set('${packTypePath(cls)}', 'polymod.abstracts.${packTypePath(cls)}${classSuffix}'); + buildAbstract(cls); + break; + } + default: + // do nothing + } + } + + Context.defineModule('funkin.util.macro.PolymodMacro', [ + { + pack: ['funkin', 'util', 'macro'], + name: 'AbstractAliases', + kind: TypeDefKind.TDClass(null, [], false, false, false), + fields: [ + { + name: 'get', + access: [Access.APublic, Access.AStatic], + kind: FieldType.FFun( + { + args: [], + ret: (macro :Map), + expr: macro + { + return $v{abstractAliases}; + } + }), + pos: Context.currentPos() + } + ], + pos: Context.currentPos() + } + ]); + + // the callback is called twice, which this leads to issues + alreadyCalled = true; + }); + } + + #if macro + static var classSuffix:String = '_'; + static var alreadyCalled:Bool = false; + static var skipFields:Array = []; + + static function buildAbstract(abstractCls:AbstractType):Void + { + if (abstractCls.impl == null) + { + return; + } + + skipFields = []; + + var cls:ClassType = abstractCls.impl.get(); + + // we use the functions to check whether we need to skip some fields + // that is why we sort the fields, so that functions are handled first + var sortedFields:Array = sortFields(cls.statics.get()); + + var fields:Array = []; + for (field in sortedFields) + { + if (field.name == '_new') + { + fields.push(buildCreateField(abstractCls, field)); + continue; + } + + fields = fields.concat(createFields(abstractCls, field)); + }; + + Context.defineType( + { + pos: Context.currentPos(), + pack: ['polymod', 'abstracts'].concat(abstractCls.pack), + name: abstractCls.name + classSuffix, + kind: TypeDefKind.TDClass(null, [], false, false, false), + fields: fields, + }, null); + } + + static function sortFields(fields:Array):Array + { + var sortedFields:Array = []; + for (field in fields) + { + switch (field.type) + { + case Type.TLazy(f): + switch (f()) + { + case Type.TFun(_, _): + sortedFields.insert(0, field); + default: + sortedFields.push(field); + } + case Type.TFun(_, _): + sortedFields.insert(0, field); + default: + sortedFields.push(field); + } + } + return sortedFields; + } + + static function buildCreateField(cls:AbstractType, field:ClassField):Field + { + var funcArgs:Array = []; + var funcArgNames:Array = []; + switch (field.type) + { + case Type.TFun(args, _): + for (arg in args) + { + funcArgs.push( + { + name: arg.name, + type: null, + opt: arg.opt + }); + funcArgNames.push(arg.name); + } + default: + throw 'how is this not a function'; + } + + var expr:String = '${newExpr(cls, field)}(${funcArgNames.join(', ')})'; + + return { + name: 'create', + access: [Access.APublic, Access.AStatic], + kind: FieldType.FFun( + { + args: funcArgs, + ret: null, + expr: macro + { + @:privateAccess + return ${Context.parse(expr, Context.currentPos())}; + }, + }), + pos: Context.currentPos() + }; + } + + static function newExpr(cls:AbstractType, field:ClassField):Null + { + if ('${moduleTypePath(cls)}' == 'flixel.util.FlxSignal.FlxTypedSignal') + { + return 'new flixel.util.FlxSignal.FlxTypedSignalVoid>'; + } + + if (cls.params.length <= 0) + { + return 'new ${moduleTypePath(cls)}'; + } + + return 'new ${moduleTypePath(cls)}< ${[for (_ in 0...cls.params.length) 'Dynamic'].join(', ')} >'; + } + + static function createFields(cls:AbstractType, field:ClassField):Array + { + if (skipFields.contains(field.name)) + { + return []; + } + + switch (field.type) + { + case Type.TLazy(f): + return _createFields(cls, field, f()); + default: + return _createFields(cls, field, field.type); + } + } + + static function _createFields(cls:AbstractType, field:ClassField, type:Type):Array + { + if (field.meta.has(':to')) + { + return []; + } + + var fields:Array = []; + + switch (type) + { + case Type.TFun(args, ret): + var fieldArgs:Array = []; + var exprArgs:Array = []; + for (arg in args) + { + if (arg.name == 'this') + { + var memberVariable:String = field.name.replace('get_', '').replace('set_', ''); + if (memberVariable != field.name) + { + skipFields.push(memberVariable); + } + return []; + } + exprArgs.push(arg.name); + fieldArgs.push( + { + name: arg.name, + type: null, + opt: arg.opt, + }); + } + + var returnStr:String = switch (ret) + { + case Type.TAbstract(t, _): + if (t.get().name == 'Void') + { + ''; + } + else + { + 'return '; + } + default: + 'return '; + }; + + var expr:String = '${returnStr}${moduleTypePath(cls)}.${field.name}(${exprArgs.join(', ')})'; + + fields.push( + { + name: field.name, + doc: field.doc, + access: [Access.APublic, Access.AStatic], + kind: FieldType.FFun( + { + args: fieldArgs, + ret: null, + expr: macro + { + @:privateAccess + ${Context.parse(expr, Context.currentPos())}; + }, + params: [] + }), + pos: Context.currentPos() + }); + case Type.TAbstract(t, params): + fields.push( + { + name: field.name, + doc: field.doc, + access: [Access.APublic, Access.AStatic], + kind: FieldType.FProp('get', 'never', (macro :Dynamic), null), + pos: Context.currentPos() + }); + + var expr:String = '${moduleTypePath(cls)}.${field.name}'; + + fields.push( + { + name: 'get_${field.name}', + doc: field.doc, + access: [Access.APublic, Access.AStatic], + kind: FieldType.FFun( + { + args: [], + ret: null, + expr: macro + { + @:privateAccess + return ${Context.parse(expr, Context.currentPos())}; + }, + params: [] + }), + pos: Context.currentPos() + }); + case TType(t, params): + fields.push( + { + name: field.name, + doc: field.doc, + access: [Access.APublic, Access.AStatic], + kind: FieldType.FProp('get', 'never', (macro :Dynamic), null), + pos: Context.currentPos() + }); + + var expr:String = '${moduleTypePath(cls)}.${field.name}'; + + fields.push( + { + name: 'get_${field.name}', + doc: field.doc, + access: [Access.APublic, Access.AStatic], + kind: FieldType.FFun( + { + args: [], + ret: null, + expr: macro + { + @:privateAccess + return ${Context.parse(expr, Context.currentPos())}; + }, + params: [] + }), + pos: Context.currentPos() + }); + default: + return []; + }; + + return fields; + } + + // Dynamic so any kind of type with `module` and `name` fields works + static function moduleTypePath(type:Dynamic):String + { + var dot:String = type.module.length != 0 ? '.' : ''; + return '${type.module}${dot}${type.name}'; + } + + // Dynamic so any kind of type with `pack` and `name` fields works + static function packTypePath(type:Dynamic):String + { + var dot:String = type.pack.length != 0 ? '.' : ''; + return '${type.pack.join('.')}${dot}${type.name}'; + } + #end +}