package funkin.backend.system.macros; import haxe.macro.Context; import haxe.macro.Expr; using StringTools; using haxe.macro.Tools; // Macro made by Ne_Eo class ControlsMacro { static var _allControls: Array = null; static var _allInternalControls: Array = null; static var _currentControls: Map> = null; static var _currentGamepadControls: Map = null; static var _keySet: Map = null; static var _internalMap: Map = null; public static macro function build(): Array { var fields = Context.getBuildFields(); var clRef = Context.getLocalClass(); if (clRef == null) return fields; var cl = clRef.get(); _allControls = []; _allInternalControls = []; _currentControls = []; _currentGamepadControls = []; _keySet = []; _internalMap = []; var fields = Context.getBuildFields(); for (field in fields.copy()) { switch (field.kind) { case FProp(g, s, t, e): var controlFields = handleControl(field, g, s, t, e); for (field in controlFields) if (field != null) fields.push(field); default: } } // Public fields fields.push({ name: "addDefaultGamepad", access: [APublic], kind: FFun({ ret: macro : Void, params: [], expr: generateGamepadCode(), args: [{name: "id", type: macro : Int}] }), pos: Context.currentPos(), doc: null, meta: [] }); // Private fields, are prefixed with macro_, for readability fields.push({ name: "macro_addKeysToActions", access: [APublic, AInline], kind: FFun({ ret: macro : Void, params: [], expr: macro $b{_allInternalControls.map((s) -> macro add($i{s}))}, args: [] }), pos: Context.currentPos(), doc: null, meta: [] }); function buildBindControl(s:String, prefix:String) { var prefixxed = prefix + s; return macro bindKeys(Control.$s, Options.$prefixxed); } fields.push({ name: "macro_bindControls", access: [], kind: FFun({ ret: macro : Void, params: [], expr: macro switch(keyScheme) { case Solo: $a{_allControls.map(buildBindControl.bind(_, "SOLO_"))}; case Duo(true): $a{_allControls.map(buildBindControl.bind(_, "P1_"))}; case Duo(false): $a{_allControls.map(buildBindControl.bind(_, "P2_"))}; case None: // nothing case Custom: // nothing }, args: [{ name: "keyScheme", type: macro : funkin.backend.system.Controls.KeyboardScheme }] }), pos: Context.currentPos(), doc: null, meta: [] }); fields.push({ name: "macro_forEachBound", access: [], kind: FFun({ ret: macro : Void, params: [], expr: generateForEachBoundCode(macro control), args: [ { name: "control", type: macro : Control }, { name: "func", type: macro : FlxActionDigital -> FlxInputState -> Void } ] }), pos: Context.currentPos(), doc: null, meta: [] }); fields.push({ name: "macro_getActionFromControl", access: [APrivate], kind: FFun({ ret: macro : FlxActionDigital, params: [], expr: generateGetActionFromControlCode(macro control), args: [{ name: "control", type: macro : Control }] }), pos: Context.currentPos(), doc: null, meta: [] }); // for (field in fields) // trace(new haxe.macro.Printer().printField(field)); _allControls = null; _allInternalControls = null; _currentControls = null; _currentGamepadControls = null; _keySet = null; _internalMap = null; return fields; } static function generateSwitchCase(expr:Expr, cases:Array, defaultCase:Expr): Expr { return { expr: ESwitch(expr, cases, defaultCase), pos: Context.currentPos() } } static function generateGetActionFromControlCode(value:Expr): Expr { return macro return ${generateSwitchCase(value, [ for (short => internal in _internalMap) { values: [macro Control.$short], expr: macro $i{internal} } ], macro null)}; } static function generateForEachBoundCode(value:Expr): Expr { return generateSwitchCase(value, [ for (field => code in _currentControls) { values: [macro Control.$field], expr: macro $b{code} } ], macro {}); } static function generateGamepadCode(): Expr { var map: Array = []; for (name in _allControls) { var expr = _currentGamepadControls.get(name); if(expr == null) continue; map.push(macro @:pos(expr.pos) Control.$name => ${expr}); } return macro addGamepad(id, $a{map}); } static function extractString(e: Expr): String { switch (e.expr) { case EConst(CString(s)): return s; default: Context.error("Expected a string", e.pos); throw "Expected a String"; } } static function camelCase(s: String): String { s = s.split("_").map((s) -> { return s.charAt(0).toUpperCase() + s.substr(1).toLowerCase(); }).join(""); return s.charAt(0).toLowerCase() + s.substr(1); } static function swapAB(e: Expr): Expr { return switch (e.expr) { case EConst(CIdent('A')): macro B; case EConst(CIdent('B')): macro A; default: e.map(swapAB); } } static function handleControl(field: Field, get: String, set: String, _: ComplexType, e: Expr): Array { var name = field.name; var shortName = name; var type = ""; if (shortName.endsWith(type = "_P")) shortName = shortName.substr(0, shortName.length - 2); else if (shortName.endsWith(type = "_R")) shortName = shortName.substr(0, shortName.length - 2); else if (shortName.endsWith(type = "_H")) shortName = shortName.substr(0, shortName.length - 2); else if (shortName.endsWith(type = "_HOLD")) shortName = shortName.substr(0, shortName.length - 5); else type = ""; var internalName = "_" + camelCase(shortName); var internalNameNoType = internalName; if (type != "") internalName += type.substr(1); var keyset: Null = null; var expr: Expr = null; var metasToRemove = []; for (meta in field.meta) { var shouldRemove = true; switch (meta.name) { case ":gamepad" | ":rawGamepad": if (!_currentGamepadControls.exists(shortName)) { var expr = meta.params[0]; var shouldRemap = meta.name != ":rawGamepad"; if (shouldRemap && Context.defined("switch")) { expr = swapAB(expr); } _currentGamepadControls.set(shortName, expr); } case ":pressed": keyset = extractString(meta.params[0]); expr = macro func($i{internalName}, PRESSED); case ":justPressed": keyset = extractString(meta.params[0]); expr = macro func($i{internalName}, JUST_PRESSED); case ":released": keyset = extractString(meta.params[0]); expr = macro func($i{internalName}, RELEASED); case ":justReleased": keyset = extractString(meta.params[0]); expr = macro func($i{internalName}, JUST_RELEASED); default: shouldRemove = false; } if (shouldRemove) metasToRemove.push(meta); } for (meta in metasToRemove) field.meta.remove(meta); if (_currentControls.exists(shortName)) _currentControls.get(shortName).push(expr); else _currentControls.set(shortName, [expr]); if (!_allInternalControls.contains(internalName)) _allInternalControls.push(internalName); if (!_allControls.contains(shortName)) _allControls.push(shortName); _keySet.set(shortName, keyset); if (!_internalMap.exists(internalNameNoType)) _internalMap.set(shortName, internalNameNoType); // Generated Code: var _uiUp = new FlxActionDigital("_uiUp"); var internalField: Field = { name: internalName, access: [APrivate], kind: FVar(macro : FlxActionDigital, macro new FlxActionDigital($v{internalName})), pos: field.pos, doc: null, meta: [] }; // Generated Code: // inline function get_UI_UP(): Bool // return _uiUp.check(); var getField: Field = { name: "get_" + name, access: [APrivate, AInline], kind: FFun({ ret: macro : Bool, params: [], expr: macro { return $i{internalName}.check(); }, args: [] }), pos: field.pos, doc: field.doc, meta: [] }; // Generated Code: // inline function set_UI_UP(val: Bool): Bool // return @:privateAccess _uiUp._checked = val; var setField: Field = { name: "set_" + name, access: [APrivate, AInline], kind: FFun({ ret: macro : Bool, params: [], expr: macro { return @:privateAccess $i{internalName}._checked = val; }, args: [{name: "val", type: macro : Bool}] }), pos: field.pos, doc: field.doc, meta: [] }; return [internalField, getField, set == "set" ? setField : null]; } }