diff --git a/source/funkin/backend/system/Controls.hx b/source/funkin/backend/system/Controls.hx index 72cd2c46..d247217d 100644 --- a/source/funkin/backend/system/Controls.hx +++ b/source/funkin/backend/system/Controls.hx @@ -150,6 +150,7 @@ class Controls extends FlxActionSet @:pressed("switchmod") public var SWITCHMOD_HOLD(get, set): Bool; @:justReleased("switchmod") public var SWITCHMOD_R(get, set): Bool; + @:allow(funkin.backend.utils.ControlsUtil) var byName:Map = []; public var gamepadsAdded:Array = []; @@ -219,13 +220,13 @@ class Controls extends FlxActionSet macro_forEachBound(control, (action, _) -> removeKeys(action, keys)); } - inline static function addKeys(action:FlxActionDigital, keys:Array, state:FlxInputState) + public inline static function addKeys(action:FlxActionDigital, keys:Array, state:FlxInputState) { for (key in keys) action.addKey(key, state); } - static function removeKeys(action:FlxActionDigital, keys:Array) + public static function removeKeys(action:FlxActionDigital, keys:Array) { var i = action.inputs.length; while (i-- > 0) @@ -302,13 +303,13 @@ class Controls extends FlxActionSet macro_forEachBound(control, (action, _) -> removeButtons(action, gamepadID, buttons)); } - inline static function addButtons(action:FlxActionDigital, buttons:Array, state, id) + public inline static function addButtons(action:FlxActionDigital, buttons:Array, state, id) { for (button in buttons) action.addGamepad(button, state, id); } - static function removeButtons(action:FlxActionDigital, gamepadID:Int, buttons:Array) + public static function removeButtons(action:FlxActionDigital, gamepadID:Int, buttons:Array) { var i = action.inputs.length; while (i-- > 0) @@ -319,8 +320,21 @@ class Controls extends FlxActionSet } } - inline static function isGamepad(input:FlxActionInput, deviceID:Int) + public inline static function isGamepad(input:FlxActionInput, deviceID:Int) { return input.device == GAMEPAD && (deviceID == FlxInputDeviceID.ALL || input.deviceID == deviceID); } -} + + @:nullSafety(Off) + public inline function getJustPressed(name:String) { + return ControlsUtil.getJustPressed(this, name); + } + @:nullSafety(Off) + public inline function getJustReleased(name:String) { + return ControlsUtil.getJustReleased(this, name); + } + @:nullSafety(Off) + public inline function getPressed(name:String) { + return ControlsUtil.getPressed(this, name); + } +} \ No newline at end of file diff --git a/source/funkin/backend/system/MainState.hx b/source/funkin/backend/system/MainState.hx index eef71604..657ae88a 100644 --- a/source/funkin/backend/system/MainState.hx +++ b/source/funkin/backend/system/MainState.hx @@ -26,6 +26,7 @@ class MainState extends FlxState { #end Options.save(); + ControlsUtil.resetCustomControls(); FlxG.bitmap.reset(); FlxG.sound.destroy(true); @@ -57,6 +58,7 @@ class MainState extends FlxState { ModsFolder.onModSwitch.dispatch(ModsFolder.currentModFolder); DiscordUtil.init(); EventsData.reloadEvents(); + ControlsUtil.loadCustomControls(); TitleState.initialized = false; if (betaWarningShown) diff --git a/source/funkin/backend/utils/ControlsUtil.hx b/source/funkin/backend/utils/ControlsUtil.hx new file mode 100644 index 00000000..9d48c25c --- /dev/null +++ b/source/funkin/backend/utils/ControlsUtil.hx @@ -0,0 +1,166 @@ +package funkin.backend.utils; + +import funkin.options.PlayerSettings; +import flixel.input.FlxInput; +import flixel.input.actions.FlxAction; +import funkin.backend.system.Controls; +import flixel.input.actions.FlxActionInput; +import flixel.input.actions.FlxActionManager; +import flixel.input.actions.FlxActionSet; +import flixel.input.gamepad.FlxGamepadInputID; +import flixel.input.keyboard.FlxKey; +import haxe.xml.Access; + +class ControlsUtil { + + private static var storedCustomControls:Array = []; + + public static function initCustomControl(controls:Controls, name:String) { + if (controls.byName.exists(name)) return; + + controls.byName.set(name, new FlxActionDigital(name)); + controls.byName.set(name+"_P", new FlxActionDigital(name+"_P")); + controls.byName.set(name+"_R", new FlxActionDigital(name+"_R")); + storedCustomControls.push(name); + } + + public static function addKeysToCustomControl(controls:Controls, name:String, keys:Array) { + if (!controls.byName.exists(name)) return; + + var pressed:FlxActionDigital = controls.byName.get(name); + if (pressed != null) Controls.addKeys(pressed, keys, PRESSED); + + var justPressed:FlxActionDigital = controls.byName.get(name+"_P"); + if (justPressed != null) Controls.addKeys(justPressed, keys, JUST_PRESSED); + + var justReleased:FlxActionDigital = controls.byName.get(name+"_R"); + if (justReleased != null) Controls.addKeys(justReleased, keys, JUST_RELEASED); + } + + public static function removeKeysFromCustomControl(controls:Controls, name:String, keys:Array) { + if (!controls.byName.exists(name)) return; + + var pressed:FlxActionDigital = controls.byName.get(name); + if (pressed != null) Controls.removeKeys(pressed, keys); + + var justPressed:FlxActionDigital = controls.byName.get(name+"_P"); + if (justPressed != null) Controls.removeKeys(justPressed, keys); + + var justReleased:FlxActionDigital = controls.byName.get(name+"_R"); + if (justReleased != null) Controls.removeKeys(justReleased, keys); + } + + public static function resetCustomControls() { + removeCustomControls(PlayerSettings.solo.controls); + removeCustomControls(PlayerSettings.player1.controls); + removeCustomControls(PlayerSettings.player2.controls); + storedCustomControls = []; + } + + private static inline function removeCustomControls(controls:Controls) { + for (name in storedCustomControls) { + var pressed:FlxActionDigital = controls.byName.get(name); + if (pressed != null) pressed.destroy(); + + var justPressed:FlxActionDigital = controls.byName.get(name+"_P"); + if (justPressed != null) justPressed.destroy(); + + var justReleased:FlxActionDigital = controls.byName.get(name+"_R"); + if (justReleased != null) justReleased.destroy(); + + controls.byName.remove(name); + controls.byName.remove(name+"_P"); + controls.byName.remove(name+"_R"); + } + } + + + private static inline function checkControl(controls:Controls, name:String) { + var control = getControl(controls, name); + if (control != null) return control.check(); + return false; + } + + public static inline function getControl(controls:Controls, name:String) : FlxActionDigital { + return controls.byName.get(name); + } + + public static inline function getJustPressed(controls:Controls, name:String) { + return checkControl(controls, name + "_P"); + } + public static inline function getJustReleased(controls:Controls, name:String) { + return checkControl(controls, name + "_R"); + } + public static inline function getPressed(controls:Controls, name:String) { + return checkControl(controls, name); + } + + public static function loadCustomControls() { + var xmlPath = Paths.xml("config/controls"); + for(source in [funkin.backend.assets.AssetsLibraryList.AssetSource.SOURCE, funkin.backend.assets.AssetsLibraryList.AssetSource.MODS]) { + if (Paths.assetsTree.existsSpecific(xmlPath, "TEXT", source)) { + var access:Access = null; + try { + access = new Access(Xml.parse(Paths.assetsTree.getSpecificAsset(xmlPath, "TEXT", source)).firstElement()); + } catch(e) { + Logs.trace('Error while parsing controls.xml: ${Std.string(e)}', ERROR); + } + + if (access != null) { + parseControlsXml(access); + } + } + } + } + + private static inline function parseControlsXml(xml:Access) { + for (category in xml.elements) { + for (control in category.elements) { + if (!control.has.name) { + continue; + } + + var name = control.getAtt("name"); + var saveName = control.getAtt("saveName").getDefault(""); + var defaultKeyP1 = control.getAtt("keyP1").getDefault(""); + var defaultKeyP2 = control.getAtt("keyP2").getDefault(""); + + var keyP1:Null = 0; + var keyP2:Null = 0; + + ControlsUtil.initCustomControl(PlayerSettings.solo.controls, name); + ControlsUtil.initCustomControl(PlayerSettings.player1.controls, name); + ControlsUtil.initCustomControl(PlayerSettings.player2.controls, name); + + if (saveName != "") { + var p1:Null = Reflect.getProperty(FlxG.save.data, "P1_"+saveName); + var p2:Null = Reflect.getProperty(FlxG.save.data, "P2_"+saveName); + + //check for save data (and load default if it doesnt exist) + if (p1 != null) { + keyP1 = p1; + } else { + if (defaultKeyP1 != "") keyP1 = FlxKey.fromString(defaultKeyP1); + Reflect.setProperty(FlxG.save.data, "P1_"+saveName, keyP1); + } + + if (p2 != null) { + keyP2 = p2; + } else { + if (defaultKeyP2 != "") keyP2 = FlxKey.fromString(defaultKeyP2); + Reflect.setProperty(FlxG.save.data, "P2_"+saveName, keyP2); + } + + } else { + //if no save data just load the default + if (defaultKeyP1 != "") keyP1 = FlxKey.fromString(defaultKeyP1); + if (defaultKeyP2 != "") keyP2 = FlxKey.fromString(defaultKeyP2); + } + + ControlsUtil.addKeysToCustomControl(PlayerSettings.solo.controls, name, [keyP1, keyP2]); + ControlsUtil.addKeysToCustomControl(PlayerSettings.player1.controls, name, [keyP1, 0]); + ControlsUtil.addKeysToCustomControl(PlayerSettings.player2.controls, name, [0, keyP2]); + } + } + } +} \ No newline at end of file diff --git a/source/funkin/game/Strum.hx b/source/funkin/game/Strum.hx index 41bf72b5..4bc8091e 100644 --- a/source/funkin/game/Strum.hx +++ b/source/funkin/game/Strum.hx @@ -26,7 +26,7 @@ class Strum extends FlxSprite { public var getJustReleased:StrumLine->Bool = null; public inline function __getPressed(strumLine:StrumLine):Bool { - return getPressed != null ? getPressed(strumLine) : switch(ID) { + return getPressed != null ? getPressed(strumLine) : strumLine.members.length != 4 ? ControlsUtil.getPressed(strumLine.controls, strumLine.members.length+"k"+ID) : switch(ID) { case 0: strumLine.controls.NOTE_LEFT; case 1: strumLine.controls.NOTE_DOWN; case 2: strumLine.controls.NOTE_UP; @@ -35,7 +35,7 @@ class Strum extends FlxSprite { } } public inline function __getJustPressed(strumLine:StrumLine) { - return getJustPressed != null ? getJustPressed(strumLine) : switch(ID) { + return getJustPressed != null ? getJustPressed(strumLine) : strumLine.members.length != 4 ? ControlsUtil.getJustPressed(strumLine.controls, strumLine.members.length+"k"+ID) : switch(ID) { case 0: strumLine.controls.NOTE_LEFT_P; case 1: strumLine.controls.NOTE_DOWN_P; case 2: strumLine.controls.NOTE_UP_P; @@ -44,7 +44,7 @@ class Strum extends FlxSprite { } } public inline function __getJustReleased(strumLine:StrumLine) { - return getJustReleased != null ? getJustReleased(strumLine) : switch(ID) { + return getJustReleased != null ? getJustReleased(strumLine) : strumLine.members.length != 4 ? ControlsUtil.getJustReleased(strumLine.controls, strumLine.members.length+"k"+ID) : switch(ID) { case 0: strumLine.controls.NOTE_LEFT_R; case 1: strumLine.controls.NOTE_DOWN_R; case 2: strumLine.controls.NOTE_UP_R; diff --git a/source/funkin/options/keybinds/KeybindSetting.hx b/source/funkin/options/keybinds/KeybindSetting.hx index 8201f013..5250fbc5 100644 --- a/source/funkin/options/keybinds/KeybindSetting.hx +++ b/source/funkin/options/keybinds/KeybindSetting.hx @@ -15,16 +15,19 @@ class KeybindSetting extends FlxTypedSpriteGroup { public var p2Selected:Bool = false; public var value:String; + public var custom:Bool = false; + public var option1:Null; public var option2:Null; - public function new(x:Float, y:Float, name:String, value:String, ?sparrowIcon:String, ?sparrowAnim:String) { + public function new(x:Float, y:Float, name:String, value:String, ?sparrowIcon:String, ?sparrowAnim:String, ?custom:Bool = false) { super(); this.value = value; + this.custom = custom; title = new Alphabet(0, 0, name, true); add(title); - var controlArrayP1:Array = Reflect.field(Options, 'P1_${value}'); - var controlArrayP2:Array = Reflect.field(Options, 'P2_${value}'); + var controlArrayP1:Array = custom ? [Reflect.getProperty(FlxG.save.data, 'P1_${value}')] : Reflect.field(Options, 'P1_${value}'); + var controlArrayP2:Array = custom ? [Reflect.getProperty(FlxG.save.data, 'P2_${value}')] : Reflect.field(Options, 'P2_${value}'); option1 = controlArrayP1[0]; option2 = controlArrayP2[0]; @@ -76,19 +79,35 @@ class KeybindSetting extends FlxTypedSpriteGroup { KeybindsOptions.instance.openSubState(new ChangeKeybindSubState(function(key:FlxKey) { flicker.stop(); flicker.destroy(); - if (p2) - Reflect.setField(Options, 'P2_$value', [option2 = key]); - else - Reflect.setField(Options, 'P1_$value', [option1 = key]); + if (!custom) { + if (p2) + Reflect.setField(Options, 'P2_$value', [option2 = key]); + else + Reflect.setField(Options, 'P1_$value', [option1 = key]); + } else { + if (p2) + Reflect.setProperty(FlxG.save.data, 'P2_$value', option2 = key); + else + Reflect.setProperty(FlxG.save.data, 'P1_$value', option1 = key); + } + updateText(); callback(); }, function() { flicker.stop(); flicker.destroy(); - if (p2) - Reflect.setField(Options, 'P2_$value', [option2 = 0]); - else - Reflect.setField(Options, 'P1_$value', [option1 = 0]); + if (!custom) { + if (p2) + Reflect.setField(Options, 'P2_$value', [option2 = 0]); + else + Reflect.setField(Options, 'P1_$value', [option1 = 0]); + } else { + if (p2) + Reflect.setProperty(FlxG.save.data, 'P2_$value', option2 = 0); + else + Reflect.setProperty(FlxG.save.data, 'P1_$value', option1 = 0); + } + updateText(); cancelCallback(); })); } diff --git a/source/funkin/options/keybinds/KeybindsOptions.hx b/source/funkin/options/keybinds/KeybindsOptions.hx index 90783215..afd552a0 100644 --- a/source/funkin/options/keybinds/KeybindsOptions.hx +++ b/source/funkin/options/keybinds/KeybindsOptions.hx @@ -1,13 +1,14 @@ package funkin.options.keybinds; import flixel.util.FlxColor; - +import haxe.xml.Access; using StringTools; + class KeybindsOptions extends MusicBeatSubstate { public static var instance:KeybindsOptions; - public var categories = [ + public var categories:Array = [ { name: 'Notes', settings: [ @@ -127,6 +128,9 @@ class KeybindsOptions extends MusicBeatSubstate { FlxG.camera.follow(camFollow, LOCKON, 0.125); } + var customCategories = loadCustomCategories(); + for (i in customCategories) categories.push(i); + var k:Int = 0; for(category in categories) { k++; @@ -152,8 +156,10 @@ class KeybindsOptions extends MusicBeatSubstate { }; e.name = e.name.substring(5, e.name.length - 1); } + if (e.sparrowIcon != null) sparrowIcon = e.sparrowIcon; + if (e.sparrowAnim != null) sparrowAnim = e.sparrowAnim; - var text = new KeybindSetting(100, k * 75, e.name, e.control, sparrowIcon, sparrowAnim); + var text = new KeybindSetting(100, k * 75, e.name, e.control, sparrowIcon, sparrowAnim, e.custom == null ? false : e.custom); if (!isSubState) text.bind1.color = text.bind2.color = FlxColor.BLACK; alphabets.add(text); @@ -199,7 +205,9 @@ class KeybindsOptions extends MusicBeatSubstate { close(); else FlxG.switchState(new OptionsMenu()); + ControlsUtil.resetCustomControls(); Options.applyKeybinds(); + ControlsUtil.loadCustomControls(); Options.save(); return; } @@ -248,4 +256,59 @@ class KeybindsOptions extends MusicBeatSubstate { camFollow.setPosition(FlxG.width / 2, FlxG.height / 2); } } + + public function loadCustomCategories() { + var customCategories:Array = []; + + var xmlPath = Paths.xml("config/controls"); + for(source in [funkin.backend.assets.AssetsLibraryList.AssetSource.SOURCE, funkin.backend.assets.AssetsLibraryList.AssetSource.MODS]) { + if (Paths.assetsTree.existsSpecific(xmlPath, "TEXT", source)) { + var access:Access = null; + try { + access = new Access(Xml.parse(Paths.assetsTree.getSpecificAsset(xmlPath, "TEXT", source)).firstElement()); + } catch(e) { + Logs.trace('Error while parsing controls.xml: ${Std.string(e)}', ERROR); + } + + if (access != null) { + for (category in access.elements) { + if (!category.has.name) continue; + + var cat:ControlsCategory = { + name: category.getAtt("name"), + settings: [] + }; + + for (control in category.elements) { + if (control.has.menuName && control.has.saveName) { + cat.settings.push({ + name: control.getAtt("menuName"), + control: control.getAtt("saveName"), + custom: true, + sparrowIcon: control.getAtt("menuIcon").getDefault(null), + sparrowAnim: control.getAtt("menuAnim").getDefault(null) + }); + } + } + + customCategories.push(cat); + } + } + } + } + return customCategories; + } +} + +typedef KeybindSettingData = { + var name:String; + var control:String; + var ?custom:Bool; + var ?sparrowIcon:String; + var ?sparrowAnim:String; +} + +typedef ControlsCategory = { + var name:String; + var settings:Array; } \ No newline at end of file