diff --git a/flixel/input/actions/FlxAction.hx b/flixel/input/actions/FlxAction.hx new file mode 100644 index 0000000000..14e2da85f9 --- /dev/null +++ b/flixel/input/actions/FlxAction.hx @@ -0,0 +1,484 @@ +package flixel.input.actions; + +import flixel.input.FlxInput.FlxInputState; +import flixel.input.IFlxInput; +import flixel.input.actions.FlxActionInput.FlxInputDeviceID; +import flixel.input.actions.FlxActionInput.FlxInputType; +import flixel.input.actions.FlxActionInputAnalog.FlxAnalogAxis; +import flixel.input.actions.FlxActionInputAnalog.FlxAnalogState; +import flixel.input.actions.FlxActionInputAnalog.FlxActionInputAnalogClickAndDragMouseMotion; +import flixel.input.actions.FlxActionInputAnalog.FlxActionInputAnalogGamepad; +import flixel.input.actions.FlxActionInputAnalog.FlxActionInputAnalogMouseMotion; +import flixel.input.actions.FlxActionInputAnalog.FlxActionInputAnalogMousePosition; +import flixel.input.actions.FlxActionInputDigital.FlxActionInputDigitalIFlxInput; +import flixel.input.actions.FlxActionInputDigital.FlxActionInputDigitalGamepad; +import flixel.input.actions.FlxActionInputDigital.FlxActionInputDigitalKeyboard; +import flixel.input.actions.FlxActionInputDigital.FlxActionInputDigitalMouse; +import flixel.input.actions.FlxActionInputDigital.FlxActionInputDigitalMouseWheel; +import flixel.input.actions.FlxActionInputDigital.FlxActionInputDigitalSteam; +import flixel.input.keyboard.FlxKey; +import flixel.input.mouse.FlxMouseButton.FlxMouseButtonID; +import flixel.input.gamepad.FlxGamepadInputID; +import flixel.util.FlxDestroyUtil; +import flixel.util.FlxDestroyUtil.IFlxDestroyable; + +#if FLX_STEAMWRAP +import steamwrap.api.Controller.EControllerActionOrigin; +#end + +using flixel.util.FlxArrayUtil; + +/** + * A digital action is a binary on/off event like "jump" or "fire". + * FlxActions let you attach multiple inputs to a single in-game action, + * so "jump" could be performed by a keyboard press, a mouse click, + * or a gamepad button press. + */ +class FlxActionDigital extends FlxAction +{ + /** + * Function to call when this action occurs + */ + public var callback:FlxActionDigital->Void; + + /** + * Create a new digital action + * @param Name name of the action + * @param Callback function to call when this action occurs + */ + public function new(?Name:String="", ?Callback:FlxActionDigital->Void) + { + super(FlxInputType.DIGITAL, Name); + callback = Callback; + } + + /** + * Add a digital input (any kind) that will trigger this action + * @param input + * @return This action + */ + public function add(input:FlxActionInputDigital):FlxActionDigital + { + addGenericInput(input); + return this; + } + + /** + * Add a generic IFlxInput action input + * + * WARNING: IFlxInput objects are often member variables of some other + * object that is often destructed at the end of a state. If you don't + * destroy() this input (or the action you assign it to), the IFlxInput + * reference will persist forever even after its parent object has been + * destroyed! + * + * @param Input A generic IFlxInput object (ex: FlxButton.input) + * @param Trigger Trigger What state triggers this action (PRESSED, JUST_PRESSED, RELEASED, JUST_RELEASED) + * @return This action + */ + public function addInput(Input:IFlxInput, Trigger:FlxInputState):FlxActionDigital + { + return add(new FlxActionInputDigitalIFlxInput(Input, Trigger)); + } + + /** + * Add a gamepad action input for digital (button-like) events + * @param InputID "universal" gamepad input ID (A, X, DPAD_LEFT, etc) + * @param Trigger What state triggers this action (PRESSED, JUST_PRESSED, RELEASED, JUST_RELEASED) + * @param GamepadID specific gamepad ID, or FlxInputDeviceID.ALL / FIRST_ACTIVE + * @return This action + */ + public function addGamepad(InputID:FlxGamepadInputID, Trigger:FlxInputState, GamepadID:Int = FlxInputDeviceID.FIRST_ACTIVE):FlxActionDigital + { + return add(new FlxActionInputDigitalGamepad(InputID, Trigger, GamepadID)); + } + + /** + * Add a keyboard action input + * @param Key Key identifier (FlxKey.SPACE, FlxKey.Z, etc) + * @param Trigger What state triggers this action (PRESSED, JUST_PRESSED, RELEASED, JUST_RELEASED) + * @return This action + */ + public function addKey(Key:FlxKey, Trigger:FlxInputState):FlxActionDigital + { + return add(new FlxActionInputDigitalKeyboard(Key, Trigger)); + } + + /** + * Mouse button action input + * @param ButtonID Button identifier (FlxMouseButtonID.LEFT / MIDDLE / RIGHT) + * @param Trigger What state triggers this action (PRESSED, JUST_PRESSED, RELEASED, JUST_RELEASED) + * @return This action + */ + public function addMouse(ButtonID:FlxMouseButtonID, Trigger:FlxInputState):FlxActionDigital + { + return add(new FlxActionInputDigitalMouse(ButtonID, Trigger)); + } + + /** + * Action for mouse wheel events + * @param Positive True: respond to mouse wheel values > 0; False: respond to mouse wheel values < 0 + * @param Trigger What state triggers this action (PRESSED, JUST_PRESSED, RELEASED, JUST_RELEASED) + * @return This action + */ + public function addMouseWheel(Positive:Bool, Trigger:FlxInputState):FlxActionDigital + { + return add(new FlxActionInputDigitalMouseWheel(Positive, Trigger)); + } + + override public function destroy():Void + { + callback = null; + super.destroy(); + } + + override public function check():Bool + { + var val = super.check(); + if (val && callback != null) + { + callback(this); + } + return val; + } +} + +/** + * Analog actions are events with continuous (floating-point) values, and up + * to two axes (x,y). This is for events like "move" and "accelerate" where the + * event is not simply on or off. + * + * FlxActions let you attach multiple inputs to a single in-game action, + * so "move" could be performed by a gamepad joystick, a mouse movement, etc. + */ +class FlxActionAnalog extends FlxAction +{ + /** + * Function to call when this action occurs + */ + public var callback:FlxActionAnalog->Void; + + /** + * X axis value, or the value of a single-axis analog input. + */ + public var x(get, null):Float; + + /** + * Y axis value. (If action only has single-axis input this is always == 0) + */ + public var y(get, null):Float; + + /** + * Create a new analog action + * @param Name name of the action + * @param Callback function to call when this action occurs + */ + public function new(?Name:String="", ?Callback:FlxActionAnalog->Void) + { + super(FlxInputType.ANALOG, Name); + callback = Callback; + } + + /** + * Add an analog input that will trigger this action + */ + public function add(input:FlxActionInputAnalog):FlxActionAnalog + { + addGenericInput(input); + return this; + } + + /** + * Add mouse input -- same as mouse motion, but requires a particular mouse button to be PRESSED + * Very useful for e.g. panning a map or canvas around + * @param ButtonID Button identifier (FlxMouseButtonID.LEFT / MIDDLE / RIGHT) + * @param Trigger What state triggers this action (MOVED, JUST_MOVED, STOPPED, JUST_STOPPED) + * @param Axis which axes to monitor for triggering: X, Y, EITHER, or BOTH + * @param PixelsPerUnit How many pixels of movement = 1.0 in analog motion (lower: more sensitive, higher: less sensitive) + * @param DeadZone Minimum analog value before motion will be reported + * @param InvertY Invert the Y axis + * @param InvertX Invert the X axis + * @return This action + */ + public function addMouseClickAndDragMotion(ButtonID:FlxMouseButtonID, Trigger:FlxAnalogState, Axis:FlxAnalogAxis = FlxAnalogAxis.EITHER, PixelsPerUnit:Int = 10, DeadZone:Float = 0.1, InvertY:Bool = false, InvertX:Bool = false):FlxActionAnalog + { + return add(new FlxActionInputAnalogClickAndDragMouseMotion(ButtonID, Trigger, Axis, PixelsPerUnit, DeadZone, InvertY, InvertX)); + } + + /** + * Add mouse input -- X/Y is the RELATIVE motion of the mouse since the last frame + * @param Trigger What state triggers this action (MOVED, JUST_MOVED, STOPPED, JUST_STOPPED) + * @param Axis which axes to monitor for triggering: X, Y, EITHER, or BOTH + * @param PixelsPerUnit How many pixels of movement = 1.0 in analog motion (lower: more sensitive, higher: less sensitive) + * @param DeadZone Minimum analog value before motion will be reported + * @param InvertY Invert the Y axis + * @param InvertX Invert the X axis + * @return This action + */ + public function addMouseMotion(Trigger:FlxAnalogState, Axis:FlxAnalogAxis = EITHER, PixelsPerUnit:Int = 10, DeadZone:Float = 0.1, InvertY:Bool = false, InvertX:Bool = false):FlxActionAnalog + { + return add(new FlxActionInputAnalogMouseMotion(Trigger, Axis, PixelsPerUnit, DeadZone, InvertY, InvertX)); + } + + /** + * Add mouse input -- X/Y is the mouse's absolute screen position + * @param Trigger What state triggers this action (MOVED, JUST_MOVED, STOPPED, JUST_STOPPED) + * @param Axis which axes to monitor for triggering: X, Y, EITHER, or BOTH + * @return This action + */ + public function addMousePosition(Trigger:FlxAnalogState, Axis:FlxAnalogAxis = EITHER):FlxActionAnalog + { + return add(new FlxActionInputAnalogMousePosition(Trigger, Axis)); + } + + /** + * Add gamepad action input for analog (trigger, joystick, touchpad, etc) events + * @param InputID "universal" gamepad input ID (LEFT_TRIGGER, RIGHT_ANALOG_STICK, TILT_PITCH, etc) + * @param Trigger What state triggers this action (MOVED, JUST_MOVED, STOPPED, JUST_STOPPED) + * @param Axis which axes to monitor for triggering: X, Y, EITHER, or BOTH + * @param GamepadID specific gamepad ID, or FlxInputDeviceID.FIRST_ACTIVE / ALL + * @return This action + */ + public function addGamepad(InputID:FlxGamepadInputID, Trigger:FlxAnalogState, Axis:FlxAnalogAxis = EITHER, GamepadID:Int = FlxInputDeviceID.FIRST_ACTIVE):FlxActionAnalog + { + return add(new FlxActionInputAnalogGamepad(InputID, Trigger, Axis, GamepadID)); + } + + override public function update():Void + { + _x = null; + _y = null; + super.update(); + } + + override public function destroy():Void + { + callback = null; + super.destroy(); + } + + override public function toString():String + { + return "FlxAction(" + type + ") name:" + name + " x/y:" + _x + "," + _y; + } + + override public function check():Bool + { + var val = super.check(); + if (val && callback != null) + { + callback(this); + } + return val; + } + + private function get_x():Float + { + (_x != null) ? return _x : return 0; + } + + private function get_y():Float + { + (_y != null) ? return _y : return 0; + } +} + +@:allow(flixel.input.actions.FlxActionDigital, flixel.input.actions.FlxActionAnalog, flixel.input.actions.FlxActionSet) +class FlxAction implements IFlxDestroyable +{ + /** + * Digital or Analog + */ + public var type(default, null):FlxInputType; + + /** + * The name of the action, "jump", "fire", "move", etc. + */ + public var name(default, null):String; + + /** + * This action's numeric handle for the Steam API (ignored if not using Steam) + */ + private var steamHandle(default, null):Int = -1; + + /** + * If true, this action has just been triggered + */ + public var triggered(default, null):Bool = false; + + /** + * The inputs attached to this action + */ + public var inputs:Array; + + private var _x:Null = null; + private var _y:Null = null; + + private var _timestamp:Int = 0; + private var _checked:Bool = false; + + /** + * Whether the steam controller inputs for this action have changed since the last time origins were polled. Always false if steam isn't active + */ + public var steamOriginsChanged(default, null):Bool = false; + + #if FLX_STEAMWRAP + private var _steamOriginsChecksum:Int = 0; + private var _steamOrigins:Array; + #end + + private function new(InputType:FlxInputType, Name:String) + { + type = InputType; + name = Name; + inputs = []; + #if FLX_STEAMWRAP + _steamOrigins = []; + for (i in 0...FlxSteamController.MAX_ORIGINS) + { + _steamOrigins.push(cast 0); + } + #end + } + + public function getFirstSteamOrigin():Int + { + #if FLX_STEAMWRAP + if (_steamOrigins == null) return 0; + for (i in 0..._steamOrigins.length) + { + if (_steamOrigins[i] != EControllerActionOrigin.NONE) + { + return cast _steamOrigins[i]; + } + } + #end + return 0; + } + + public function getSteamOrigins(?origins:Array):Array + { + #if FLX_STEAMWRAP + if (origins == null) + { + origins = []; + } + if (_steamOrigins != null) + { + for (i in 0..._steamOrigins.length) + { + origins[i] = cast _steamOrigins[i]; + } + } + #end + return origins; + } + + public function removeAll(Destroy:Bool = true):Void + { + var len = inputs.length; + for (i in 0...len) + { + var j = len - i - 1; + var input = inputs[j]; + remove(input, Destroy); + inputs.splice(j, 1); + } + } + + public function remove(Input:FlxActionInput, Destroy:Bool = false):Void + { + if (Input == null) return; + inputs.remove(Input); + if (Destroy) + { + Input.destroy(); + } + } + + public function toString():String + { + return("FlxAction(" + type + ") name:" + name); + } + + /** + * See if this action has just been triggered + */ + public function check():Bool + { + _x = null; + _y = null; + + if (_timestamp == FlxG.game.ticks) + { + triggered = _checked; + return _checked; //run no more than once per frame + } + + _timestamp = FlxG.game.ticks; + _checked = false; + + var len = inputs != null ? inputs.length : 0; + for (i in 0...len) + { + var j = len - i - 1; + var input = inputs[j]; + + if (input.destroyed) + { + inputs.splice(j, 1); + continue; + } + + input.update(); + + if (input.check(this)) + { + _checked = true; + } + } + + triggered = _checked; + return _checked; + } + + /** + * Check input states & fire callbacks if anything is triggered + */ + public function update():Void + { + check(); + } + + public function destroy():Void + { + FlxDestroyUtil.destroyArray(inputs); + inputs = null; + #if FLX_STEAMWRAP + FlxArrayUtil.clearArray(_steamOrigins); + _steamOrigins = null; + #end + } + + public function match(other:FlxAction):Bool + { + return name == other.name && steamHandle == other.steamHandle; + } + + private function addGenericInput(input:FlxActionInput):FlxAction + { + if (inputs == null) + { + inputs = []; + } + if (!checkExists(input)) inputs.push(input); + + return this; + } + + private function checkExists(input:FlxActionInput):Bool + { + if (inputs == null) return false; + return inputs.contains(input); + } +} \ No newline at end of file diff --git a/flixel/input/actions/FlxActionInput.hx b/flixel/input/actions/FlxActionInput.hx new file mode 100644 index 0000000000..00c2397389 --- /dev/null +++ b/flixel/input/actions/FlxActionInput.hx @@ -0,0 +1,140 @@ +package flixel.input.actions; + +import flixel.input.FlxInput.FlxInputState; +import flixel.util.FlxDestroyUtil.IFlxDestroyable; + +@:allow(flixel.input.actions.FlxActionInputDigital, flixel.input.actions.FlxActionInputAnalog) +class FlxActionInput implements IFlxDestroyable +{ + /** + * Digital or Analog + */ + public var type:FlxInputType; + + /** + * Mouse, Keyboard, Gamepad, SteamController, etc. + */ + public var device:FlxInputDevice; + + /** + * Gamepad ID or Steam Controller handle (ignored for Mouse & Keyboard) + */ + public var deviceID:Int; + + public var destroyed(default, null):Bool = false; + + /** + * Input code (FlxMouseButtonID, FlxKey, FlxGamepadInputID, or Steam Controller action handle) + */ + public var inputID(default, null):Int; + + /** + * What state triggers this action (PRESSED, JUST_PRESSED, RELEASED, JUST_RELEASED) + */ + public var trigger(default, null):FlxInputState; + + function new(InputType:FlxInputType, Device:FlxInputDevice, InputID:Int, Trigger:FlxInputState, DeviceID:Int = FlxInputDeviceID.FIRST_ACTIVE) + { + type = InputType; + device = Device; + inputID = InputID; + trigger = Trigger; + deviceID = DeviceID; + } + + public function update():Void {} + + public function destroy():Void + { + destroyed = true; + } + + /** + * Check whether this action has just been triggered + */ + public function check(action:FlxAction):Bool + { + return false; + } + + /** + * Check whether `state` fulfills `condition`. Note: order of operations is + * important here. `compareState(JUST_PRESSED, PRESSED) == false`, while + * `compareState(PRESSED, JUST_PRESSED) == true`. + * @return Whether or not the condition is satisfied by state. + */ + inline function compareState(condition:FlxInputState, state:FlxInputState):Bool + { + return switch (condition) + { + case PRESSED: state == PRESSED || state == JUST_PRESSED; + case RELEASED: state == RELEASED || state == JUST_RELEASED; + case JUST_PRESSED: state == JUST_PRESSED; + case JUST_RELEASED: state == JUST_RELEASED; + default: false; + } + } +} + +enum FlxInputType +{ + DIGITAL; + ANALOG; +} + +enum FlxInputDevice +{ + UNKNOWN; + MOUSE; + MOUSE_WHEEL; + KEYBOARD; + GAMEPAD; + STEAM_CONTROLLER; + IFLXINPUT_OBJECT; + OTHER; + ALL; + NONE; +} + +/** + * Just a bucket for some handy sentinel values. + */ +class FlxInputDeviceID +{ + /** + * Means "every connected device of the given type" (ie all gamepads, all steam controllers, etc) + */ + public static inline var ALL:Int = -1; + + /** + * Means "the first connected device that has an active input" (ie a pressed button or moved analog stick/trigger/etc) + */ + public static inline var FIRST_ACTIVE:Int = -2; + + /** + * Means "no device" + */ + public static inline var NONE:Int = -3; +} + +/** + * Just a bucket for being able to refer to a specific device by type & slot number + */ +class FlxInputDeviceObject +{ + public var device:FlxInputDevice; + public var id:Int; + public var model:String; + + public function new(Device:FlxInputDevice, ID:Int, Model:String = "") + { + device = Device; + id = ID; + model = Model; + } + + public function toString():String + { + return "{device:" + device + ",id:" + id + ",model:" + model + "}"; + } +} \ No newline at end of file diff --git a/flixel/input/actions/FlxActionInputAnalog.hx b/flixel/input/actions/FlxActionInputAnalog.hx new file mode 100644 index 0000000000..cf18b074de --- /dev/null +++ b/flixel/input/actions/FlxActionInputAnalog.hx @@ -0,0 +1,375 @@ +package flixel.input.actions; + +import flixel.input.FlxInput; +import flixel.input.actions.FlxActionInput.FlxInputType; +import flixel.input.actions.FlxActionInput.FlxInputDevice; +import flixel.input.actions.FlxActionInput.FlxInputDeviceID; +import flixel.input.gamepad.FlxGamepad; +import flixel.input.gamepad.FlxGamepadInputID; +import flixel.input.mouse.FlxMouseButton.FlxMouseButtonID; + +#if FLX_STEAMWRAP +import steamwrap.api.Controller.ControllerAnalogActionData; +#end + +@:enum +abstract FlxAnalogState(Int) from Int +{ + var JUST_STOPPED = cast FlxInputState.JUST_RELEASED; // became 0 on this frame + var STOPPED = cast FlxInputState.RELEASED; // is 0 + var MOVED = cast FlxInputState.PRESSED; // is !0 + var JUST_MOVED = cast FlxInputState.JUST_PRESSED; // became !0 on this frame +} + +class FlxActionInputAnalogClickAndDragMouseMotion extends FlxActionInputAnalogMouseMotion +{ + var button:FlxMouseButtonID; + + /** + * Mouse input -- same as FlxActionInputAnalogMouseMotion, but requires a particular mouse button to be PRESSED + * Very useful for e.g. panning a map or canvas around + * @param ButtonID Button identifier (FlxMouseButtonID.LEFT / MIDDLE / RIGHT) + * @param Trigger What state triggers this action (MOVED, JUST_MOVED, STOPPED, JUST_STOPPED) + * @param Axis which axes to monitor for triggering: X, Y, EITHER, or BOTH + * @param PixelsPerUnit How many pixels of movement = 1.0 in analog motion (lower: more sensitive, higher: less sensitive) + * @param DeadZone Minimum analog value before motion will be reported + * @param InvertY Invert the Y axis + * @param InvertX Invert the X axis + */ + public function new(ButtonID:FlxMouseButtonID, Trigger:FlxAnalogState, Axis:FlxAnalogAxis = FlxAnalogAxis.EITHER, PixelsPerUnit:Int = 10, DeadZone:Float = 0.1, InvertY:Bool = false, InvertX:Bool = false) + { + super(Trigger, Axis, PixelsPerUnit, DeadZone, InvertY, InvertX); + button = ButtonID; + } + + override function updateValues(X:Float, Y:Float):Void + { + var pass = false; + #if !FLX_NO_MOUSE + pass = switch (button) + { + case FlxMouseButtonID.LEFT: FlxG.mouse.pressed; + case FlxMouseButtonID.RIGHT: FlxG.mouse.pressedRight; + case FlxMouseButtonID.MIDDLE: FlxG.mouse.pressedMiddle; + } + #end + if (!pass) + { + X = 0; + Y = 0; + } + super.updateValues(X, Y); + + } +} + +class FlxActionInputAnalogMouseMotion extends FlxActionInputAnalog +{ + var lastX:Float = 0; + var lastY:Float = 0; + var pixelsPerUnit:Int; + var deadZone:Float; + var invertX:Bool; + var invertY:Bool; + + /** + * Mouse input -- X/Y is the RELATIVE motion of the mouse since the last frame + * @param Trigger What state triggers this action (MOVED, JUST_MOVED, STOPPED, JUST_STOPPED) + * @param Axis which axes to monitor for triggering: X, Y, EITHER, or BOTH + * @param PixelsPerUnit How many pixels of movement = 1.0 in analog motion (lower: more sensitive, higher: less sensitive) + * @param DeadZone Minimum analog value before motion will be reported + * @param InvertY Invert the Y axis + * @param InvertX Invert the X axis + */ + public function new(Trigger:FlxAnalogState, Axis:FlxAnalogAxis = EITHER, PixelsPerUnit:Int = 10, DeadZone:Float = 0.1, InvertY:Bool = false, InvertX:Bool = false) + { + pixelsPerUnit = PixelsPerUnit; + if (pixelsPerUnit < 1) + pixelsPerUnit = 1; + deadZone = DeadZone; + invertX = InvertX; + invertY = InvertY; + super(FlxInputDevice.MOUSE, -1, cast Trigger, Axis); + } + + override public function update():Void + { + #if !FLX_NO_MOUSE + updateXYPosition(FlxG.mouse.x, FlxG.mouse.y); + #end + } + + function updateXYPosition(X:Float, Y:Float):Void + { + var xDiff = X - lastX; + var yDiff = Y - lastY; + + lastX = X; + lastY = Y; + + if (invertX) xDiff *= -1; + if (invertY) yDiff *= -1; + + xDiff /= (pixelsPerUnit); + yDiff /= (pixelsPerUnit); + + if (Math.abs(xDiff) < deadZone) xDiff = 0; + if (Math.abs(yDiff) < deadZone) yDiff = 0; + + updateValues(xDiff, yDiff); + } +} + +class FlxActionInputAnalogMousePosition extends FlxActionInputAnalog +{ + /** + * Mouse input -- X/Y is the mouse's absolute screen position + * @param Trigger What state triggers this action (MOVED, JUST_MOVED, STOPPED, JUST_STOPPED) + * @param Axis which axes to monitor for triggering: X, Y, EITHER, or BOTH + */ + public function new(Trigger:FlxAnalogState, Axis:FlxAnalogAxis = EITHER) + { + super(FlxInputDevice.MOUSE, -1, cast Trigger, Axis); + } + + override public function update():Void + { + #if !FLX_NO_MOUSE + updateValues(FlxG.mouse.x, FlxG.mouse.y); + #end + } + + override function updateValues(X:Float, Y:Float):Void + { + if (X != x) + { + xMoved.press(); + } + else + { + xMoved.release(); + } + + if (Y != y) + { + yMoved.press(); + } + else + { + yMoved.release(); + } + + x = X; + y = Y; + } +} + +class FlxActionInputAnalogGamepad extends FlxActionInputAnalog +{ + /** + * Gamepad action input for analog (trigger, joystick, touchpad, etc) events + * @param InputID "universal" gamepad input ID (LEFT_TRIGGER, RIGHT_ANALOG_STICK, TILT_PITCH, etc) + * @param Trigger What state triggers this action (MOVED, JUST_MOVED, STOPPED, JUST_STOPPED) + * @param Axis which axes to monitor for triggering: X, Y, EITHER, or BOTH + * @param GamepadID specific gamepad ID, or FlxInputDeviceID.FIRST_ACTIVE / ALL + */ + public function new(InputID:FlxGamepadInputID, Trigger:FlxAnalogState, Axis:FlxAnalogAxis = EITHER, GamepadID:Int = FlxInputDeviceID.FIRST_ACTIVE) + { + super(FlxInputDevice.GAMEPAD, InputID, cast Trigger, Axis, GamepadID); + } + + override public function update():Void + { + if (deviceID == FlxInputDeviceID.ALL) + { + return; //analog data is only meaningful on an individual device + } + + #if !FLX_NO_GAMEPAD + var gamepad:FlxGamepad = null; + + if (deviceID == FlxInputDeviceID.FIRST_ACTIVE) + { + gamepad = FlxG.gamepads.getFirstActiveGamepad(); + } + else if (deviceID >= 0) + { + gamepad = FlxG.gamepads.getByID(deviceID); + } + + if (gamepad != null) + { + switch (inputID) + { + case FlxGamepadInputID.LEFT_ANALOG_STICK: + updateValues(gamepad.analog.value.LEFT_STICK_X, gamepad.analog.value.LEFT_STICK_Y); + + case FlxGamepadInputID.RIGHT_ANALOG_STICK: + updateValues(gamepad.analog.value.RIGHT_STICK_X, gamepad.analog.value.RIGHT_STICK_Y); + + case FlxGamepadInputID.LEFT_TRIGGER: + updateValues(gamepad.analog.value.LEFT_TRIGGER, 0); + + case FlxGamepadInputID.RIGHT_TRIGGER: + updateValues(gamepad.analog.value.RIGHT_TRIGGER, 0); + + case FlxGamepadInputID.POINTER_X: + updateValues(gamepad.analog.value.POINTER_X, 0); + + case FlxGamepadInputID.POINTER_Y: + updateValues(gamepad.analog.value.POINTER_Y, 0); + + case FlxGamepadInputID.DPAD: + updateValues( + gamepad.pressed.DPAD_LEFT ? -1.0 : gamepad.pressed.DPAD_RIGHT ? 1.0 : 0.0, + gamepad.pressed.DPAD_UP ? -1.0 : gamepad.pressed.DPAD_DOWN ? 1.0 : 0.0 + ); + + } + } + else + { + updateValues(0, 0); + } + #end + } +} + +class FlxActionInputAnalogSteam extends FlxActionInputAnalog +{ + /** + * Steam Controller action input for analog (trigger, joystick, touchpad, etc) events + * @param ActionHandle handle received from FlxSteamController.getAnalogActionHandle() + * @param Trigger what state triggers this action (MOVING, JUST_MOVED, STOPPED, JUST_STOPPED) + * @param Axis which axes to monitor for triggering: X, Y, EITHER, or BOTH + * @param DeviceHandle handle received from FlxSteamController.getConnectedControllers(), or FlxInputDeviceID.ALL / FlxInputDeviceID.FIRST_ACTIVE + */ + @:allow(flixel.input.actions.FlxActionSet) + function new(ActionHandle:Int, Trigger:FlxAnalogState, Axis:FlxAnalogAxis = EITHER, DeviceID:Int = FlxInputDeviceID.ALL) + { + super(FlxInputDevice.STEAM_CONTROLLER, ActionHandle, cast Trigger, Axis, DeviceID); + #if FLX_NO_STEAM + FlxG.log.warn("steamwrap library not installed; steam inputs will be ignored."); + #end + } + + override public function update():Void + { + #if FLX_STEAMWRAP + var handle = deviceID; + if (handle == FlxInputDeviceID.NONE) + { + return; + } + else if (deviceID == FlxInputDeviceID.FIRST_ACTIVE) + { + handle = FlxSteamController.getFirstActiveHandle(); + } + + analogActionData = FlxSteamController.getAnalogActionData(handle, inputID, analogActionData); + updateValues(analogActionData.x, analogActionData.y); + #end + } + + #if FLX_STEAMWRAP + private static var analogActionData:ControllerAnalogActionData = new ControllerAnalogActionData(); + #end +} + +@:access(flixel.input.actions.FlxAction) +class FlxActionInputAnalog extends FlxActionInput +{ + public var axis(default, null):FlxAnalogAxis; + + public var x(default, null):Float = 0; + public var y(default, null):Float = 0; + public var xMoved(default, null):FlxInput; + public var yMoved(default, null):FlxInput; + + static inline var A_X = true; + static inline var A_Y = false; + + function new (Device:FlxInputDevice, InputID:Int, Trigger:FlxInputState, Axis:FlxAnalogAxis = EITHER, DeviceID:Int = FlxInputDeviceID.FIRST_ACTIVE) + { + super(FlxInputType.ANALOG, Device, InputID, Trigger, DeviceID); + axis = Axis; + xMoved = new FlxInput(0); + yMoved = new FlxInput(1); + } + + override public function check(Action:FlxAction):Bool + { + var returnVal = switch (axis) + { + case X: compareState(trigger, xMoved.current); + case Y: compareState(trigger, yMoved.current); + case BOTH: compareState(trigger, xMoved.current) && compareState(trigger, yMoved.current); + //in practice, "both pressed" and "both released" could be useful, whereas + //"both just pressed" and "both just released" seem like very unlikely real-world events + case EITHER: + switch (trigger) + { + case PRESSED: + checkAxis(A_X, PRESSED) || checkAxis(A_Y, PRESSED); //either one pressed + case RELEASED: + checkAxis(A_X, RELEASED) || checkAxis(A_Y, RELEASED); //either one NOT pressed + + case JUST_PRESSED: + (checkAxis(A_X, JUST_PRESSED) && checkAxis(A_Y, JUST_PRESSED)) || //both just pressed == whole stick just pressed + (checkAxis(A_X, JUST_PRESSED) && checkAxis(A_Y, RELEASED)) || //one just pressed & other NOT pressed == whole stick just pressed + (checkAxis(A_X, RELEASED) && checkAxis(A_Y, JUST_PRESSED)); + + case JUST_RELEASED: + (checkAxis(A_X, JUST_RELEASED) && checkAxis(A_Y, RELEASED)) || + (checkAxis(A_X, RELEASED) && checkAxis(A_Y, JUST_RELEASED)); //one just released & other NOT pressed = whole stick just released + } + } + + if (returnVal) + { + if (Action._x == null) Action._x = x; + if (Action._y == null) Action._y = y; + } + + return returnVal; + } + + function checkAxis(isX:Bool, state:FlxInputState):Bool + { + var input = isX ? xMoved : yMoved; + return compareState(state, input.current); + } + + function updateValues(X:Float, Y:Float):Void + { + if (X != 0) + { + xMoved.press(); + } + else + { + xMoved.release(); + } + + if (Y != 0) + { + yMoved.press(); + } + else + { + yMoved.release(); + } + + x = X; + y = Y; + } +} + +@:enum +abstract FlxAnalogAxis(Int) from Int +{ + var X = 0; + var Y = 1; + var BOTH = 2; + var EITHER = 3; +} \ No newline at end of file diff --git a/flixel/input/actions/FlxActionInputDigital.hx b/flixel/input/actions/FlxActionInputDigital.hx new file mode 100644 index 0000000000..f0e38fc2a6 --- /dev/null +++ b/flixel/input/actions/FlxActionInputDigital.hx @@ -0,0 +1,330 @@ +package flixel.input.actions; + +import flixel.input.FlxInput; +import flixel.input.IFlxInput; +import flixel.input.actions.FlxActionInput.FlxInputType; +import flixel.input.actions.FlxActionInput.FlxInputDevice; +import flixel.input.actions.FlxActionInput.FlxInputDeviceID; +import flixel.input.keyboard.FlxKey; +import flixel.input.mouse.FlxMouseButton.FlxMouseButtonID; +import flixel.input.gamepad.FlxGamepad; +import flixel.input.gamepad.FlxGamepadInputID; + +class FlxActionInputDigital extends FlxActionInput +{ + function new(Device:FlxInputDevice, InputID:Int, Trigger:FlxInputState, DeviceID:Int = FlxInputDeviceID.FIRST_ACTIVE) + { + super(FlxInputType.DIGITAL, Device, InputID, Trigger, DeviceID); + inputID = InputID; + } +} + +class FlxActionInputDigitalMouseWheel extends FlxActionInputDigital +{ + var input:FlxInput; + var sign:Int = 0; + + /** + * Action for mouse wheel events + * @param Positive True: respond to mouse wheel values > 0; False: respond to mouse wheel values < 0 + * @param Trigger What state triggers this action (PRESSED, JUST_PRESSED, RELEASED, JUST_RELEASED) + */ + public function new(Positive:Bool, Trigger:FlxInputState) + { + super(FlxInputDevice.MOUSE_WHEEL, 0, Trigger); + input = new FlxInput(0); + sign = Positive ? 1 : -1; + } + + override public function check(Action:FlxAction):Bool + { + return switch (trigger) + { + #if !FLX_NO_MOUSE + case PRESSED: return input.pressed || input.justPressed; + case RELEASED: return input.released || input.justReleased; + case JUST_PRESSED: return input.justPressed; + case JUST_RELEASED: return input.justReleased; + #end + default: false; + } + } + + override public function update():Void + { + super.update(); + #if !FLX_NO_MOUSE + if (FlxG.mouse.wheel * sign > 0) + { + input.press(); + } + else + { + input.release(); + } + #end + } +} + +class FlxActionInputDigitalGamepad extends FlxActionInputDigital +{ + var input:FlxInput; + + /** + * Gamepad action input for digital (button-like) events + * @param InputID "universal" gamepad input ID (A, X, DPAD_LEFT, etc) + * @param Trigger What state triggers this action (PRESSED, JUST_PRESSED, RELEASED, JUST_RELEASED) + * @param GamepadID specific gamepad ID, or FlxInputDeviceID.ALL / FIRST_ACTIVE + */ + public function new(InputID:FlxGamepadInputID, Trigger:FlxInputState, GamepadID:Int = FlxInputDeviceID.FIRST_ACTIVE) + { + super(FlxInputDevice.GAMEPAD, InputID, Trigger, GamepadID); + input = new FlxInput(InputID); + } + + public function toString():String + { + return "FlxActionInputDigitalGamepad{inputID:" + inputID + ",trigger:" + trigger + ",deviceID:" + deviceID + ",device:" + device + ",type:" + type + "}"; + } + + override public function update():Void + { + super.update(); + #if !FLX_NO_GAMEPAD + if (deviceID == FlxInputDeviceID.ALL) + { + if (FlxG.gamepads.anyPressed(inputID) || FlxG.gamepads.anyJustPressed(inputID)) + { + input.press(); + } + else + { + input.release(); + } + } + else + { + var gamepad:FlxGamepad = null; + + if (deviceID == FlxInputDeviceID.FIRST_ACTIVE) + { + gamepad = FlxG.gamepads.getFirstActiveGamepad(); + } + else if (deviceID >= 0) + { + gamepad = FlxG.gamepads.getByID(deviceID); + } + + if (gamepad != null) + { + if (inputID == ANY && trigger == RELEASED) + { + if (gamepad.released.ANY) + { + input.release(); + } + else + { + input.press(); + } + } + else + { + if (gamepad.checkStatus(inputID, PRESSED) || gamepad.checkStatus(inputID, JUST_PRESSED)) + { + input.press(); + } + else + { + input.release(); + } + } + } + else + { + if (deviceID == FlxInputDeviceID.FIRST_ACTIVE) + { + input.release(); + } + } + } + #end + } + + override public function check(Action:FlxAction):Bool + { + return switch (trigger) + { + case PRESSED: return input.pressed || input.justPressed; + case RELEASED: return input.released || input.justReleased; + case JUST_PRESSED: return input.justPressed; + case JUST_RELEASED: return input.justReleased; + default: false; + } + } +} + +class FlxActionInputDigitalKeyboard extends FlxActionInputDigital +{ + /** + * Keyboard action input + * @param Key Key identifier (FlxKey.SPACE, FlxKey.Z, etc) + * @param Trigger What state triggers this action (PRESSED, JUST_PRESSED, RELEASED, JUST_RELEASED) + */ + public function new(Key:FlxKey, Trigger:FlxInputState) + { + super(FlxInputDevice.KEYBOARD, Key, Trigger); + } + + override public function check(Action:FlxAction):Bool + { + return switch (trigger) + { + #if !FLX_NO_KEYBOARD + case PRESSED: FlxG.keys.checkStatus(inputID, PRESSED ) || FlxG.keys.checkStatus(inputID, JUST_PRESSED); + case RELEASED: FlxG.keys.checkStatus(inputID, RELEASED) || FlxG.keys.checkStatus(inputID, JUST_RELEASED); + case JUST_PRESSED: FlxG.keys.checkStatus(inputID, JUST_PRESSED); + case JUST_RELEASED: FlxG.keys.checkStatus(inputID, JUST_RELEASED); + #end + default: false; + } + } +} + +class FlxActionInputDigitalMouse extends FlxActionInputDigital +{ + /** + * Mouse button action input + * @param ButtonID Button identifier (FlxMouseButtonID.LEFT / MIDDLE / RIGHT) + * @param Trigger What state triggers this action (PRESSED, JUST_PRESSED, RELEASED, JUST_RELEASED) + */ + public function new(ButtonID:FlxMouseButtonID, Trigger:FlxInputState) + { + super(FlxInputDevice.MOUSE, ButtonID, Trigger); + } + + override public function check(Action:FlxAction):Bool + { + return switch (inputID) + { + #if !FLX_NO_MOUSE + case FlxMouseButtonID.LEFT : switch (trigger) + { + case PRESSED: FlxG.mouse.pressed || FlxG.mouse.justPressed; + case RELEASED: !FlxG.mouse.pressed || FlxG.mouse.justReleased; + case JUST_PRESSED: FlxG.mouse.justPressed; + case JUST_RELEASED: FlxG.mouse.justReleased; + } + case FlxMouseButtonID.MIDDLE: switch (trigger) + { + case PRESSED: FlxG.mouse.pressedMiddle || FlxG.mouse.justPressedMiddle; + case RELEASED: !FlxG.mouse.pressedMiddle || FlxG.mouse.justReleasedMiddle; + case JUST_PRESSED: FlxG.mouse.justPressedMiddle; + case JUST_RELEASED: FlxG.mouse.justReleasedMiddle; + } + case FlxMouseButtonID.RIGHT : switch (trigger) + { + case PRESSED: FlxG.mouse.pressedRight || FlxG.mouse.justPressedRight; + case RELEASED: !FlxG.mouse.pressedRight || FlxG.mouse.justReleasedRight; + case JUST_PRESSED: FlxG.mouse.justPressedRight; + case JUST_RELEASED: FlxG.mouse.justReleasedRight; + } + #end + default: false; + } + } +} + +class FlxActionInputDigitalSteam extends FlxActionInputDigital +{ + var steamInput:FlxInput; + + /** + * Steam Controller action input for digital (button-like) events + * @param ActionHandle handle received from FlxSteamController.getDigitalActionHandle() + * @param Trigger what state triggers this action (PRESSED, JUST_PRESSED, RELEASED, JUST_RELEASED) + * @param DeviceHandle handle received from FlxSteamController.getConnectedControllers(), or FlxInputDeviceID.ALL / FIRST_ACTIVE + */ + @:allow(flixel.input.actions.FlxActionSet) + function new(ActionHandle:Int, Trigger:FlxInputState, ?DeviceHandle:Int = FlxInputDeviceID.FIRST_ACTIVE) + { + super(FlxInputDevice.STEAM_CONTROLLER, ActionHandle, Trigger, DeviceHandle); + #if FLX_STEAMWRAP + steamInput = new FlxInput(ActionHandle); + #else + FlxG.log.warn("steamwrap library not installed; steam inputs will be ignored."); + #end + } + + override public function check(Action:FlxAction):Bool + { + return switch (trigger) + { + case PRESSED: steamInput.pressed || steamInput.justPressed; + case RELEASED: !steamInput.released || steamInput.justReleased; + case JUST_PRESSED: steamInput.justPressed; + case JUST_RELEASED: steamInput.justReleased; + } + } + + override public function update():Void + { + if (getSteamControllerData(deviceID)) + steamInput.press(); + else + steamInput.release(); + } + + inline function getSteamControllerData(controllerHandle:Int):Bool + { + if (controllerHandle == FlxInputDeviceID.FIRST_ACTIVE) + { + controllerHandle = FlxSteamController.getFirstActiveHandle(); + } + + var data = FlxSteamController.getDigitalActionData(controllerHandle, inputID); + + return (data.bActive && data.bState); + } +} + +class FlxActionInputDigitalIFlxInput extends FlxActionInputDigital +{ + var input:IFlxInput; + + /** + * Generic IFlxInput action input + * + * WARNING: IFlxInput objects are often member variables of some other + * object that is often destructed at the end of a state. If you don't + * destroy() this input (or the action you assign it to), the IFlxInput + * reference will persist forever even after its parent object has been + * destroyed! + * + * @param Input A generic IFlxInput object (ex: FlxButton.input) + * @param Trigger Trigger What state triggers this action (PRESSED, JUST_PRESSED, RELEASED, JUST_RELEASED) + */ + public function new(Input:IFlxInput, Trigger:FlxInputState) + { + super(FlxInputDevice.IFLXINPUT_OBJECT, 0, Trigger); + input = Input; + } + + override public function check(action:FlxAction):Bool + { + return switch (trigger) + { + case PRESSED: input.pressed || input.justPressed; + case RELEASED: !input.pressed || input.justReleased; + case JUST_PRESSED: input.justPressed; + case JUST_RELEASED: input.justReleased; + default: false; + } + } + + override public function destroy():Void + { + super.destroy(); + input = null; + } +} \ No newline at end of file diff --git a/flixel/input/actions/FlxActionManager.hx b/flixel/input/actions/FlxActionManager.hx new file mode 100644 index 0000000000..b6f56dba5c --- /dev/null +++ b/flixel/input/actions/FlxActionManager.hx @@ -0,0 +1,915 @@ +package flixel.input.actions; + +import flixel.input.actions.FlxAction.FlxActionAnalog; +import flixel.input.actions.FlxAction.FlxActionDigital; +import flixel.input.actions.FlxActionInput.FlxInputDevice; +import flixel.input.actions.FlxActionInput.FlxInputDeviceID; +import flixel.input.actions.FlxActionInput.FlxInputType; +import flixel.input.actions.FlxActionManager.ActionSetRegister; +import flixel.input.gamepad.FlxGamepad; +import flixel.util.FlxArrayUtil; +import flixel.util.FlxDestroyUtil; +import flixel.util.FlxDestroyUtil.IFlxDestroyable; +import flixel.util.FlxSignal.FlxTypedSignal; +import haxe.Json; + +#if FLX_STEAMWRAP +import steamwrap.api.Steam; +import steamwrap.data.ControllerConfig; +#end + +using flixel.util.FlxArrayUtil; + +/** + * High level input manager for `FlxAction`s. This lets you manage multiple input + * devices and action sets, and is the only supported method for natively using + * the Steam Controller API. + * + * Once you've set up `FlxActionManager`, you can let flixel handle it globally + * with: `FlxG.inputs.add(myFlxActionManager)`; + * + * If you don't add it globally, you will have to call `update()` on it yourself. + * + * If you are using the steamwrap library, `FlxActionManager` can automatically + * create action sets from a steamwrap object derived from the master vdf game + * actions file that Steam makes you set up for native controller support. + * You must then ACTIVATE one of those action sets for any connected steam + * controllers, which will automatically attach the proper steam action inputs + * to the actions in the set. You can also add as many regular `FlxActionInput`s + * as you like to any actions in the sets. + * + */ +class FlxActionManager implements IFlxInputManager implements IFlxDestroyable +{ + var sets:Array; + var register:ActionSetRegister; + + /** + * The number of registered action sets + */ + public var numSets(get, null):Int; + + /** + * A signal fired when a device currently in use is suddenly disconnected. Returns the device type, handle/id, and a string identifier for the device model (if applicable) + */ + public var deviceDisconnected(default, null):FlxTypedSignalInt->String->Void>; + + /** + * A signal fired when a device is connected. Returns the device type, handle/id, and a string identifier for the device model (if applicable) + */ + public var deviceConnected(default, null):FlxTypedSignalInt->String->Void>; + + /** + * A signal fired when an action's inputs have been externally changed. Returns a list of all actions whose inputs have changed. For now only used for Steam Controllers. + */ + public var inputsChanged(default, null):FlxTypedSignal->Void>; + + public function new() + { + sets = []; + register = new ActionSetRegister(); + deviceConnected = new FlxTypedSignalInt->String->Void>(); + deviceDisconnected = new FlxTypedSignalInt->String->Void>(); + inputsChanged = new FlxTypedSignal->Void>(); + #if FLX_GAMEPAD + FlxG.gamepads.deviceConnected.add(onDeviceConnected); + FlxG.gamepads.deviceDisconnected.add(onDeviceDisconnected); + #end + FlxSteamController.onControllerConnect = updateSteamControllers; + FlxSteamController.onOriginUpdate = updateSteamOrigins; + } + + /** + * Activate an action set for a particular device + * @param ActionSet The integer ID for the Action Set you want to activate + * @param Device The device type (Mouse, Keyboard, Gamepad, SteamController, etc) + * @param DeviceID FlxGamepad ID or a Steam Controller Handle (ignored for Mouse/Keyboard) + */ + public function activateSet(ActionSet:Int, Device:FlxInputDevice, DeviceID:Int) + { + register.activate(ActionSet, Device, DeviceID); + onChange(); + } + + /** + * Add actions to a particular action set + * @param Actions The FlxActions you want to add + * @param ActionSet The index of the FlxActionSet you want to add + * @return whether they were all successfully added + */ + public function addActions(Actions:Array, ActionSet:Int = 0):Bool + { + var success = true; + for (Action in Actions) + { + var result = addAction(Action); + if (!result) success = false; + } + return success; + } + + /** + * Add an action to a particular action set + * @param Action The FlxAction you want to add + * @param ActionSet The index of the FlxActionSet you want to add + * @return whether it was successfully added + */ + public function addAction(Action:FlxAction, ActionSet:Int = 0):Bool + { + var success = false; + + if (sets == null) sets = []; + if (sets.length == 0) + { + sets.push(new FlxActionSet("default")); + activateSet(getSetIndex("default"), FlxInputDevice.ALL, FlxInputDeviceID.ALL); + } + + if (ActionSet >= 0 && ActionSet < sets.length) + { + success = sets[ActionSet].add(Action); + } + + onChange(); + + return success; + } + + /** + * Add a FlxActionSet to the manager + * @param set The FlxActionSet you want to add + * @return the index of the FlxActionSet + */ + public function addSet(set:FlxActionSet):Int + { + if (sets.contains(set)) + { + return -1; + } + + sets.push(set); + + onChange(); + + return sets.length - 1; + } + + /** + * Deactivate an action set for any devices it is currently active for + * @param ActionSet The integer ID for the Action Set you want to deactivate + * @param DeviceID FlxGamepad ID or a Steam Controller Handle (ignored for Mouse/Keyboard) + */ + public function deactivateSet(ActionSet:Int, DeviceID:Int = FlxInputDeviceID.ALL) + { + register.activate(ActionSet, FlxInputDevice.NONE, DeviceID); + onChange(); + } + + public function destroy():Void + { + sets = FlxDestroyUtil.destroyArray(sets); + register = FlxDestroyUtil.destroy(register); + } + + /** + * Get the index of a particular action set + * @param name Action set name + * @return Index number of that set, -1 if not found + */ + public function getSetIndex(Name:String):Int + { + for (i in 0...sets.length) + { + if (sets[i].name == Name) return i; + } + return -1; + } + + /** + * Get the name of an action set + * @param Index Action set index + * @return Name of that set, "" if not found + */ + public function getSetName(Index:Int):String + { + if (Index >= 0 && Index < sets.length) + return sets[Index].name; + return ""; + } + + /** + * Get an action set + * @param Index Action set index + * @return The FlxActionSet at that location, or null if not found + */ + public function getSet(Index:Int):FlxActionSet + { + if (Index >= 0 && Index < sets.length) + return sets[Index]; + return null; + } + + /** + * Returns the action set that has been activated for this specific device + */ + @:access(flixel.input.actions.FlxActionManager.ActionSetRegister) + public function getSetActivatedForDevice(device:FlxInputDevice, deviceID:Int = FlxInputDeviceID.ALL):FlxActionSet + { + var id = -1; + var index = -1; + + switch (device) + { + case FlxInputDevice.KEYBOARD: index = register.keyboardSet; + case FlxInputDevice.MOUSE: index = register.mouseSet; + case FlxInputDevice.GAMEPAD: + switch (deviceID) + { + case FlxInputDeviceID.ALL: index = register.gamepadAllSet; + case FlxInputDeviceID.FIRST_ACTIVE: + #if FLX_GAMEPAD + id = FlxG.gamepads.getFirstActiveGamepadID(); + #end + case FlxInputDeviceID.NONE: index = -1; + default: + if (register.gamepadAllSet != -1) + index = register.gamepadAllSet; + else + id = deviceID; + } + if (id >= 0 && id < register.gamepadSets.length) + { + index = register.gamepadSets[id]; + } + case FlxInputDevice.STEAM_CONTROLLER: + switch (deviceID) + { + case FlxInputDeviceID.ALL: index = register.steamControllerAllSet; + case FlxInputDeviceID.NONE: index = -1; + default: + if (register.steamControllerAllSet != -1) + index = register.steamControllerAllSet; + else + id = deviceID; + } + if (id >= 0 && id < register.steamControllerSets.length) + { + index = register.steamControllerSets[id]; + } + case FlxInputDevice.ALL: + switch (deviceID) + { + case FlxInputDeviceID.ALL: index = register.gamepadAllSet; + } + default: index = -1; + } + + if (index >= 0 && index < sets.length) + { + return sets[index]; + } + + return null; + } + + #if FLX_STEAMWRAP + /** + * Load action sets from a steamwrap ControllerConfig object + * @param Config ControllerConfig object derived from your game's "game_actions_XYZ.vdf" file + * @param CallbackDigital Callback function for digital actions + * @param CallbackAnalog Callback function for analog actions + * @return The number of new FlxActionSets created and added. 0 means nothing happened. + */ + public function initSteam(Config:ControllerConfig, CallbackDigital:FlxActionDigital->Void, CallbackAnalog:FlxActionAnalog->Void):Int + { + var i:Int = 0; + for (set in Config.actionSets) + { + if (addSet(FlxActionSet.fromSteam(set, CallbackDigital, CallbackAnalog)) != -1) + { + i++; + } + } + + onChange(); + + return i; + } + #end + + /** + * Load action sets from a parsed JSON object + * @param data JSON object parsed from the same format that "exportToJSON()" outputs + * @param CallbackDigital Callback function for digital actions + * @param CallbackAnalog Callback function for analog actions + * @return The number of new FlxActionSets created and added. 0 means nothing happened. + */ + public function initFromJSON(data:ActionSetJSONArray, CallbackDigital:FlxActionDigital->Void, CallbackAnalog:FlxActionAnalog->Void):Int + { + if (data == null) return 0; + + var i:Int = 0; + var actionSets:Array = Reflect.hasField(data, "actionSets") ? Reflect.field(data, "actionSets") : null; + if (actionSets == null) return 0; + + for (set in actionSets) + { + if (addSet(FlxActionSet.fromJSON(set, CallbackDigital, CallbackAnalog)) != -1) + { + i++; + } + } + + onChange(); + + return i; + } + + @:access(flixel.input.actions.FlxAction) + public function exportToJSON():String + { + var space:String = "\t"; + return Json.stringify({"actionSets":sets}, + function(key:Dynamic, value:Dynamic):Dynamic + { + if (Std.is(value, FlxAction)) + { + var fa:FlxAction = cast value; + return fa.name; + } + if (Std.is(value, FlxActionSet)) + { + var fas:FlxActionSet = cast value; + return { + "name": fas.name, + "digitalActions": fas.digitalActions, + "analogActions": fas.analogActions + } + } + + return value; + + }, + space); + } + + /** + * Remove a set from the manager + * @param Set The set you want to remove + * @param Destroy Whether to also destroy the set or just remove it from the list + * @return whether the action was found & successfully removed + */ + public function removeSet(Set:FlxActionSet, Destroy:Bool = true):Bool + { + var success = sets.remove(Set); + if (success) + { + if (Destroy) + FlxDestroyUtil.destroy(Set); + + onChange(); + } + + return success; + } + + /** + * Remove an action to a particular action set + * @param Action The FlxAction you want to remove + * @param ActionSet The index of the FlxActionSet you want to remove + * @return whether it was successfully removed + */ + public function removeAction(Action:FlxAction, ActionSet:Int):Bool + { + var success = false; + + if (ActionSet >= 0 && ActionSet < sets.length) + { + success = sets[ActionSet].remove(Action); + } + + onChange(); + + return success; + } + + public function reset():Void {} + + function get_numSets():Int + { + return sets.length; + } + + function onChange():Void + { + register.markActiveSets(sets); + } + + function onFocus():Void {} + + function onFocusLost():Void {} + + function onDeviceConnected(gamepad:FlxGamepad) + { + deviceConnected.dispatch(FlxInputDevice.GAMEPAD, gamepad.id, Std.string(gamepad.model).toLowerCase()); + } + + function onDeviceDisconnected(gamepad:FlxGamepad) + { + if (gamepad != null) + { + var actionSet = getSetActivatedForDevice(FlxInputDevice.GAMEPAD, gamepad.id); + if (actionSet != null && actionSet.active) + { + var id = gamepad.id; + var model = gamepad.model != null ? Std.string(gamepad.model).toLowerCase() : ""; + deviceDisconnected.dispatch(FlxInputDevice.GAMEPAD, id, model); + } + } + } + + function onSteamConnected(handle:Int) + { + var allSetIndex = register.steamControllerAllSet; + + if (allSetIndex != -1) + { + activateSet(allSetIndex, FlxInputDevice.STEAM_CONTROLLER, FlxInputDeviceID.ALL); + } + else + { + var actionSet = getSetActivatedForDevice(FlxInputDevice.STEAM_CONTROLLER, handle); + if (actionSet != null && actionSet.active) + { + activateSet(getSetIndex(actionSet.name), FlxInputDevice.STEAM_CONTROLLER, handle); + } + } + + deviceConnected.dispatch(FlxInputDevice.STEAM_CONTROLLER, handle, ""); + } + + function onSteamDisconnected(handle:Int) + { + if (handle >= 0) + { + var actionSet = getSetActivatedForDevice(FlxInputDevice.STEAM_CONTROLLER, handle); + if (actionSet != null && actionSet.active) + { + deviceDisconnected.dispatch(FlxInputDevice.STEAM_CONTROLLER, handle, ""); + } + } + } + + @:access(flixel.input.actions.FlxSteamController) + function updateSteamControllers():Void + { + #if FLX_STEAMWRAP + for (i in 0...FlxSteamController.MAX_CONTROLLERS) + { + if (FlxSteamController.controllers[i].connected.justReleased) + { + onSteamDisconnected(FlxSteamController.controllers[i].handle); + } + else if (FlxSteamController.controllers[i].connected.justPressed) + { + onSteamConnected(FlxSteamController.controllers[i].handle); + } + } + #end + } + + function updateSteamOrigins():Void + { + #if FLX_STEAMWRAP + var changed = register.updateSteamOrigins(sets); + if (changed != null) + { + inputsChanged.dispatch(changed); + } + #end + } + + function update():Void + { + register.update(sets); + } +} + +/** + * internal helper class + */ +@:allow(flixel.input.actions.FlxActionManager) +class ActionSetRegister implements IFlxDestroyable +{ + /** + * The current action set for the mouse + */ + var mouseSet:Int = -1; + + /** + * The current action set for the keyboard + */ + var keyboardSet:Int = -1; + + /** + * The current action set for ALL gamepads + */ + var gamepadAllSet:Int = -1; + + /** + * The current action set for ALL steam controllers + */ + var steamControllerAllSet:Int = -1; + + /** + * Maps individual gamepad ID's to different action sets + */ + var gamepadSets:Array; + + /** + * Maps individual steam controller handles to different action sets + */ + var steamControllerSets:Array; + + function new() + { + FlxSteamController.init(); + gamepadSets = []; + steamControllerSets = []; + } + + public function destroy() + { + gamepadSets = null; + steamControllerSets = null; + } + + public function activate(ActionSet:Int, Device:FlxInputDevice, DeviceID:Int = FlxInputDeviceID.FIRST_ACTIVE) + { + setActivate(ActionSet, Device, DeviceID); + } + + public function markActiveSets(sets:Array) + { + for (i in 0...sets.length) + { + sets[i].active = false; + } + + syncDevice(FlxInputDevice.MOUSE, sets); + syncDevice(FlxInputDevice.KEYBOARD, sets); + syncDevice(FlxInputDevice.GAMEPAD, sets); + syncDevice(FlxInputDevice.STEAM_CONTROLLER, sets); + } + + public function update(sets:Array) + { + #if FLX_STEAMWRAP + updateSteam(sets); + #end + + for (i in 0...sets.length) + { + sets[i].update(); + } + } + + public function updateSteam(sets:Array) + { + #if FLX_STEAMWRAP + //Steam explicitly recommend in their documentation that you should re-activate the current action set every frame + if (steamControllerAllSet != -1) + { + var allSet = sets[steamControllerAllSet]; + var allSetHandle = allSet.steamHandle; + FlxSteamController.activateActionSet(FlxInputDeviceID.ALL, allSetHandle); + } + else + { + for (i in 0...steamControllerSets.length) + { + if (steamControllerSets[i] != -1) + { + var theSet = sets[steamControllerSets[i]]; + var theSetHandle = theSet.steamHandle; + FlxSteamController.activateActionSet(i, theSetHandle); + } + } + } + #end + } + + function setActivate(ActionSet:Int, Device:FlxInputDevice, DeviceID:Int = FlxInputDeviceID.FIRST_ACTIVE, DoActivate:Bool = true) + { + switch (Device) + { + case FlxInputDevice.MOUSE: mouseSet = DoActivate ? ActionSet : -1; + case FlxInputDevice.KEYBOARD: keyboardSet = DoActivate ? ActionSet : -1; + case FlxInputDevice.GAMEPAD: + switch (DeviceID) + { + case FlxInputDeviceID.ALL: + clearSetFromArray( -1, gamepadSets); + gamepadAllSet = DoActivate ? ActionSet : -1; + + case FlxInputDeviceID.NONE: + clearSetFromArray(ActionSet, gamepadSets); + + #if FLX_GAMEPAD + case FlxInputDeviceID.FIRST_ACTIVE: + gamepadSets[FlxG.gamepads.getFirstActiveGamepadID()] = DoActivate ? ActionSet : -1; + #end + + default: + gamepadSets[DeviceID] = DoActivate ? ActionSet : -1; + } + + case FlxInputDevice.STEAM_CONTROLLER: + switch (DeviceID) + { + case FlxInputDeviceID.ALL: + steamControllerAllSet = DoActivate ? ActionSet : -1; + clearSetFromArray( -1, steamControllerSets); + for (i in 0...FlxSteamController.MAX_CONTROLLERS) + { + steamControllerSets[i] = DoActivate ? ActionSet : -1; + } + + case FlxInputDeviceID.NONE: + clearSetFromArray(ActionSet, steamControllerSets); + + case FlxInputDeviceID.FIRST_ACTIVE: + steamControllerSets[FlxSteamController.getFirstActiveHandle()] = DoActivate ? ActionSet : -1; + + default: + steamControllerSets[DeviceID] = DoActivate ? ActionSet : -1; + } + + case FlxInputDevice.ALL: + setActivate(ActionSet, FlxInputDevice.MOUSE, DeviceID, DoActivate); + setActivate(ActionSet, FlxInputDevice.KEYBOARD, DeviceID, DoActivate); + setActivate(ActionSet, FlxInputDevice.GAMEPAD, DeviceID, DoActivate); + #if FLX_STEAMWRAP + setActivate(ActionSet, FlxInputDevice.STEAM_CONTROLLER, DeviceID, DoActivate); + #end + + case FlxInputDevice.NONE: + setActivate(ActionSet, FlxInputDevice.ALL, DeviceID, false); + + default: + + //do nothing + } + } + + /**********PRIVATE*********/ + function updateSteamOrigins(sets:Array):Array + { + #if FLX_STEAMWRAP + + var changed:Array = null; + + if (steamControllerAllSet != -1) + { + var allSet = sets[steamControllerAllSet]; + var allSetHandle = allSet.steamHandle; + + for (dAction in allSet.digitalActions) + { + updateDigitalActionOrigins(dAction, FlxInputDeviceID.ALL, allSetHandle); + if (dAction.steamOriginsChanged) changed = FlxArrayUtil.safePush(changed, dAction); + } + for (aAction in allSet.analogActions) + { + updateAnalogActionOrigins(aAction, FlxInputDeviceID.ALL, allSetHandle); + if (aAction.steamOriginsChanged) changed = FlxArrayUtil.safePush(changed, aAction); + } + } + else + { + for (i in 0...steamControllerSets.length) + { + if (steamControllerSets[i] != -1) + { + var theSet = sets[steamControllerSets[i]]; + var theSetHandle = theSet.steamHandle; + + for (dAction in theSet.digitalActions) + { + updateDigitalActionOrigins(dAction, i, theSetHandle); + if (dAction.steamOriginsChanged) changed = FlxArrayUtil.safePush(changed, dAction); + } + for (aAction in theSet.analogActions) + { + updateAnalogActionOrigins(aAction, i, theSetHandle); + if (aAction.steamOriginsChanged) changed = FlxArrayUtil.safePush(changed, aAction); + } + } + } + } + + return changed; + #end + return []; + } + + @:access(flixel.input.actions.FlxAction) + function updateDigitalActionOrigins(action:FlxActionDigital, deviceID:Int, setHandle:Int) + { + #if FLX_STEAMWRAP + if (Steam.controllers == null) return; + var checksum = action._steamOriginsChecksum; + if (deviceID == FlxInputDeviceID.ALL) deviceID = 0; + Steam.controllers.getDigitalActionOrigins(deviceID, setHandle, action.steamHandle, action._steamOrigins); + var newChecksum = cheapChecksum(cast action._steamOrigins); + + if (checksum != newChecksum) + { + action.steamOriginsChanged = true; + } + else + { + action.steamOriginsChanged = false; + } + action._steamOriginsChecksum = newChecksum; + #end + } + + @:access(flixel.input.actions.FlxAction) + function updateAnalogActionOrigins(action:FlxActionAnalog, deviceID:Int, setHandle:Int) + { + #if FLX_STEAMWRAP + if (Steam.controllers == null) return; + var checksum = action._steamOriginsChecksum; + if (deviceID == FlxInputDeviceID.ALL) deviceID = 0; + Steam.controllers.getAnalogActionOrigins(deviceID, setHandle, action.steamHandle, cast action._steamOrigins); + var newChecksum = cheapChecksum(cast action._steamOrigins); + if (checksum != newChecksum) + { + action.steamOriginsChanged = true; + } + else + { + action.steamOriginsChanged = false; + } + action._steamOriginsChecksum = newChecksum; + #end + } + + function cheapChecksum(arr:Array):Int + { + //Fletcher's algorithm: + + var sum1 = 0; + var sum2 = 0; + + if (arr != null) + { + for (n in arr) + { + sum1 = (sum1 + n) % 255; + sum2 = (sum2 + sum1) % 255; + } + } + + return (sum2 << 8) | sum1; + } + + /**********PRIVATE*********/ + /** + * Helper function to properly update the action sets with proper steam inputs + */ + function updateSteamInputs(sets:Array):Void + { + #if FLX_STEAMWRAP + if (steamControllerAllSet != -1) + { + for (i in 0...steamControllerSets.length) + { + changeSteamControllerActionSet(i, steamControllerAllSet, sets); + } + } + else + { + for (i in 0...steamControllerSets.length) + { + if (steamControllerSets[i] != -1) + { + changeSteamControllerActionSet(i, steamControllerSets[i], sets); + } + } + } + updateSteam(sets); + updateSteamOrigins(sets); + #end + } + + /** + * Attach this controller to a new action set, remove it from the old action set, update internal steam inputs accordingly + * @param controllerHandle steam controller handle (guaranteed not to be FlxInputDeviceID.ALL or FlxInputDeviceID.FIRST_ACTIVE) + * @param newSet the action set handle of the new set to attach to + */ + function changeSteamControllerActionSet(controllerHandle:Int, newSet:Int, sets:Array) + { + var lastSet = FlxSteamController.getCurrentActionSet(controllerHandle); + if (lastSet == newSet) return; + + if (sets == null) + { + return; + } + + if (lastSet != -1) + { + if (lastSet < sets.length) + { + //detach inputs for this controller from any steam-aware actions in the old set + sets[lastSet].attachSteamController(controllerHandle, false); + } + } + + //attach inputs for this controller to any steam-aware actions in the new set + sets[newSet].attachSteamController(controllerHandle); + } + + /** + * Go through all action sets and mark them as active if any device is + * subscribed to one of their actions. (Assumes you've previously set + * them all to active=false earlier in the udpate loop) + * @param device + * @param sets + */ + function syncDevice(device:FlxInputDevice, sets:Array) + { + switch (device) + { + case FlxInputDevice.MOUSE: + if (mouseSet >= 0 && mouseSet < sets.length) + sets[mouseSet].active = true; + + case FlxInputDevice.KEYBOARD: + if (keyboardSet >= 0 && keyboardSet < sets.length) + sets[keyboardSet].active = true; + + case FlxInputDevice.GAMEPAD: + if (gamepadAllSet >= 0 && gamepadAllSet < sets.length) + { + sets[gamepadAllSet].active = true; + } + else + { + for (i in 0...gamepadSets.length) + { + var gset = gamepadSets[i]; + if (gset >= 0 && gset < sets.length) + { + sets[gset].active = true; + } + } + } + + case FlxInputDevice.STEAM_CONTROLLER: + updateSteamInputs(sets); + if (steamControllerAllSet >= 0 && steamControllerAllSet < sets.length) + { + sets[steamControllerAllSet].active = true; + } + else + { + for (i in 0...steamControllerSets.length) + { + var sset = steamControllerSets[i]; + if (sset >= 0 && sset < sets.length) + { + sets[sset].active = true; + } + } + } + default: + //do nothing + } + } + + function clearSetFromArray(ActionSet:Int = -1, array:Array) + { + for (i in 0...array.length) + { + if (ActionSet == -1 || array[i] == ActionSet) + { + array[i] = -1; + } + } + } +} + +typedef ActionSetJSONArray = +{ + @:optional var actionSets:Array; +} + +typedef ActionSetJSON = +{ + @:optional var name:String; + @:optional var analogActions:Array; + @:optional var digitalActions:Array; +} diff --git a/flixel/input/actions/FlxActionSet.hx b/flixel/input/actions/FlxActionSet.hx new file mode 100644 index 0000000000..feeb325c81 --- /dev/null +++ b/flixel/input/actions/FlxActionSet.hx @@ -0,0 +1,323 @@ +package flixel.input.actions; + +import flixel.input.FlxInput.FlxInputState; +import flixel.input.actions.FlxAction.FlxActionAnalog; +import flixel.input.actions.FlxAction.FlxActionDigital; +import flixel.input.actions.FlxActionInput.FlxInputDevice; +import flixel.input.actions.FlxActionInput.FlxInputType; +import flixel.input.actions.FlxActionInputAnalog.FlxActionInputAnalogSteam; +import flixel.input.actions.FlxActionInputAnalog.FlxAnalogState; +import flixel.input.actions.FlxActionInputAnalog.FlxAnalogAxis; +import flixel.input.actions.FlxActionInputDigital.FlxActionInputDigitalSteam; +import flixel.input.actions.FlxActionManager.ActionSetJSON; +import flixel.util.FlxDestroyUtil; +import flixel.util.FlxDestroyUtil.IFlxDestroyable; +import haxe.Json; + +#if FLX_STEAMWRAP +import steamwrap.data.ControllerConfig.ControllerActionSet; +#end + +using flixel.util.FlxArrayUtil; + +@:allow(flixel.input.actions.FlxActionManager) +class FlxActionSet implements IFlxDestroyable +{ + /** + * Name of the action set + */ + public var name(default, null):String = ""; + + #if FLX_STEAMWRAP + /** + * This action set's numeric handle for the Steam API (ignored if not using Steam) + */ + public var steamHandle(default, null):Int = -1; + #end + + /** + * Digital actions in this set + */ + public var digitalActions(default, null):Array; + + /** + * Analog actions in this set + */ + public var analogActions(default, null):Array; + + /** + * Whether this action set runs when update() is called + */ + public var active:Bool = true; + + #if FLX_STEAMWRAP + /** + * Create an action set from a steamwrap configuration file. + * + * NOTE: no steam inputs will be attached to the created actions; you must call + * attachSteamController() which will automatically add or remove steam + * inputs for a particular controller. + * + * This is unique to steam inputs, which cannot be constructed directly. + * Non-steam inputs can be constructed and added to the actions normally. + * + * @param SteamSet A steamwrap ControllerActionSet file (found in ControllerConfig) + * @param CallbackDigital A function to call when digital actions fire + * @param CallbackAnalog A function to call when analog actions fire + * @return An action set + */ + @:access(flixel.input.actions.FlxActionManager) + private static function fromSteam(SteamSet:ControllerActionSet, CallbackDigital:FlxActionDigital->Void, CallbackAnalog:FlxActionAnalog->Void):FlxActionSet + { + if (SteamSet == null) return null; + + var digitalActions:Array = []; + var analogActions:Array = []; + + if (SteamSet.button != null) + { + for (b in SteamSet.button) + { + if (b == null) continue; + var action = new FlxActionDigital(b.name, CallbackDigital); + var aHandle = FlxSteamController.getDigitalActionHandle(b.name); + action.steamHandle = aHandle; + digitalActions.push(action); + } + } + if (SteamSet.analogTrigger != null) + { + for (a in SteamSet.analogTrigger) + { + if (a == null) continue; + var action = new FlxActionAnalog(a.name, CallbackAnalog); + var aHandle = FlxSteamController.getAnalogActionHandle(a.name); + action.steamHandle = aHandle; + analogActions.push(action); + } + } + for (s in SteamSet.stickPadGyro) + { + if (s == null) continue; + var action = new FlxActionAnalog(s.name, CallbackAnalog); + var aHandle = FlxSteamController.getAnalogActionHandle(s.name); + action.steamHandle = aHandle; + analogActions.push(action); + } + + var set = new FlxActionSet(SteamSet.name, digitalActions, analogActions); + set.steamHandle = FlxSteamController.getActionSetHandle(SteamSet.name); + + return set; + } + #end + + /** + * Create an action set from a parsed JSON object + * + * @param Data A parsed JSON object + * @param CallbackDigital A function to call when digital actions fire + * @param CallbackAnalog A function to call when analog actions fire + * @return An action set + */ + @:access(flixel.input.actions.FlxActionManager) + static function fromJSON(Data:ActionSetJSON, CallbackDigital:FlxActionDigital->Void, CallbackAnalog:FlxActionAnalog->Void):FlxActionSet + { + var digitalActions:Array = []; + var analogActions:Array = []; + + if (Data == null) return null; + + if (Data.digitalActions != null) + { + var arrD:Array = Data.digitalActions; + for (d in arrD) + { + var dName:String = cast d; + var action = new FlxActionDigital(dName, CallbackDigital); + digitalActions.push(action); + } + } + + if (Data.analogActions != null) + { + var arrA:Array = Data.analogActions; + for (a in arrA) + { + var aName:String = cast a; + var action = new FlxActionAnalog(aName, CallbackAnalog); + analogActions.push(action); + } + } + + if (Data.name != null) + { + var name:String = Data.name; + var set = new FlxActionSet(name, digitalActions, analogActions); + return set; + } + + return null; + } + + public function toJSON():String + { + var space:String = "\t"; + return Json.stringify(this, + function(key:Dynamic, value:Dynamic):Dynamic + { + if (Std.is(value, FlxAction)) + { + var fa:FlxAction = cast value; + return { + "type": fa.type, + "name": fa.name, + "steamHandle": fa.steamHandle + } + } + return value; + } + , space); + } + + public function new(Name:String, ?DigitalActions:Array, ?AnalogActions:Array) + { + name = Name; + if (DigitalActions == null) DigitalActions = []; + if (AnalogActions == null) AnalogActions = []; + digitalActions = DigitalActions; + analogActions = AnalogActions; + } + + /** + * Automatically adds or removes inputs for a steam controller + * to any steam-affiliated actions + * @param Handle steam controller handle from FlxSteam.getConnectedControllers(), or FlxInputDeviceID.FIRST_ACTIVE / ALL + * @param Attach true: adds inputs, false: removes inputs + */ + public function attachSteamController(Handle:Int, Attach:Bool = true):Void + { + attachSteamControllerSub(Handle, Attach, FlxInputType.DIGITAL, digitalActions, null); + attachSteamControllerSub(Handle, Attach, FlxInputType.ANALOG, null, analogActions); + } + + public function add(Action:FlxAction):Bool + { + if (Action.type == DIGITAL) + { + var dAction:FlxActionDigital = cast Action; + if (digitalActions.contains(dAction)) return false; + digitalActions.push(dAction); + return true; + } + else if (Action.type == ANALOG) + { + var aAction:FlxActionAnalog = cast Action; + if (analogActions.contains(aAction)) return false; + analogActions.push(aAction); + return true; + } + return false; + } + + public function destroy():Void + { + digitalActions = FlxDestroyUtil.destroyArray(digitalActions); + analogActions = FlxDestroyUtil.destroyArray(analogActions); + } + + /** + * Remove an action from this set + * @param Action a FlxAction + * @param Destroy whether to destroy it as well + * @return whether it was found and removed + */ + public function remove(Action:FlxAction, Destroy:Bool = true):Bool + { + var result = false; + if (Action.type == DIGITAL) + { + result = digitalActions.remove(cast Action); + if (result && Destroy) + { + Action.destroy(); + } + } + else if (Action.type == ANALOG) + { + result = analogActions.remove(cast Action); + if (result && Destroy) + { + Action.destroy(); + } + } + return result; + } + + /** + * Update all the actions in this set (each will check inputs & potentially trigger) + */ + public function update():Void + { + if (!active) return; + for (digitalAction in digitalActions) + { + digitalAction.update(); + } + for (analogAction in analogActions) + { + analogAction.update(); + } + } + + function attachSteamControllerSub(Handle:Int, Attach:Bool, InputType:FlxInputType, DigitalActions:Array, AnalogActions:Array) + { + var length = InputType == FlxInputType.DIGITAL ? DigitalActions.length : AnalogActions.length; + + for (i in 0...length) + { + var action = InputType == FlxInputType.DIGITAL ? DigitalActions[i] : AnalogActions[i]; + + if (action.steamHandle != -1) //all steam-affiliated actions will have this numeric ID assigned + { + var inputExists = false; + var theInput:FlxActionInput = null; + + //check if any of the steam controller inputs match this handle + if (action.inputs != null) + { + for (input in action.inputs) + { + if (input.device == FlxInputDevice.STEAM_CONTROLLER && input.deviceID == Handle) + { + inputExists = true; + theInput = input; + } + } + } + + if (Attach) + { + //attaching: add inputs for this controller if they don't exist + + if (!inputExists) + { + if (InputType == FlxInputType.DIGITAL) + { + DigitalActions[i].add(new FlxActionInputDigitalSteam(action.steamHandle, FlxInputState.JUST_PRESSED, Handle)); + } + else if (InputType == FlxInputType.ANALOG) + { + AnalogActions[i].add(new FlxActionInputAnalogSteam(action.steamHandle, FlxAnalogState.MOVED, FlxAnalogAxis.EITHER, Handle)); + } + } + } + else if (inputExists) + { + //detaching: remove inputs for this controller if they exist + action.remove(theInput); + } + } + } + } +} \ No newline at end of file diff --git a/flixel/input/actions/FlxSteamController.hx b/flixel/input/actions/FlxSteamController.hx new file mode 100644 index 0000000000..c18765ea4f --- /dev/null +++ b/flixel/input/actions/FlxSteamController.hx @@ -0,0 +1,344 @@ +package flixel.input.actions; + +import flixel.input.IFlxInputManager; +import flixel.input.actions.FlxActionInput.FlxInputDeviceID; + +#if FLX_STEAMWRAP +import steamwrap.api.Steam; +import steamwrap.api.Controller.ControllerDigitalActionData; +import steamwrap.api.Controller.ControllerAnalogActionData; +#end + +/** + * Helper class that wraps steam API so that flixel can do some basic + * book-keeping on top of it + * + * Also cuts down a bit on the (cpp && steamwrap) clutter by letting me stuff + * all those conditionals and imports over here and just letting them + * resolve to no-ops if steamwrap isn't detected. + * + * Not meant to be exposed to end users, just for internal use by + * FlxGamepadManager. If a user wants to use the Steam API directly + * they should just be making naked calls to steamwrap themselves. + * + */ +@:allow(flixel.input.actions) +class FlxSteamController +{ + /** + * The maximum number of controllers that can be connected + */ + public static var MAX_CONTROLLERS(get, null):Int; + + /** + * The maximum number of origins (input glyphs, basically) that can be assigned to an action + */ + public static var MAX_ORIGINS(get, null):Int; + + /** + * The wait time between polls for connected controllers + */ + public static var CONTROLLER_CONNECT_POLL_TIME(default, null):Float = 0.25; + + /** + * The wait time between polls for action origins (checking which input glyphs are associated with an action, in case the player has re-configured them) + */ + public static var ORIGIN_DATA_POLL_TIME(default, null):Float = 1.0; + + static var controllers:Array; + + static var onControllerConnect(default, null):Void->Void = null; + static var onOriginUpdate(default, null):Void->Void = null; + + static inline function get_MAX_CONTROLLERS():Int + { + #if FLX_STEAMWRAP + if (Steam.controllers == null) return 0; + return Steam.controllers.MAX_CONTROLLERS; + #else + return 0; + #end + } + + static inline function get_MAX_ORIGINS():Int + { + #if FLX_STEAMWRAP + if (Steam.controllers == null) return 0; + return Steam.controllers.MAX_ORIGINS; + #else + return 0; + #end + } + + static function clear() + { + #if FLX_STEAMWRAP + if (controllers == null) return; + for (i in 0...controllers.length) + { + controllers[i].active = false; + controllers[i].connected.release(); + } + #end + } + + static function init() + { + controllers = []; + #if FLX_STEAMWRAP + + if (Steam.controllers == null) return; + + for (i in 0...Steam.controllers.MAX_CONTROLLERS) + { + controllers.push(new FlxSteamControllerMetadata()); + } + + //If the FlxSteamUpdater hasn't been added to FlxG.inputs yet, + //we need to do so now to ensure that steam controllers will + //be properly updated every frame + var steamExists = false; + for (input in FlxG.inputs.list) + { + if (Std.is(input, FlxSteamUpdater)) + { + steamExists = true; + break; + } + } + if (!steamExists) + { + FlxG.inputs.add(new FlxSteamUpdater()); + } + + #end + } + + static function getActionSetHandle(name:String):Int + { + #if FLX_STEAMWRAP + if (Steam.controllers == null) return -1; + return Steam.controllers.getActionSetHandle(name); + #end + return -1; + } + + static function getCurrentActionSet(SteamControllerHandle:Int):Int + { + #if FLX_STEAMWRAP + if (controllers == null) return -1; + if (SteamControllerHandle >= 0 && SteamControllerHandle <= controllers.length) + { + return controllers[SteamControllerHandle].actionSet; + } + #end + return -1; + } + + static function activateActionSet(SteamControllerHandle:Int, ActionSetHandle:Int) + { + #if FLX_STEAMWRAP + if (Steam.controllers == null) return; + if (SteamControllerHandle == FlxInputDeviceID.NONE) return; + if (SteamControllerHandle == FlxInputDeviceID.ALL) + { + for (i in 0...controllers.length) + { + controllers[i].actionSet = ActionSetHandle; + Steam.controllers.activateActionSet(controllers[i].handle, ActionSetHandle); + } + } + else if (SteamControllerHandle == FlxInputDeviceID.FIRST_ACTIVE) + { + //TODO: not sure FIRST_ACTIVE will be very reliable in a steam controller context... I might consider dropping support for this handle in the future + for (i in 0...controllers.length) + { + if (controllers[i].active) + { + controllers[i].actionSet = ActionSetHandle; + Steam.controllers.activateActionSet(controllers[i].handle, ActionSetHandle); + break; + } + } + } + else + { + Steam.controllers.activateActionSet(SteamControllerHandle, ActionSetHandle); + } + #end + } + + static function getFirstActiveHandle():Int + { + #if FLX_STEAMWRAP + if (controllers == null) return -1; + for (i in 0...controllers.length) + { + if (controllers[i].active) + { + return controllers[i].handle; + } + } + #end + return -1; + } + + static function getConnectedControllers():Array + { + #if FLX_STEAMWRAP + if (Steam.controllers == null) return []; + var arr = Steam.controllers.getConnectedControllers(); + + for (i in 0...Steam.controllers.MAX_CONTROLLERS) + { + if (i < arr.length && arr[i] >= 0) + { + var index = arr[i]; + controllers[index].handle = index; + } + + controllers[i].connected.update(); + if (arr.indexOf(controllers[i].handle) != -1) + { + controllers[i].connected.press(); + } + else + { + controllers[i].connected.release(); + } + } + + return arr; + #else + return []; + #end + } + + #if FLX_STEAMWRAP + static function getAnalogActionData(controller:Int, action:Int, ?data:ControllerAnalogActionData):ControllerAnalogActionData + { + if (Steam.controllers == null) return data; + data = Steam.controllers.getAnalogActionData(controller, action, data); + if (controller >= 0 && controller < controllers.length) + { + if (data.bActive > 0 && data.x != 0 || data.y != 0) + { + controllers[controller].active = true; + } + } + return data; + } + #else + static function getAnalogActionData(controller:Int, action:Int, ?data:Dynamic):Dynamic + { + return null; + } + #end + + #if FLX_STEAMWRAP + private static function getDigitalActionData(controller:Int, action:Int):ControllerDigitalActionData + { + if (Steam.controllers == null) return 0; + var data = Steam.controllers.getDigitalActionData(controller, action); + if (controller >= 0 && controller < controllers.length) + { + if (data.bActive && data.bState) + { + controllers[controller].active = true; + } + } + return data; + } + #else + static function getDigitalActionData(controller:Int, action:Int):DigitalActionData + { + return new DigitalActionData(false, false); + } + #end + + static inline function getAnalogActionHandle(name:String):Int + { + #if FLX_STEAMWRAP + if (Steam.controllers == null) return -1; + return Steam.controllers.getAnalogActionHandle(name); + #else + return -1; + #end + } + + static inline function getDigitalActionHandle(name:String):Int + { + #if FLX_STEAMWRAP + if (Steam.controllers == null) return -1; + return Steam.controllers.getDigitalActionHandle(name); + #else + return -1; + #end + } +} + +private class DigitalActionData +{ + public var bActive:Bool; + public var bState:Bool; + + public function new(bActive:Bool, bState:Bool) + { + this.bActive = bActive; + this.bState = bState; + } +} + +@:allow(flixel.input.actions) +#if !FLX_UNIT_TEST private #end class FlxSteamControllerMetadata +{ + public var handle:Int = -1; + public var actionSet:Int = -1; + public var active:Bool = false; + public var connected:FlxInput = new FlxInput(0); + + function new():Void{} +} + +@:allow(flixel.input.actions) +private class FlxSteamUpdater implements IFlxInputManager +{ + var controllerTime:Float = 0.0; + var originTime:Float = 0.0; + + function new(){}; + + public function destroy():Void {} + + public function reset():Void {} + + //run the steam API every frame if steam is detected + function update():Void + { + #if FLX_STEAMWRAP + Steam.onEnterFrame(); + + controllerTime += FlxG.elapsed; + originTime += FlxG.elapsed; + + if (controllerTime > FlxSteamController.CONTROLLER_CONNECT_POLL_TIME) + { + controllerTime -= FlxSteamController.CONTROLLER_CONNECT_POLL_TIME; + FlxSteamController.getConnectedControllers(); + if (FlxSteamController.onControllerConnect != null) + FlxSteamController.onControllerConnect(); + } + + if (originTime > FlxSteamController.ORIGIN_DATA_POLL_TIME) + { + originTime -= FlxSteamController.ORIGIN_DATA_POLL_TIME; + if (FlxSteamController.onOriginUpdate != null) + FlxSteamController.onOriginUpdate(); + } + #end + } + + function onFocus():Void {} + + function onFocusLost():Void {} +} \ No newline at end of file diff --git a/flixel/input/gamepad/FlxGamepad.hx b/flixel/input/gamepad/FlxGamepad.hx index c970961eee..a32ab5152f 100644 --- a/flixel/input/gamepad/FlxGamepad.hx +++ b/flixel/input/gamepad/FlxGamepad.hx @@ -84,6 +84,10 @@ class FlxGamepad implements IFlxDestroyable * Helper class to check if a button is pressed. */ public var pressed(default, null):FlxGamepadButtonList; + /** + * Helper class to check if a button is released + */ + public var released(default, null):FlxGamepadButtonList; /** * Helper class to check if a button was just pressed. */ @@ -130,6 +134,7 @@ class FlxGamepad implements IFlxDestroyable manager = Manager; pressed = new FlxGamepadButtonList(FlxInputState.PRESSED, this); + released = new FlxGamepadButtonList(FlxInputState.RELEASED, this); justPressed = new FlxGamepadButtonList(FlxInputState.JUST_PRESSED, this); justReleased = new FlxGamepadButtonList(FlxInputState.JUST_RELEASED, this); analog = new FlxGamepadAnalogList(this); @@ -137,7 +142,17 @@ class FlxGamepad implements IFlxDestroyable pointer = new FlxGamepadPointerValueList(this); if (Model == null) - Model = XINPUT; + { + #if vita + Model = PSVITA; + #elseif ps4 + Model = PS4; + #elseif xbox1 + Model = XINPUT; + #else + Model = XINPUT; + #end + } if (Attachment == null) Attachment = NONE; @@ -287,7 +302,40 @@ class FlxGamepad implements IFlxDestroyable */ public inline function checkStatus(ID:FlxGamepadInputID, Status:FlxInputState):Bool { - return checkStatusRaw(mapping.getRawID(ID), Status); + return switch (ID) + { + case FlxGamepadInputID.ANY: + switch (Status) + { + case PRESSED: pressed.ANY; + case JUST_PRESSED: justPressed.ANY; + case RELEASED: released.ANY; + case JUST_RELEASED: justReleased.ANY; + } + case FlxGamepadInputID.NONE: + switch (Status) + { + case PRESSED: pressed.NONE; + case JUST_PRESSED: justPressed.NONE; + case RELEASED: released.NONE; + case JUST_RELEASED: justReleased.NONE; + } + default: + var rawID = mapping.getRawID(ID); + var button = buttons[rawID]; + if (button == null) + { + return false; + } + var value = button.current; + switch (Status) + { + case PRESSED: value == PRESSED; + case RELEASED: value == RELEASED; + case JUST_PRESSED: value == JUST_PRESSED; + case JUST_RELEASED: value == JUST_RELEASED; + } + } } /** @@ -840,7 +888,7 @@ class FlxGamepad implements IFlxDestroyable function get_deadZone():Float { - return (manager.globalDeadZone == null) ? _deadZone : manager.globalDeadZone; + return (manager == null || manager.globalDeadZone == null) ? _deadZone : manager.globalDeadZone; } inline function set_deadZone(deadZone:Float):Float diff --git a/flixel/input/gamepad/FlxGamepadManager.hx b/flixel/input/gamepad/FlxGamepadManager.hx index 198460f095..26ce16ce40 100644 --- a/flixel/input/gamepad/FlxGamepadManager.hx +++ b/flixel/input/gamepad/FlxGamepadManager.hx @@ -1,6 +1,7 @@ package flixel.input.gamepad; import flixel.input.FlxInput.FlxInputState; +import flixel.util.FlxSignal.FlxTypedSignal; import flixel.input.gamepad.FlxGamepad.FlxGamepadModel; import flixel.util.FlxDestroyUtil; @@ -37,6 +38,18 @@ class FlxGamepadManager implements IFlxInputManager */ public var globalDeadZone:Null; + /** + * Signal for when a device is connected; returns the connected gamepad object to the attached listener + * @since 4.6.0 + */ + public var deviceConnected:FlxTypedSignalVoid>; + + /** + * Signal for when a device is disconnected; returns the id of the disconnected gamepad to the attached listener + * @since 4.6.0 + */ + public var deviceDisconnected:FlxTypedSignalVoid>; + /** * Stores all gamepads - can have null entries, but index matches event.device */ @@ -65,14 +78,19 @@ class FlxGamepadManager implements IFlxInputManager function removeByID(GamepadID:Int):Void { var gamepad:FlxGamepad = _gamepads[GamepadID]; + if (gamepad != null) { - FlxDestroyUtil.destroy(gamepad); _gamepads[GamepadID] = null; var i = _activeGamepads.indexOf(gamepad); if (i != -1) + { _activeGamepads[i] = null; + deviceDisconnected.dispatch(gamepad); + } + + FlxDestroyUtil.destroy(gamepad); } if (lastActive == gamepad) @@ -303,6 +321,8 @@ class FlxGamepadManager implements IFlxInputManager @:allow(flixel.FlxG) function new() { + deviceConnected = new FlxTypedSignalVoid>(); + deviceDisconnected = new FlxTypedSignalVoid>(); #if FLX_JOYSTICK_API FlxG.stage.addEventListener(JoystickEvent.AXIS_MOVE, handleAxisMove); FlxG.stage.addEventListener(JoystickEvent.BALL_MOVE, handleBallMove); @@ -350,17 +370,21 @@ class FlxGamepadManager implements IFlxInputManager Device.enabled = true; var id:Int = findGamepadIndex(Device); + if (id < 0) return; var gamepad:FlxGamepad = createByID(id, getModelFromDeviceName(Device.name)); gamepad._device = Device; + + deviceConnected.dispatch(gamepad); } function getModelFromDeviceName(name:String):FlxGamepadModel { //If we're actually running on console hardware, we know what controller hardware you're using //TODO: add support for multiple controller types on console that support that (WiiU for instance) + if (name == null) return UNKNOWN; #if vita return PSVITA; @@ -390,12 +414,14 @@ class FlxGamepadManager implements IFlxInputManager { if (Device == null) return; - + for (i in 0..._gamepads.length) { var gamepad:FlxGamepad = _gamepads[i]; if (gamepad != null && gamepad._device == Device) + { removeByID(i); + } } } #end @@ -519,7 +545,8 @@ class FlxGamepadManager implements IFlxInputManager function handleDeviceAdded(event:JoystickEvent):Void { - createByID(event.device, getModelFromJoystick(event.x)); + var gamepad = createByID(event.device, getModelFromJoystick(event.x)); + deviceConnected.dispatch(gamepad); } function handleDeviceRemoved(event:JoystickEvent):Void diff --git a/flixel/input/gamepad/id/PS4ID.hx b/flixel/input/gamepad/id/PS4ID.hx index 4896ceec50..0e0b4b1189 100644 --- a/flixel/input/gamepad/id/PS4ID.hx +++ b/flixel/input/gamepad/id/PS4ID.hx @@ -53,7 +53,6 @@ class PS4ID public static inline var CIRCLE:Int = 7; public static inline var SQUARE:Int = 8; public static inline var TRIANGLE:Int = 9; - public static inline var SHARE:Int = 10; public static inline var PS:Int = 11; public static inline var OPTIONS:Int = 12; public static inline var LEFT_STICK_CLICK:Int = 13; @@ -61,10 +60,21 @@ class PS4ID public static inline var L1:Int = 15; public static inline var R1:Int = 16; - public static inline var TOUCHPAD_CLICK:Int = 21; + #if ps4 + public static inline var TOUCHPAD_CLICK:Int = 10; //On an actual PS4, share is reserved by the system, and the touchpad click can serve more or less as a replacement for the "back/select" button + + public static var LEFT_ANALOG_STICK (default, null) = new FlxGamepadAnalogStick(0, 1, {up:32, down:33, left:34, right:35}); + public static var RIGHT_ANALOG_STICK(default, null) = new FlxGamepadAnalogStick(2, 3, {up:36, down:37, left:38, right:39}); + + public static inline var SHARE:Int = 40; //Not accessible on an actual PS4, just setting it to a dummy value + #else + public static inline var SHARE:Int = 10; //This is only accessible when not using an actual Playstation 4, otherwise it's reserved by the system public static var LEFT_ANALOG_STICK (default, null) = new FlxGamepadAnalogStick(0, 1, {up:22, down:23, left:24, right:25}); public static var RIGHT_ANALOG_STICK(default, null) = new FlxGamepadAnalogStick(2, 3, {up:26, down:27, left:28, right:29}); + + public static inline var TOUCHPAD_CLICK:Int = 30; //I don't believe this is normally accessible on PC, just setting it to a dummy value + #end public static inline var L2:Int = 4; public static inline var R2:Int = 5; diff --git a/flixel/input/gamepad/id/PSVitaID.hx b/flixel/input/gamepad/id/PSVitaID.hx index 3e70d75c63..c6d53149a3 100644 --- a/flixel/input/gamepad/id/PSVitaID.hx +++ b/flixel/input/gamepad/id/PSVitaID.hx @@ -25,5 +25,5 @@ class PSVitaID public static inline var DPAD_RIGHT:Int = 20; public static var LEFT_ANALOG_STICK (default, null) = new FlxGamepadAnalogStick(0, 1, {up:21, down:22, left:23, right:24}); - public static var RIGHT_ANALOG_STICK(default, null) = new FlxGamepadAnalogStick(2, 3, {up:21, down:22, left:23, right:24}); + public static var RIGHT_ANALOG_STICK(default, null) = new FlxGamepadAnalogStick(2, 3, {up:25, down:26, left:27, right:28}); } \ No newline at end of file diff --git a/flixel/input/gamepad/lists/FlxBaseGamepadList.hx b/flixel/input/gamepad/lists/FlxBaseGamepadList.hx index 169a9088eb..1f36d2208c 100644 --- a/flixel/input/gamepad/lists/FlxBaseGamepadList.hx +++ b/flixel/input/gamepad/lists/FlxBaseGamepadList.hx @@ -39,4 +39,34 @@ class FlxBaseGamepadList return false; } + + public var ALL(get, never):Bool; + + function get_ALL():Bool + { + for (button in gamepad.buttons) + { + if (button != null && !checkRaw(button.ID)) + { + return false; + } + } + + return true; + } + + public var NONE(get, never):Bool; + + function get_NONE():Bool + { + for (button in gamepad.buttons) + { + if (button != null && checkRaw(button.ID)) + { + return false; + } + } + + return true; + } } \ No newline at end of file diff --git a/flixel/input/gamepad/mappings/FlxGamepadMapping.hx b/flixel/input/gamepad/mappings/FlxGamepadMapping.hx index cb7c6bc05c..e46bded272 100644 --- a/flixel/input/gamepad/mappings/FlxGamepadMapping.hx +++ b/flixel/input/gamepad/mappings/FlxGamepadMapping.hx @@ -69,11 +69,6 @@ class FlxGamepadMapping return -1; } - public function isAxisForMotion(ID:FlxGamepadInputID):Bool - { - return false; - } - /** * Whether this axis needs to be flipped */ @@ -82,6 +77,11 @@ class FlxGamepadMapping return false; } + public function isAxisForMotion(ID:FlxGamepadInputID):Bool + { + return false; + } + #if FLX_JOYSTICK_API /** * Given an axis index value like 0-6, figures out which input that diff --git a/flixel/input/gamepad/mappings/PS4Mapping.hx b/flixel/input/gamepad/mappings/PS4Mapping.hx index 43d6b1761a..4dcd28f961 100644 --- a/flixel/input/gamepad/mappings/PS4Mapping.hx +++ b/flixel/input/gamepad/mappings/PS4Mapping.hx @@ -20,6 +20,8 @@ class PS4Mapping extends FlxGamepadMapping { leftStick = PS4ID.LEFT_ANALOG_STICK; rightStick = PS4ID.RIGHT_ANALOG_STICK; + supportsMotion = true; + supportsPointer = true; } override public function getID(rawID:Int):FlxGamepadInputID @@ -30,7 +32,11 @@ class PS4Mapping extends FlxGamepadMapping case PS4ID.CIRCLE: B; case PS4ID.SQUARE: X; case PS4ID.TRIANGLE: Y; + #if ps4 + case PS4ID.TOUCHPAD_CLICK: BACK; + #else case PS4ID.SHARE: BACK; + #end case PS4ID.PS: GUIDE; case PS4ID.OPTIONS: START; case PS4ID.LEFT_STICK_CLICK: LEFT_STICK_CLICK; @@ -61,7 +67,11 @@ class PS4Mapping extends FlxGamepadMapping case B: PS4ID.CIRCLE; case X: PS4ID.SQUARE; case Y: PS4ID.TRIANGLE; + #if ps4 + case BACK: PS4ID.TOUCHPAD_CLICK; + #else case BACK: PS4ID.SHARE; + #end case GUIDE: PS4ID.PS; case START: PS4ID.OPTIONS; case LEFT_STICK_CLICK: PS4ID.LEFT_STICK_CLICK; diff --git a/flixel/input/gamepad/mappings/XInputMapping.hx b/flixel/input/gamepad/mappings/XInputMapping.hx index a4d1d58915..e8919bcc75 100644 --- a/flixel/input/gamepad/mappings/XInputMapping.hx +++ b/flixel/input/gamepad/mappings/XInputMapping.hx @@ -26,10 +26,10 @@ class XInputMapping extends FlxGamepadMapping { return switch (rawID) { - case XInputID.A: B; - case XInputID.B: A; - case XInputID.X: Y; - case XInputID.Y: X; + case XInputID.A: A; + case XInputID.B: B; + case XInputID.X: X; + case XInputID.Y: Y; case XInputID.BACK: BACK; case XInputID.GUIDE: GUIDE; case XInputID.START: START; @@ -108,6 +108,14 @@ class XInputMapping extends FlxGamepadMapping } #end + #if xbox1 + override public function isAxisFlipped(axisID:Int):Bool + { + return axisID == XInputID.LEFT_ANALOG_STICK.y || + axisID == XInputID.RIGHT_ANALOG_STICK.y; + } + #end + #if FLX_JOYSTICK_API override public function axisIndexToRawID(axisID:Int):Int { diff --git a/flixel/system/macros/FlxDefines.hx b/flixel/system/macros/FlxDefines.hx index 98e056f3aa..5a7def9931 100644 --- a/flixel/system/macros/FlxDefines.hx +++ b/flixel/system/macros/FlxDefines.hx @@ -38,6 +38,7 @@ private enum HelperDefines FLX_SOUND_SYSTEM; FLX_FOCUS_LOST_SCREEN; FLX_DEBUG; + FLX_STEAMWRAP; FLX_MOUSE_ADVANCED; FLX_NATIVE_CURSOR; @@ -152,6 +153,9 @@ class FlxDefines if (defined("cpp") || defined("neko")) define(FLX_POST_PROCESS); #end + + if (defined("cpp") && defined("steamwrap")) + define(FLX_STEAMWRAP); if (defined("mobile") || defined("js")) define(FLX_ACCELEROMETER); diff --git a/flixel/util/FlxArrayUtil.hx b/flixel/util/FlxArrayUtil.hx index 69c8aa8fcb..6b1e0e81e4 100644 --- a/flixel/util/FlxArrayUtil.hx +++ b/flixel/util/FlxArrayUtil.hx @@ -159,6 +159,18 @@ class FlxArrayUtil return array[array.length - 1]; } + /** + * Pushes the element into the array (and if the array is null, creates it first) and returns the array. + * @since 4.6.0 + */ + public static function safePush(array:Array, element:T):Array + { + if (array == null) + array = []; + array.push(element); + return array; + } + public static inline function contains(array:Array, element:T):Bool { return array.indexOf(element) != -1; diff --git a/tests/RunTravis.hx b/tests/RunTravis.hx index b380f32ee2..14e4433772 100644 --- a/tests/RunTravis.hx +++ b/tests/RunTravis.hx @@ -81,7 +81,8 @@ class RunTravis haxelibGit.bind("HaxeFlixel", "flixel-templates"), haxelibGit.bind("HaxeFlixel", "flixel-demos"), haxelibGit.bind("HaxeFlixel", "flixel-addons"), - haxelibGit.bind("HaxeFlixel", "flixel-ui") + haxelibGit.bind("HaxeFlixel", "flixel-ui"), + haxelibGit.bind("larsiusprime", "steamwrap") ]); } @@ -149,7 +150,7 @@ class RunTravis else { Sys.println("Running unit tests...\n"); - return runOpenFL("test", "unit", target); + return runOpenFL("test", "unit", target, "travis"); } } diff --git a/tests/unit/project.xml b/tests/unit/project.xml index da758efb8e..f2b46ff510 100644 --- a/tests/unit/project.xml +++ b/tests/unit/project.xml @@ -12,6 +12,7 @@ + diff --git a/tests/unit/src/flixel/input/actions/FlxActionInputAnalogTest.hx b/tests/unit/src/flixel/input/actions/FlxActionInputAnalogTest.hx new file mode 100644 index 0000000000..bcac075715 --- /dev/null +++ b/tests/unit/src/flixel/input/actions/FlxActionInputAnalogTest.hx @@ -0,0 +1,644 @@ +package flixel.input.actions; + +import flixel.input.actions.FlxActionInputAnalog.FlxActionInputAnalogClickAndDragMouseMotion; +import flixel.input.actions.FlxActionInputAnalog.FlxActionInputAnalogGamepad; +import flixel.input.actions.FlxActionInputAnalog.FlxActionInputAnalogMouseMotion; +import flixel.input.actions.FlxActionInputAnalog.FlxActionInputAnalogMousePosition; +import flixel.input.actions.FlxActionInputAnalog.FlxAnalogAxis; +import flixel.input.actions.FlxActionInputAnalog.FlxAnalogState; +import flixel.input.gamepad.FlxGamepad; +import flixel.input.gamepad.FlxGamepadAnalogStick; +import flixel.input.gamepad.FlxGamepadButton; +import flixel.input.gamepad.FlxGamepadInputID; +import flixel.input.mouse.FlxMouseButton; +import flixel.math.FlxPoint; +import flixel.input.actions.FlxAction.FlxActionAnalog; +import flixel.input.mouse.FlxMouseButton.FlxMouseButtonID; +#if FLX_GAMEINPUT_API +import lime.ui.Gamepad; +import openfl.ui.GameInput; +import openfl.ui.GameInputControl; +import openfl.ui.GameInputDevice; +#elseif FLX_JOYSTICK_API +import openfl.events.JoystickEvent; +#end + +class FlxActionInputAnalogTest extends FlxTest +{ + var value0:Int = 0; + var value1:Int = 0; + var value2:Int = 0; + var value3:Int = 0; + + @Before + function before() {} + + @Test + function testMousePosition() + { + var axes = + [ + {name:"x", value:FlxAnalogAxis.X}, + {name:"y", value:FlxAnalogAxis.Y}, + {name:"either", value:FlxAnalogAxis.EITHER}, + {name:"both", value:FlxAnalogAxis.BOTH} + ]; + + for (a in axes) + { + var name = "mouse_pos." + a.name; + var axis = a.value; + + var t = new TestShell(name + "."); + runTestMousePosition(t, axis, false); + + //Press & release w/o callbacks + t.assertTrue (name + ".move1.just"); + t.assertTrue (name + ".move1.value"); + t.assertFalse(name + ".move2.just"); + t.assertTrue (name + ".move2.value"); + t.assertTrue (name + ".stop1.just"); + t.assertTrue (name + ".stop1.value"); + t.assertFalse(name + ".stop2.just"); + t.assertTrue (name + ".stop2.value"); + } + } + + function runTestMousePosition(test:TestShell, axis:FlxAnalogAxis, callbacks:Bool) + { + var a = new FlxActionInputAnalogMousePosition(FlxAnalogState.MOVED, axis); + var b = new FlxActionInputAnalogMousePosition(FlxAnalogState.JUST_MOVED, axis); + var c = new FlxActionInputAnalogMousePosition(FlxAnalogState.STOPPED, axis); + var d = new FlxActionInputAnalogMousePosition(FlxAnalogState.JUST_STOPPED, axis); + + var clear = clearMousePosition; + var move = moveMousePosition; + + var pos1 = new FlxPoint(); + var pos2 = new FlxPoint(); + + switch (axis) + { + case X: pos1.set(10, 0); pos2.set(20, 0); + case Y: pos1.set( 0, 10); pos2.set( 0, 20); + case EITHER: pos1.set(10, 0); pos2.set(20, 0); + case BOTH: pos1.set(10, 10); pos2.set(20, 20); + } + + testInputStates(test, clear, move, [pos1, pos2, pos2, pos2], axis, a, b, c, d, callbacks); + } + + @Test + function testMouseMotion() + { + var axes = + [ + {name:"x", value:FlxAnalogAxis.X}, + {name:"y", value:FlxAnalogAxis.Y}, + {name:"either", value:FlxAnalogAxis.EITHER}, + {name:"both", value:FlxAnalogAxis.BOTH} + ]; + + for (a in axes) + { + var name = "mouse_move." + a.name; + var axis = a.value; + + var t = new TestShell(name + "."); + runTestMouseMotion(t, axis, false); + + //Press & release w/o callbacks + t.assertTrue (name + ".move1.just"); + t.assertTrue (name + ".move1.value"); + t.assertFalse(name + ".move2.just"); + t.assertTrue (name + ".move2.value"); + t.assertTrue (name + ".stop1.just"); + t.assertTrue (name + ".stop1.value"); + t.assertFalse(name + ".stop2.just"); + t.assertTrue (name + ".stop2.value"); + } + } + + function runTestMouseMotion(test:TestShell, axis:FlxAnalogAxis, callbacks:Bool) + { + var a = new FlxActionInputAnalogMouseMotion(FlxAnalogState.MOVED, axis); + var b = new FlxActionInputAnalogMouseMotion(FlxAnalogState.JUST_MOVED, axis); + var c = new FlxActionInputAnalogMouseMotion(FlxAnalogState.STOPPED, axis); + var d = new FlxActionInputAnalogMouseMotion(FlxAnalogState.JUST_STOPPED, axis); + + var clear = clearMousePosition; + var move = moveMousePosition; + + var pos1 = new FlxPoint(); + var pos2 = new FlxPoint(); + var pos3 = new FlxPoint(); + var pos4 = new FlxPoint(); + + switch (axis) + { + case X: + pos1.set( 50, 0); + pos2.set(100, 0); + pos3.set(100, 0); + pos4.set(100, 0); + case Y: + pos1.set( 0, 50); + pos2.set( 0, 100); + pos3.set( 0, 100); + pos4.set( 0, 100); + case EITHER: + pos1.set( 50, 0); + pos2.set(100, 0); + pos3.set(100, 0); + pos4.set(100, 0); + case BOTH: + pos1.set( 50, 50); + pos2.set(100, 100); + pos3.set(100, 100); + pos4.set(100, 100); + } + + testInputStates(test, clear, move, [pos1, pos2, pos3, pos4], axis, a, b, c, d, callbacks); + } + + @Test + function testClickAndDragMouseMotion() + { + var axes = + [ + {name:"x", value:FlxAnalogAxis.X}, + {name:"y", value:FlxAnalogAxis.Y}, + {name:"either", value:FlxAnalogAxis.EITHER}, + {name:"both", value:FlxAnalogAxis.BOTH} + ]; + + var buttons = + [ + {name:"left", value:FlxMouseButtonID.LEFT}, + {name:"right", value:FlxMouseButtonID.RIGHT}, + {name:"middle", value:FlxMouseButtonID.MIDDLE} + ]; + + for (b in buttons) + { + var button = b.value; + for (a in axes) + { + var name = "mouse_click_and_drag_move." + b.name + "." + a.name; + var axis = a.value; + + var t = new TestShell(name + "."); + runTestClickAndDragMouseMotion(t, button, axis, false); + + //Press & release w/o callbacks + t.assertTrue (name + ".move1.just"); + t.assertTrue (name + ".move1.value"); + t.assertFalse(name + ".move2.just"); + t.assertTrue (name + ".move2.value"); + t.assertTrue (name + ".stop1.just"); + t.assertTrue (name + ".stop1.value"); + t.assertFalse(name + ".stop2.just"); + t.assertTrue (name + ".stop2.value"); + } + } + } + + function runTestClickAndDragMouseMotion(test:TestShell, button:FlxMouseButtonID, axis:FlxAnalogAxis, callbacks:Bool) + { + var a = new FlxActionInputAnalogClickAndDragMouseMotion(button, MOVED, axis); + var b = new FlxActionInputAnalogClickAndDragMouseMotion(button, JUST_MOVED, axis); + var c = new FlxActionInputAnalogClickAndDragMouseMotion(button, STOPPED, axis); + var d = new FlxActionInputAnalogClickAndDragMouseMotion(button, JUST_STOPPED, axis); + + var clear = clearClickAndDragMousePosition; + var move = moveClickAndDragMousePosition.bind(button); + + var pos1 = new FlxPoint(); + var pos2 = new FlxPoint(); + var pos3 = new FlxPoint(); + var pos4 = new FlxPoint(); + + switch (axis) + { + case X: + pos1.set( 50, 0); + pos2.set(100, 0); + pos3.set(100, 0); + pos4.set(100, 0); + case Y: + pos1.set( 0, 50); + pos2.set( 0, 100); + pos3.set( 0, 100); + pos4.set( 0, 100); + case EITHER: + pos1.set( 50, 0); + pos2.set(100, 0); + pos3.set(100, 0); + pos4.set(100, 0); + case BOTH: + pos1.set( 50, 50); + pos2.set(100, 100); + pos3.set(100, 100); + pos4.set(100, 100); + } + + testInputStates(test, clear, move, [pos1, pos2, pos3, pos4], axis, a, b, c, d, callbacks); + } + + @Test + function testGamepad() + { + #if flash + return; + #end + + var inputs = + [ + {input:FlxGamepadInputID.LEFT_ANALOG_STICK, value:FlxAnalogAxis.X, label:"left_stick_x"}, + {input:FlxGamepadInputID.LEFT_ANALOG_STICK, value:FlxAnalogAxis.Y, label:"left_stick_y"}, + {input:FlxGamepadInputID.LEFT_ANALOG_STICK, value:FlxAnalogAxis.EITHER, label:"left_stick_either"}, + {input:FlxGamepadInputID.LEFT_ANALOG_STICK, value:FlxAnalogAxis.BOTH, label:"left_stick_both"}, + {input:FlxGamepadInputID.RIGHT_ANALOG_STICK, value:FlxAnalogAxis.X, label:"right_stick_x"}, + {input:FlxGamepadInputID.RIGHT_ANALOG_STICK, value:FlxAnalogAxis.Y, label:"right_stick_y"}, + {input:FlxGamepadInputID.RIGHT_ANALOG_STICK, value:FlxAnalogAxis.EITHER, label:"right_stick_either"}, + {input:FlxGamepadInputID.RIGHT_ANALOG_STICK, value:FlxAnalogAxis.BOTH, label:"right_stick_both"}, + {input:FlxGamepadInputID.LEFT_TRIGGER, value:FlxAnalogAxis.X, label:"left_trigger_x"}, + {input:FlxGamepadInputID.RIGHT_TRIGGER, value:FlxAnalogAxis.X, label:"right_trigger_x"}, + ]; + + for (inp in inputs) + { + var name = "gamepad." + inp.label; + var input = inp.input; + var axis = inp.value; + + var t = new TestShell(name + "."); + runTestGamepad(t, input, axis, false); + + //Press & release w/o callbacks + t.assertTrue (name + ".move1.just"); + t.assertTrue (name + ".move1.value"); + t.assertFalse(name + ".move2.just"); + t.assertTrue (name + ".move2.value"); + t.assertTrue (name + ".stop1.just"); + t.assertTrue (name + ".stop1.value"); + t.assertFalse(name + ".stop2.just"); + t.assertTrue (name + ".stop2.value"); + } + } + + function runTestGamepad(test:TestShell, input:FlxGamepadInputID, axis:FlxAnalogAxis, callbacks:Bool) + { + #if FLX_JOYSTICK_API + FlxG.stage.dispatchEvent(new JoystickEvent(JoystickEvent.DEVICE_ADDED, false, false, 0, 0, 0, 0, 0)); + #elseif (!flash && FLX_GAMEINPUT_API) + var g = makeFakeGamepad(); + #end + + var gamepad:FlxGamepad = FlxG.gamepads.getByID(0); + + var a = new FlxActionInputAnalogGamepad(input, FlxAnalogState.MOVED, axis, 0); + var b = new FlxActionInputAnalogGamepad(input, FlxAnalogState.JUST_MOVED, axis, 0); + var c = new FlxActionInputAnalogGamepad(input, FlxAnalogState.STOPPED, axis, 0); + var d = new FlxActionInputAnalogGamepad(input, FlxAnalogState.JUST_STOPPED, axis, 0); + + var clear = clearGamepad.bind(gamepad, input); + var move = moveGamepad.bind(gamepad, input); + + var pos1 = new FlxPoint(); + var pos2 = new FlxPoint(); + var pos3 = new FlxPoint(); + + switch (axis) + { + case X: pos1.set(10, 0); pos2.set(20, 0); pos3.set(0, 0); + case Y: pos1.set( 0, 10); pos2.set( 0, 20); pos3.set(0, 0); + case EITHER: pos1.set(10, 0); pos2.set(20, 0); pos3.set(0, 0); + case BOTH: pos1.set(10, 10); pos2.set(20, 20); pos3.set(0, 0); + } + + testInputStates(test, clear, move, [pos1, pos2, pos3, pos3], axis, a, b, c, d, callbacks); + + #if FLX_JOYSTICK_API + FlxG.stage.dispatchEvent(new JoystickEvent(JoystickEvent.DEVICE_REMOVED, false, false, 0, 0, 0, 0, 0)); + #elseif (!flash && FLX_GAMEINPUT_API) + removeGamepad(g); + #end + } + + #if (!flash && FLX_GAMEINPUT_API) + private function makeFakeGamepad() + { + var xinput = @:privateAccess new Gamepad(0); + @:privateAccess GameInput.__onGamepadConnect(xinput); + var gamepad = FlxG.gamepads.getByID(0); + gamepad.model = FlxGamepadModel.XINPUT; + var gid:GameInputDevice = @:privateAccess gamepad._device; + + @:privateAccess gid.id = "0"; + @:privateAccess gid.name = "xinput"; + + var control:GameInputControl = null; + + for (i in 0...6) + { + control = @:privateAccess new GameInputControl (gid, "AXIS_" + i, -1, 1); + @:privateAccess gid.__axis.set (i, control); + @:privateAccess gid.__controls.push (control); + } + + for (i in 0...15) + { + control = @:privateAccess new GameInputControl (gid, "BUTTON_" + i, 0, 1); + @:privateAccess gid.__button.set (i, control); + @:privateAccess gid.__controls.push (control); + } + + gamepad.update(); + return xinput; + } + + private function removeGamepad(g:Gamepad) + { + @:privateAccess GameInput.__onGamepadDisconnect(g); + } + #end + + function getCallback(i:Int) + { + return function (a:FlxActionAnalog) + { + onCallback(i); + } + } + + function testInputStates(test:TestShell, clear:Void->Void, move:Float->Float->Array->Void, values:Array, axis:FlxAnalogAxis, moved:FlxActionInputAnalog, jMoved:FlxActionInputAnalog, stopped:FlxActionInputAnalog, jStopped:FlxActionInputAnalog, testCallbacks:Bool) + { + var aMoved:FlxActionAnalog; + var ajMoved:FlxActionAnalog; + var aStopped:FlxActionAnalog; + var ajStopped:FlxActionAnalog; + + if (!testCallbacks) + { + ajMoved = new FlxActionAnalog("jMoved", null); + aMoved = new FlxActionAnalog("moved", null); + ajStopped = new FlxActionAnalog("jStopped", null); + aStopped = new FlxActionAnalog("stopped", null); + } + else + { + ajMoved = new FlxActionAnalog("jMoved", getCallback(0)); + aMoved = new FlxActionAnalog("moved", getCallback(1)); + ajStopped = new FlxActionAnalog("jStopped", getCallback(2)); + aStopped = new FlxActionAnalog("stopped", getCallback(3)); + } + + ajMoved.add(jMoved); + aMoved.add(moved); + ajStopped.add(jStopped); + aStopped.add(stopped); + + var arr = [aMoved, ajMoved, aStopped, ajStopped]; + + clear(); + + var callbackStr = (testCallbacks ? "callbacks." : ""); + + test.prefix = "move1." + callbackStr; + + var x1 = values[0].x; + var y1 = values[0].y; + + var x2 = values[1].x; + var y2 = values[1].y; + + var x3 = values[2].x; + var y3 = values[2].y; + + var x4 = values[3].x; + var y4 = values[3].y; + + //JUST moved + move(x1, y1, arr); + + test.testBool(ajMoved.triggered, "just"); + test.testBool(aMoved.triggered, "value"); + if (testCallbacks) + { + test.testBool(value0 == 1, "callback1"); + test.testBool(value1 == 1, "callback2"); + test.testBool(value2 == 0, "callback3"); + test.testBool(value3 == 0, "callback4"); + } + + test.prefix = "move2." + callbackStr; + + //STILL moved + move(x2, y2, arr); + test.testBool(ajMoved.triggered, "just"); + test.testBool(aMoved.triggered, "value"); + if (testCallbacks) + { + test.testBool(value0 == 1, "callback1"); + test.testBool(value1 == 2, "callback2"); + test.testBool(value2 == 0, "callback3"); + test.testBool(value3 == 0, "callback4"); + } + + test.prefix = "stop1." + callbackStr; + + //JUST stopped + move(x3, y3, arr); + test.testBool(ajStopped.triggered, "just"); + test.testBool(aStopped.triggered, "value"); + if (testCallbacks) + { + test.testBool(value0 == 1, "callback1"); + test.testBool(value1 == 2, "callback2"); + test.testBool(value2 == 1, "callback3"); + test.testBool(value3 == 1, "callback4"); + } + + test.prefix = "stop2." + callbackStr; + + //STILL stopped + move(x4, y4, arr); + test.testBool(ajStopped.triggered, "just"); + test.testBool(aStopped.triggered, "value"); + if (testCallbacks) + { + test.testBool(value0 == 1, "callback1"); + test.testBool(value1 == 2, "callback2"); + test.testBool(value2 == 1, "callback3"); + test.testBool(value3 == 2, "callback4"); + } + + clear(); + clearValues(); + + aMoved.destroy(); + aStopped.destroy(); + ajMoved.destroy(); + ajStopped.destroy(); + } + + @:access(flixel.input.mouse.FlxMouse) + private function clearClickAndDragMousePosition() + { + if (FlxG.mouse == null) return; + FlxG.mouse.setGlobalScreenPositionUnsafe(0, 0); + + var left = @:privateAccess FlxG.mouse._leftButton; + var right = @:privateAccess FlxG.mouse._rightButton; + var middle = @:privateAccess FlxG.mouse._middleButton; + + left.update(); + right.update(); + middle.update(); + + step(); + step(); + } + + @:access(flixel.input.gamepad.FlxGamepad) + private function clearGamepad(Gamepad:FlxGamepad, Input:FlxGamepadInputID) + { + if (Input == FlxGamepadInputID.LEFT_ANALOG_STICK || Input == FlxGamepadInputID.RIGHT_ANALOG_STICK) + { + var stick:FlxGamepadAnalogStick = Gamepad.mapping.getAnalogStick(Input); + moveStick(Gamepad, stick, 0.0, 0.0); + } + else + { + moveTrigger(Gamepad, Input, 0.0); + } + step(); + step(); + } + + @:access(flixel.input.gamepad.FlxGamepad) + private function moveTrigger(Gamepad:FlxGamepad, Input:FlxGamepadInputID, X:Float) + { + #if FLX_JOYSTICK_API + var fakeAxisRawID:Int = Gamepad.mapping.checkForFakeAxis(Input); + if (fakeAxisRawID == -1) + { + //regular axis value + var rawID = Gamepad.mapping.getRawID(Input); + Gamepad.applyAxisFlip(X, Input); + Gamepad.axis[rawID] = X; + } + else + { + //if analog isn't supported for this input, return the correct digital button input instead + var btn:FlxGamepadButton = Gamepad.getButton(fakeAxisRawID); + if (btn != null) + { + btn.release(); + } + } + #elseif (FLX_GAMEINPUT_API && !flash) + var rawAxisID = Gamepad.mapping.getRawID(Input); + var control:GameInputControl = Gamepad._device.getControlAt(rawAxisID); + @:privateAccess control.value = X; + #end + } + + @:access(flixel.input.gamepad.FlxGamepad) + private function moveStick(Gamepad:FlxGamepad, stick:FlxGamepadAnalogStick, X:Float, Y:Float) + { + #if FLX_JOYSTICK_API + + Gamepad.axis[stick.x] = X; + Gamepad.axis[stick.y] = Y; + + #elseif (FLX_GAMEINPUT_API && !flash) + + var controlX:GameInputControl = Gamepad._device.getControlAt(stick.x); + var controlY:GameInputControl = Gamepad._device.getControlAt(stick.y); + @:privateAccess controlX.value = X; + @:privateAccess controlY.value = Y; + + #end + } + + @:access(flixel.input.mouse.FlxMouse) + private function clearMousePosition() + { + if (FlxG.mouse == null) return; + FlxG.mouse.setGlobalScreenPositionUnsafe(0, 0); + step(); + step(); + } + + private function moveClickAndDragMousePosition(Button:FlxMouseButtonID, X:Float, Y:Float, arr:Array) + { + var button:FlxMouseButton = + switch (Button) + { + case FlxMouseButtonID.LEFT: @:privateAccess FlxG.mouse._leftButton; + case FlxMouseButtonID.RIGHT: @:privateAccess FlxG.mouse._rightButton; + case FlxMouseButtonID.MIDDLE: @:privateAccess FlxG.mouse._middleButton; + default: null; + } + + button.press(); + + if (FlxG.mouse == null) return; + step(); + FlxG.mouse.setGlobalScreenPositionUnsafe(X, Y); + updateActions(arr); + } + + @:access(flixel.input.gamepad.FlxGamepad) + private function moveGamepad(Gamepad:FlxGamepad, Input:FlxGamepadInputID, X:Float, Y:Float, arr:Array) + { + step(); + + if (Input == FlxGamepadInputID.LEFT_ANALOG_STICK || Input == FlxGamepadInputID.RIGHT_ANALOG_STICK) + { + var stick = Gamepad.mapping.getAnalogStick(Input); + moveStick(Gamepad, stick, X, Y); + } + else + { + moveTrigger(Gamepad, Input, X); + } + + updateActions(arr); + } + + private function moveMousePosition(X:Float, Y:Float, arr:Array) + { + if (FlxG.mouse == null) return; + step(); + FlxG.mouse.setGlobalScreenPositionUnsafe(X, Y); + updateActions(arr); + } + + private function updateActions(arr:Array) + { + for (a in arr) + { + if (a == null) continue; + a.update(); + } + } + + private function onCallback(i:Int) + { + switch (i) + { + case 0: value0++; + case 1: value1++; + case 2: value2++; + case 3: value3++; + } + } + + private function clearValues() + { + value0 = value1 = value2 = value3 = 0; + } +} \ No newline at end of file diff --git a/tests/unit/src/flixel/input/actions/FlxActionInputDigitalTest.hx b/tests/unit/src/flixel/input/actions/FlxActionInputDigitalTest.hx new file mode 100644 index 0000000000..5ffb659fab --- /dev/null +++ b/tests/unit/src/flixel/input/actions/FlxActionInputDigitalTest.hx @@ -0,0 +1,933 @@ +package flixel.input.actions; + +#if (!flash && FLX_GAMEINPUT_API) +import flixel.input.gamepad.FlxGamepad.FlxGamepadModel; +#elseif FLX_JOYSTICK_API +import openfl.events.JoystickEvent; +#end +import flixel.input.actions.FlxAction.FlxActionDigital; +import flixel.input.actions.FlxActionInputDigital.FlxActionInputDigitalMouseWheel; +import flixel.input.actions.FlxActionInputDigital.FlxActionInputDigitalGamepad; +import flixel.input.actions.FlxActionInputDigital.FlxActionInputDigitalIFlxInput; +import flixel.input.actions.FlxActionInputDigital.FlxActionInputDigitalKeyboard; +import flixel.input.actions.FlxActionInputDigital.FlxActionInputDigitalMouse; +import flixel.input.FlxInput; +import flixel.input.FlxInput.FlxInputState; +import flixel.input.gamepad.FlxGamepadButton; +import flixel.input.gamepad.FlxGamepadInputID; +import flixel.input.keyboard.FlxKey; +import flixel.input.mouse.FlxMouseButton; +import flixel.input.mouse.FlxMouseButton.FlxMouseButtonID; + +#if (!flash && FLX_GAMEINPUT_API) +import lime.ui.Gamepad; +import openfl.ui.GameInput; +import openfl.ui.GameInputControl; +import openfl.ui.GameInputDevice; +#end + +class FlxActionInputDigitalTest extends FlxTest +{ + var action:FlxActionDigital; + + var value0:Int = 0; + var value1:Int = 0; + var value2:Int = 0; + var value3:Int = 0; + + @Before + function before() {} + + @Test + function testIFlxInput() + { + var t = new TestShell("iflxinput."); + + runTestIFlxInput(t, false); + + //Press & release w/o callbacks + t.assertTrue ("iflxinput.press1.just"); + t.assertTrue ("iflxinput.press1.value"); + t.assertFalse("iflxinput.press2.just"); + t.assertTrue ("iflxinput.press2.value"); + t.assertTrue ("iflxinput.release1.just"); + t.assertTrue ("iflxinput.release1.value"); + t.assertFalse("iflxinput.release2.just"); + t.assertTrue ("iflxinput.release2.value"); + } + + @Test + function testIFlxInputCallbacks() + { + var t = new TestShell("iflxinput."); + + runTestIFlxInput(t, true); + + //Press & release w/ callbacks + t.assertTrue ("iflxinput.press1.callbacks.just"); + t.assertTrue ("iflxinput.press1.callbacks.value"); + t.assertFalse("iflxinput.press2.callbacks.just"); + t.assertTrue ("iflxinput.press2.callbacks.value"); + t.assertTrue ("iflxinput.release1.callbacks.just"); + t.assertTrue ("iflxinput.release1.callbacks.value"); + t.assertFalse("iflxinput.release2.callbacks.just"); + t.assertTrue ("iflxinput.release2.callbacks.value"); + + //Callbacks themselves (1-4: pressed, just_pressed, released, just_released) + for (i in 1...5) + { + t.assertTrue("iflxinput.press1.callbacks.callback" + i); + t.assertTrue("iflxinput.press2.callbacks.callback" + i); + t.assertTrue("iflxinput.release1.callbacks.callback" + i); + t.assertTrue("iflxinput.release2.callbacks.callback" + i); + } + } + + function runTestIFlxInput(test:TestShell, callbacks:Bool) + { + var state = new FlxInput(0); + + var a = new FlxActionInputDigitalIFlxInput(state, FlxInputState.PRESSED); + var b = new FlxActionInputDigitalIFlxInput(state, FlxInputState.JUST_PRESSED); + var c = new FlxActionInputDigitalIFlxInput(state, FlxInputState.RELEASED); + var d = new FlxActionInputDigitalIFlxInput(state, FlxInputState.JUST_RELEASED); + + var clear = clearFlxInput.bind(state); + var click = clickFlxInput.bind(state); + + testInputStates(test, clear, click, a, b, c, d, callbacks); + } + + @Test + function testFlxMouseButton() + { + var buttons = + [ + {name:"left", value:FlxMouseButtonID.LEFT}, + {name:"right", value:FlxMouseButtonID.RIGHT}, + {name:"middle", value:FlxMouseButtonID.MIDDLE} + ]; + + for (button in buttons) + { + var name = button.name; + var value = button.value; + + var t = new TestShell(name + "."); + runTestFlxMouseButton(t, value, false); + + //Press & release w/o callbacks + t.assertTrue (name + ".press1.just"); + t.assertTrue (name + ".press1.value"); + t.assertFalse(name + ".press2.just"); + t.assertTrue (name + ".press2.value"); + t.assertTrue (name + ".release1.just"); + t.assertTrue (name + ".release1.value"); + t.assertFalse(name + ".release2.just"); + t.assertTrue (name + ".release2.value"); + } + + } + + @Test + function testFlxMouseButtonCallbacks() + { + var buttons = + [ + {name:"left", value:FlxMouseButtonID.LEFT}, + {name:"right", value:FlxMouseButtonID.RIGHT}, + {name:"middle", value:FlxMouseButtonID.MIDDLE} + ]; + + for (button in buttons) + { + var name = button.name; + var value = button.value; + + var t = new TestShell(name + "."); + runTestFlxMouseButton(t, value, true); + + //Press & release w/ callbacks + t.assertTrue (name + ".press1.callbacks.just"); + t.assertTrue (name + ".press1.callbacks.value"); + t.assertFalse(name + ".press2.callbacks.just"); + t.assertTrue (name + ".press2.callbacks.value"); + t.assertTrue (name + ".release1.callbacks.just"); + t.assertTrue (name + ".release1.callbacks.value"); + t.assertFalse(name + ".release2.callbacks.just"); + t.assertTrue (name + ".release2.callbacks.value"); + + //Callbacks themselves (1-4: pressed, just_pressed, released, just_released) + for (i in 1...5) + { + t.assertTrue(name + ".press1.callbacks.callback" + i); + t.assertTrue(name + ".press2.callbacks.callback" + i); + t.assertTrue(name + ".release1.callbacks.callback" + i); + t.assertTrue(name + ".release2.callbacks.callback" + i); + } + } + } + + function runTestFlxMouseButton(test:TestShell, buttonID:FlxMouseButtonID, callbacks:Bool) + { + var button:FlxMouseButton = switch (buttonID) + { + case FlxMouseButtonID.LEFT: @:privateAccess FlxG.mouse._leftButton; + case FlxMouseButtonID.RIGHT: @:privateAccess FlxG.mouse._rightButton; + case FlxMouseButtonID.MIDDLE: @:privateAccess FlxG.mouse._middleButton; + default: null; + } + + var a = new FlxActionInputDigitalMouse(buttonID, FlxInputState.PRESSED); + var b = new FlxActionInputDigitalMouse(buttonID, FlxInputState.JUST_PRESSED); + var c = new FlxActionInputDigitalMouse(buttonID, FlxInputState.RELEASED); + var d = new FlxActionInputDigitalMouse(buttonID, FlxInputState.JUST_RELEASED); + + var clear = clearMouseButton.bind(button); + var click = clickMouseButton.bind(button); + + testInputStates(test, clear, click, a, b, c, d, callbacks); + } + + private function getGamepadButtons():Array + { + return + [ + FlxGamepadInputID.A, + FlxGamepadInputID.B, + FlxGamepadInputID.X, + FlxGamepadInputID.Y, + FlxGamepadInputID.LEFT_SHOULDER, + FlxGamepadInputID.RIGHT_SHOULDER, + FlxGamepadInputID.BACK, + FlxGamepadInputID.START, + FlxGamepadInputID.DPAD_UP, + FlxGamepadInputID.DPAD_DOWN, + FlxGamepadInputID.DPAD_LEFT, + FlxGamepadInputID.DPAD_RIGHT + ]; + + //Things that have to be tested separately: + // - LEFT/RIGHT_TRIGGER (digitized) and LEFT/RIGHT_TRIGGER_BUTTON + // - LEFT/RIGHT_STICK_DIGITAL_UP/DOWN/LEFT/RIGHT + } + + private function getFlxKeys():Array + { + //Trying to get these values directly from FlxG.keys.fromStringMap will cause the thing to hard crash whenever I try to do *ANY* logical test to exclude "ANY" from the returned array. + //It's really creepy and weird! + var arr = ["NUMPADSEVEN", "PERIOD", "ESCAPE", "A", "NUMPADEIGHT", "SIX", "B", "C", "D", "E", "ONE", "F", "LEFT", "G", "H", "ALT", "I", "J", "K", "CAPSLOCK", "L", "M", "N", "O", "P", "NUMPADTHREE", "SEMICOLON", "Q", "R", "S", "T", "NUMPADSIX", "U", "BACKSLASH", "V", "W", "X", "NUMPADONE", "Y", "Z", "UP", "QUOTE", "SLASH", "BACKSPACE", "HOME", "SHIFT", "DOWN", "F10", "F11", "FOUR", "SPACE", "F12", "ZERO", "PAGEUP", "F1", "DELETE", "F2", "TWO", "F3", "SEVEN", "F4", "F5", "EIGHT", "GRAVEACCENT", "F6", "NUMPADMULTIPLY", "F7", "PAGEDOWN", "F8", "FIVE", "NINE", "NUMPADFOUR", "F9", "TAB", "COMMA", "RBRACKET", "ENTER", "PRINTSCREEN", "INSERT", "END", "RIGHT", "LBRACKET", "CONTROL", "THREE", "NUMPADNINE", "NUMPADFIVE", "NUMPADTWO"]; + + //these values will hard crash the test and I don't know why + var problems = ["PLUS", "MINUS", "NUMPADPLUS", "NUMPADMINUS", "NUMPADPERIOD", "NUMPADZERO"]; + + var arr2 = []; + for (key in arr) + { + if (problems.indexOf(key) != -1) + { + arr2.push(key); + } + } + + return arr2; + } + + @Test + function testFlxKeyboard() + { + var keys = getFlxKeys(); + + for (key in keys) + { + var t = new TestShell(key + "."); + runTestFlxKeyboard(t, key, false); + + //Press & release w/o callbacks + t.assertTrue (key + ".press1.just"); + t.assertTrue (key + ".press1.value"); + t.assertFalse(key + ".press2.just"); + t.assertTrue (key + ".press2.value"); + t.assertTrue (key + ".release1.just"); + t.assertTrue (key + ".release1.value"); + t.assertFalse(key + ".release2.just"); + t.assertTrue (key + ".release2.value"); + + //Test "ANY" key input as well: + t.assertTrue (key + "any.press1.just"); + t.assertTrue (key + "any.press1.value"); + t.assertFalse(key + "any.press2.just"); + t.assertTrue (key + "any.press2.value"); + t.assertTrue (key + "any.release1.just"); + t.assertTrue (key + "any.release1.value"); + t.assertFalse(key + "any.release2.just"); + t.assertTrue (key + "any.release2.value"); + } + } + + @Test + function testFlxKeyboardCallbacks() + { + var keys = getFlxKeys(); + + for (key in keys) + { + var t = new TestShell(key + "."); + + runTestFlxKeyboard(t, key, true); + + //Press & release w/ callbacks + t.assertTrue (key + ".press1.callbacks.just"); + t.assertTrue (key + ".press1.callbacks.value"); + t.assertFalse(key + ".press2.callbacks.just"); + t.assertTrue (key + ".press2.callbacks.value"); + t.assertTrue (key + ".release1.callbacks.just"); + t.assertTrue (key + ".release1.callbacks.value"); + t.assertFalse(key + ".release2.callbacks.just"); + t.assertTrue (key + ".release2.callbacks.value"); + + //Test "ANY" key input as well: + t.assertTrue (key + "any.press1.callbacks.just"); + t.assertTrue (key + "any.press1.callbacks.value"); + t.assertFalse(key + "any.press2.callbacks.just"); + t.assertTrue (key + "any.press2.callbacks.value"); + t.assertTrue (key + "any.release1.callbacks.just"); + t.assertTrue (key + "any.release1.callbacks.value"); + t.assertFalse(key + "any.release2.callbacks.just"); + t.assertTrue (key + "any.release2.callbacks.value"); + + //Callbacks themselves (1-4: pressed, just_pressed, released, just_released) + for (i in 1...5) + { + t.assertTrue(key + ".press1.callbacks.callback" + i); + t.assertTrue(key + ".press2.callbacks.callback" + i); + t.assertTrue(key + ".release1.callbacks.callback" + i); + t.assertTrue(key + ".release2.callbacks.callback" + i); + + t.assertTrue(key + ".any.press1.callbacks.callback" + i); + t.assertTrue(key + ".any.press2.callbacks.callback" + i); + t.assertTrue(key + ".any.release1.callbacks.callback" + i); + t.assertTrue(key + ".any.release2.callbacks.callback" + i); + } + } + } + + function runTestFlxKeyboard(test:TestShell, key:FlxKey, callbacks:Bool) + { + var a = new FlxActionInputDigitalKeyboard(key, FlxInputState.PRESSED); + var b = new FlxActionInputDigitalKeyboard(key, FlxInputState.JUST_PRESSED); + var c = new FlxActionInputDigitalKeyboard(key, FlxInputState.RELEASED); + var d = new FlxActionInputDigitalKeyboard(key, FlxInputState.JUST_RELEASED); + + var aAny = new FlxActionInputDigitalKeyboard("ANY", FlxInputState.PRESSED); + var bAny = new FlxActionInputDigitalKeyboard("ANY", FlxInputState.JUST_PRESSED); + var cAny = new FlxActionInputDigitalKeyboard("ANY", FlxInputState.RELEASED); + var dAny = new FlxActionInputDigitalKeyboard("ANY", FlxInputState.JUST_RELEASED); + + var clear = clearFlxKey.bind(key); + var click = clickFlxKey.bind(key); + + testInputStates(test, clear, click, a, b, c, d, callbacks); + test.name = test.name + "any."; + testInputStates(test, clear, click, aAny, bAny, cAny, dAny, callbacks); + } + + @Test + function testFlxMouseWheel() + { + var polarities = + [ + {name:"positive", value:true}, + {name:"negative", value:false} + ]; + + for (polarity in polarities) + { + var name = polarity.name; + var value = polarity.value; + + var t = new TestShell(name + "."); + runTestFlxMouseWheel(t, value, false); + + //Press & release w/o callbacks + t.assertTrue (name + ".press1.just"); + t.assertTrue (name + ".press1.value"); + t.assertFalse(name + ".press2.just"); + t.assertTrue (name + ".press2.value"); + t.assertTrue (name + ".release1.just"); + t.assertTrue (name + ".release1.value"); + t.assertFalse(name + ".release2.just"); + t.assertTrue (name + ".release2.value"); + } + } + + @Test + function testFlxMouseWheelCallbacks() + { + var polarities = + [ + {name:"positive", value:true}, + {name:"negative", value:false} + ]; + + for (polarity in polarities) + { + var name = polarity.name; + var value = polarity.value; + + var t = new TestShell(name + "."); + runTestFlxMouseWheel(t, value, true); + + //Press & release w/ callbacks + t.assertTrue (name + ".press1.callbacks.just"); + t.assertTrue (name + ".press1.callbacks.value"); + t.assertFalse(name + ".press2.callbacks.just"); + t.assertTrue (name + ".press2.callbacks.value"); + t.assertTrue (name + ".release1.callbacks.just"); + t.assertTrue (name + ".release1.callbacks.value"); + t.assertFalse(name + ".release2.callbacks.just"); + t.assertTrue (name + ".release2.callbacks.value"); + + //Callbacks themselves (1-4: pressed, just_pressed, released, just_released) + for (i in 1...5) + { + t.assertTrue(name + ".press1.callbacks.callback" + i); + t.assertTrue(name + ".press2.callbacks.callback" + i); + t.assertTrue(name + ".release1.callbacks.callback" + i); + t.assertTrue(name + ".release2.callbacks.callback" + i); + } + } + } + + function runTestFlxMouseWheel(test:TestShell, positive:Bool, callbacks:Bool) + { + var a = new FlxActionInputDigitalMouseWheel(positive, FlxInputState.PRESSED); + var b = new FlxActionInputDigitalMouseWheel(positive, FlxInputState.JUST_PRESSED); + var c = new FlxActionInputDigitalMouseWheel(positive, FlxInputState.RELEASED); + var d = new FlxActionInputDigitalMouseWheel(positive, FlxInputState.JUST_RELEASED); + + var clear = clearFlxMouseWheel; + var move = moveFlxMouseWheel.bind(positive); + + testInputStates(test, clear, move, a, b, c, d, callbacks); + } + + #if (!flash && FLX_GAMEINPUT_API) + private function makeFakeGamepad() + { + var xinput = @:privateAccess new Gamepad(0); + @:privateAccess GameInput.__onGamepadConnect(xinput); + var gamepad = FlxG.gamepads.getByID(0); + gamepad.model = FlxGamepadModel.XINPUT; + var gid:GameInputDevice = @:privateAccess gamepad._device; + + @:privateAccess gid.id = "0"; + @:privateAccess gid.name = "xinput"; + + var control:GameInputControl = null; + + for (i in 0...6) + { + control = @:privateAccess new GameInputControl (gid, "AXIS_" + i, -1, 1); + @:privateAccess gid.__axis.set (i, control); + @:privateAccess gid.__controls.push (control); + } + + for (i in 0...15) + { + control = @:privateAccess new GameInputControl (gid, "BUTTON_" + i, 0, 1); + @:privateAccess gid.__button.set (i, control); + @:privateAccess gid.__controls.push (control); + } + + gamepad.update(); + } + #end + + @Test + function testFlxGamepad() + { + #if (!flash && FLX_GAMEINPUT_API) + makeFakeGamepad(); + #end + + var buttons = getGamepadButtons(); + + for (btn in buttons) + { + var t = new TestShell(btn + "."); + + runTestFlxGamepad(t, btn, false, false); + + //Press & release w/ callbacks + t.assertTrue (btn + ".press1.just"); + t.assertTrue (btn + ".press1.value"); + t.assertFalse(btn + ".press2.just"); + t.assertTrue (btn + ".press2.value"); + t.assertTrue (btn + ".release1.just"); + t.assertTrue (btn + ".release1.value"); + t.assertFalse(btn + ".release2.just"); + t.assertTrue (btn + ".release2.value"); + } + } + + @Test + function testFlxGamepadAny() + { + var buttons = getGamepadButtons(); + + for (btn in buttons) + { + var t = new TestShell(btn + ".any."); + + runTestFlxGamepad(t, btn, false, true); + + //Press & release w/ callbacks + //Test "ANY" button input + t.assertTrue (btn + ".any.press1.just"); + t.assertTrue (btn + ".any.press1.value"); + t.assertFalse(btn + ".any.press2.just"); + t.assertTrue (btn + ".any.press2.value"); + t.assertTrue (btn + ".any.release1.just"); + t.assertTrue (btn + ".any.release1.value"); + t.assertFalse(btn + ".any.release2.just"); + t.assertTrue (btn + ".any.release2.value"); + } + } + + @Test + function testFlxGamepadCallbacks() + { + var buttons = getGamepadButtons(); + + for (btn in buttons) + { + var t = new TestShell(btn + "."); + + runTestFlxGamepad(t, btn, true, false); + + //Press & release w/o callbacks + t.assertTrue (btn + ".press1.callbacks.just"); + t.assertTrue (btn + ".press1.callbacks.value"); + t.assertFalse(btn + ".press2.callbacks.just"); + t.assertTrue (btn + ".press2.callbacks.value"); + t.assertTrue (btn + ".release1.callbacks.just"); + t.assertTrue (btn + ".release1.callbacks.value"); + t.assertFalse(btn + ".release2.callbacks.just"); + t.assertTrue (btn + ".release2.callbacks.value"); + + //Callbacks themselves (1-4: pressed, just_pressed, released, just_released) + for (i in 1...5) + { + t.assertTrue(btn + ".press1.callbacks.callback" + i); + t.assertTrue(btn + ".press2.callbacks.callback" + i); + t.assertTrue(btn + ".release1.callbacks.callback" + i); + t.assertTrue(btn + ".release2.callbacks.callback" + i); + } + } + } + + @Test + function testFlxGamepadAnyCallbacks() + { + var buttons = getGamepadButtons(); + + for (btn in buttons) + { + var t = new TestShell(btn + ".any."); + + runTestFlxGamepad(t, btn, true, true); + + //Press & release w/ callbacks + //Test "ANY" button input + t.assertTrue (btn + ".any.press1.callbacks.just"); + t.assertTrue (btn + ".any.press1.callbacks.value"); + t.assertFalse(btn + ".any.press2.callbacks.just"); + t.assertTrue (btn + ".any.press2.callbacks.value"); + t.assertTrue (btn + ".any.release1.callbacks.just"); + t.assertTrue (btn + ".any.release1.callbacks.value"); + t.assertFalse(btn + ".any.release2.callbacks.just"); + t.assertTrue (btn + ".any.release2.callbacks.value"); + + //Callbacks themselves (1-4: pressed, just_pressed, released, just_released) + for (i in 1...5) + { + t.assertTrue(btn + ".any.press1.callbacks.callback" + i); + t.assertTrue(btn + ".any.press2.callbacks.callback" + i); + t.assertTrue(btn + ".any.release1.callbacks.callback" + i); + t.assertTrue(btn + ".any.release2.callbacks.callback" + i); + } + } + } + + function runTestFlxGamepad(test:TestShell, inputID:FlxGamepadInputID, callbacks:Bool, any:Bool) + { + var a:FlxActionInputDigitalGamepad; + var b:FlxActionInputDigitalGamepad; + var c:FlxActionInputDigitalGamepad; + var d:FlxActionInputDigitalGamepad; + + //NOTE: I found that gamepad tests can fail in unexpected ways for "RELEASED" actions if + //your gamepad ID is "FIRST_ACTIVE"... since "first active" means "the first gamepad with + //any non-released input" -- if there's no input on a given frame, then no gamepad is returned + //that frame to register the releases! a strict reading of the API perhaps would not see this as a bug? + //In any case, we might consider warning about this unexpected (bug logically valid?) edge case + //when using FIRST_ACTIVE as the gamepad ID and RELEASED/JUST_RELEASED as the trigger + + var stateGrid:InputStateGrid = null; + + if (!any) + { + a = new FlxActionInputDigitalGamepad(inputID, FlxInputState.PRESSED, 0); + b = new FlxActionInputDigitalGamepad(inputID, FlxInputState.JUST_PRESSED, 0); + c = new FlxActionInputDigitalGamepad(inputID, FlxInputState.RELEASED, 0); + d = new FlxActionInputDigitalGamepad(inputID, FlxInputState.JUST_RELEASED, 0); + } + else + { + a = new FlxActionInputDigitalGamepad(FlxGamepadInputID.ANY, FlxInputState.PRESSED, 0); + b = new FlxActionInputDigitalGamepad(FlxGamepadInputID.ANY, FlxInputState.JUST_PRESSED, 0); + c = new FlxActionInputDigitalGamepad(FlxGamepadInputID.ANY, FlxInputState.RELEASED, 0); + d = new FlxActionInputDigitalGamepad(FlxGamepadInputID.ANY, FlxInputState.JUST_RELEASED, 0); + + stateGrid = + { + press1 : [1, 1, 0, 1], + press2 : [1, 2, 0, 2], + release1 : [1, 2, 1, 3], + release2 : [1, 2, 1, 4] + }; + } + + #if FLX_JOYSTICK_API + + var clear = clearJoystick.bind(inputID); + var click = clickJoystick.bind(inputID); + testInputStates(test, clear, click, a, b, c, d, callbacks, stateGrid); + + #elseif (!flash && FLX_GAMEINPUT_API) + + var clear = clearGamepad.bind(inputID); + var click = clickGamepad.bind(inputID); + testInputStates(test, clear, click, a, b, c, d, callbacks, stateGrid); + + #end + } + + function getCallback(i:Int) + { + return function (a:FlxActionDigital) + { + onCallback(i); + } + } + + function testInputStates(test:TestShell, clear:Void->Void, click:Bool->Array->Void, pressed:FlxActionInputDigital, jPressed:FlxActionInputDigital, released:FlxActionInputDigital, jReleased:FlxActionInputDigital, testCallbacks:Bool, ?g:InputStateGrid) + { + if (g == null) + { + g = + { + press1 : [1, 1, 0, 0], + press2 : [1, 2, 0, 0], + release1 : [1, 2, 1, 1], + release2 : [1, 2, 1, 2] + }; + } + + var aPressed:FlxActionDigital; + var ajPressed:FlxActionDigital; + var aReleased:FlxActionDigital; + var ajReleased:FlxActionDigital; + + if (!testCallbacks) + { + ajPressed = new FlxActionDigital("jPressed", null); + aPressed = new FlxActionDigital("pressed", null); + ajReleased = new FlxActionDigital("jReleased", null); + aReleased = new FlxActionDigital("released", null); + } + else + { + ajPressed = new FlxActionDigital("jPressed", getCallback(0)); + aPressed = new FlxActionDigital("pressed", getCallback(1)); + ajReleased = new FlxActionDigital("jReleased", getCallback(2)); + aReleased = new FlxActionDigital("released", getCallback(3)); + } + + ajPressed.add(jPressed); + aPressed.add(pressed); + ajReleased.add(jReleased); + aReleased.add(released); + + var arr = [aPressed, ajPressed, aReleased, ajReleased]; + + clear(); + clearValues(); + + var callbackStr = (testCallbacks ? "callbacks." : ""); + + test.prefix = "press1." + callbackStr; + + //JUST PRESSED + click(true, arr); + + test.testBool(ajPressed.triggered, "just"); + test.testBool(aPressed.triggered, "value"); + if (testCallbacks) + { + test.testBool(value0 == g.press1[0], "callback1"); + test.testBool(value1 == g.press1[1], "callback2"); + test.testBool(value2 == g.press1[2], "callback3"); + test.testBool(value3 == g.press1[3], "callback4"); + } + + test.prefix = "press2." + callbackStr; + + //STILL PRESSED + click(true, arr); + + test.testBool(ajPressed.triggered, "just"); + test.testBool(aPressed.triggered, "value"); + if (testCallbacks) + { + test.testBool(value0 == g.press2[0], "callback1"); + test.testBool(value1 == g.press2[1], "callback2"); + test.testBool(value2 == g.press2[2], "callback3"); + test.testBool(value3 == g.press2[3], "callback4"); + } + + test.prefix = "release1." + callbackStr; + + //JUST RELEASED + click(false, arr); + + test.testBool(ajReleased.triggered, "just"); + test.testBool(aReleased.triggered, "value"); + if (testCallbacks) + { + test.testBool(value0 == g.release1[0], "callback1"); + test.testBool(value1 == g.release1[1], "callback2"); + test.testBool(value2 == g.release1[2], "callback3"); + test.testBool(value3 == g.release1[3], "callback4"); + } + + test.prefix = "release2." + callbackStr; + + //STILL RELEASED + click(false, arr); + + test.testBool(ajReleased.triggered, "just"); + test.testBool(aReleased.triggered, "value"); + if (testCallbacks) + { + test.testBool(value0 == g.release2[0], "callback1"); + test.testBool(value1 == g.release2[1], "callback2"); + test.testBool(value2 == g.release2[2], "callback3"); + test.testBool(value3 == g.release2[3], "callback4"); + } + + clear(); + clearValues(); + + aPressed.destroy(); + aReleased.destroy(); + ajPressed.destroy(); + ajReleased.destroy(); + } + + #if FLX_JOYSTICK_API + private function clearJoystick(ID:FlxGamepadInputID) + { + FlxG.stage.dispatchEvent(new JoystickEvent(JoystickEvent.BUTTON_UP, false, false, 0, ID, 0, 0, 0)); + step(); + } + #end + + #if (!flash && FLX_GAMEINPUT_API) + @:access(flixel.input.gamepad.FlxGamepad) + private function clearGamepad(ID:FlxGamepadInputID) + { + var gamepad = FlxG.gamepads.getByID(0); + if (gamepad == null) return; + var input:FlxInput = gamepad.buttons[gamepad.mapping.getRawID(ID)]; + if (input == null) return; + input.release(); + step(); + gamepad.update(); + step(); + gamepad.update(); + } + #end + + @:access(flixel.input.mouse.FlxMouse) + private function clearFlxMouseWheel() + { + if (FlxG.mouse == null) return; + FlxG.mouse.wheel = 0; + step(); + step(); + } + + @:access(flixel.input.FlxKeyManager) + private function clearFlxKey(key:FlxKey) + { + var input:FlxInput = FlxG.keys._keyListMap.get(key); + if (input == null) return; + input.release(); + step(); + input.update(); + step(); + input.update(); + } + + private function clearMouseButton(button:FlxMouseButton) + { + if (button == null) return; + button.release(); + step(); + button.update(); + step(); + button.update(); + } + + private function clearFlxInput(thing:FlxInput) + { + if (thing == null) return; + thing.release(); + step(); + thing.update(); + step(); + thing.update(); + } + + #if FLX_JOYSTICK_API + private function clickJoystick(ID:FlxGamepadInputID, pressed:Bool, arr:Array) + { + var event = pressed ? JoystickEvent.BUTTON_DOWN : JoystickEvent.BUTTON_UP; + FlxG.stage.dispatchEvent(new JoystickEvent(event, false, false, 0, ID, 0, 0, 0)); + + step(); + updateActions(arr); + } + #end + + #if (!flash && FLX_GAMEINPUT_API) + @:access(flixel.input.gamepad.FlxGamepad) + private function clickGamepad(ID:FlxGamepadInputID, pressed:Bool, arr:Array) + { + var gamepad = FlxG.gamepads.getByID(0); + if (gamepad == null) return; + + var button:FlxGamepadButton = gamepad.buttons[gamepad.mapping.getRawID(ID)]; + if (button == null) return; + + if (pressed) button.press(); + else button.release(); + + updateActions(arr); + step(); + } + #end + + @:access(flixel.input.mouse.FlxMouse) + private function moveFlxMouseWheel(positive:Bool, pressed:Bool, arr:Array) + { + if (FlxG.mouse == null) return; + if (pressed) + { + if (positive) + { + FlxG.mouse.wheel = 1; + } + else + { + FlxG.mouse.wheel = -1; + } + } + else + { + FlxG.mouse.wheel = 0; + } + updateActions(arr); + step(); + } + + @:access(flixel.input.FlxKeyManager) + private function clickFlxKey(key:FlxKey, pressed:Bool, arr:Array) + { + if (FlxG.keys == null || FlxG.keys._keyListMap == null) return; + + var input:FlxInput = FlxG.keys._keyListMap.get(key); + if (input == null) return; + + step(); + + input.update(); + + if (pressed) + { + input.press(); + } + else + { + input.release(); + } + + updateActions(arr); + + } + + private function clickMouseButton(button:FlxMouseButton, pressed:Bool, arr:Array) + { + if (button == null) return; + step(); + button.update(); + if (pressed) button.press(); + else button.release(); + updateActions(arr); + } + + private function clickFlxInput(thing:FlxInput, pressed:Bool, arr:Array) + { + if (thing == null) return; + step(); + thing.update(); + if (pressed) thing.press(); + else thing.release(); + updateActions(arr); + } + + private function updateActions(arr:Array) + { + for (a in arr) + { + if (a == null) continue; + a.update(); + } + } + + private function onCallback(i:Int) + { + switch (i) + { + case 0: value0++; + case 1: value1++; + case 2: value2++; + case 3: value3++; + } + } + + private function clearValues() + { + value0 = value1 = value2 = value3 = 0; + } +} + +typedef InputStateGrid = +{ + press1:Array, + press2:Array, + release1:Array, + release2:Array +} \ No newline at end of file diff --git a/tests/unit/src/flixel/input/actions/FlxActionInputTest.hx b/tests/unit/src/flixel/input/actions/FlxActionInputTest.hx new file mode 100644 index 0000000000..e597537258 --- /dev/null +++ b/tests/unit/src/flixel/input/actions/FlxActionInputTest.hx @@ -0,0 +1,57 @@ +package flixel.input.actions; + +import flixel.input.FlxInput.FlxInputState; +import flixel.input.actions.FlxActionInput.FlxInputDevice; +import flixel.input.actions.FlxActionInput.FlxInputType; + +import massive.munit.Assert; + +class FlxActionInputTest extends FlxTest +{ + @Before + function before() {} + + @Test + function testCompareState() + { + var fake = new FakeFlxActionInput(); + + inline function compare(a:FlxInputState, b:FlxInputState):Bool + { + return fake.testCompareState(a, b); + } + + Assert.isTrue (compare(FlxInputState.PRESSED, FlxInputState.PRESSED)); + Assert.isTrue (compare(FlxInputState.PRESSED, FlxInputState.JUST_PRESSED)); + Assert.isFalse(compare(FlxInputState.PRESSED, FlxInputState.RELEASED)); + Assert.isFalse(compare(FlxInputState.PRESSED, FlxInputState.JUST_RELEASED)); + + Assert.isTrue (compare(FlxInputState.RELEASED, FlxInputState.RELEASED)); + Assert.isTrue (compare(FlxInputState.RELEASED, FlxInputState.JUST_RELEASED)); + Assert.isFalse(compare(FlxInputState.RELEASED, FlxInputState.PRESSED)); + Assert.isFalse(compare(FlxInputState.RELEASED, FlxInputState.JUST_PRESSED)); + + Assert.isTrue (compare(FlxInputState.JUST_RELEASED, FlxInputState.JUST_RELEASED)); + Assert.isFalse(compare(FlxInputState.JUST_RELEASED, FlxInputState.RELEASED)); + Assert.isFalse(compare(FlxInputState.JUST_RELEASED, FlxInputState.PRESSED)); + Assert.isFalse(compare(FlxInputState.JUST_RELEASED, FlxInputState.JUST_PRESSED)); + + Assert.isTrue (compare(FlxInputState.JUST_PRESSED, FlxInputState.JUST_PRESSED)); + Assert.isFalse(compare(FlxInputState.JUST_PRESSED, FlxInputState.RELEASED)); + Assert.isFalse(compare(FlxInputState.JUST_PRESSED, FlxInputState.PRESSED)); + Assert.isFalse(compare(FlxInputState.JUST_PRESSED, FlxInputState.JUST_RELEASED)); + } +} + +class FakeFlxActionInput extends FlxActionInput +{ + public function new() + { + super(FlxInputType.DIGITAL, FlxInputDevice.OTHER, 0, FlxInputState.PRESSED); + } + + public function testCompareState(a:FlxInputState, b:FlxInputState):Bool + { + return compareState(a, b); + } +} \ No newline at end of file diff --git a/tests/unit/src/flixel/input/actions/FlxActionManagerTest.hx b/tests/unit/src/flixel/input/actions/FlxActionManagerTest.hx new file mode 100644 index 0000000000..70c30e92f8 --- /dev/null +++ b/tests/unit/src/flixel/input/actions/FlxActionManagerTest.hx @@ -0,0 +1,1004 @@ +package flixel.input.actions; + +#if FLX_GAMEINPUT_API +import openfl.ui.GameInput; +import openfl.ui.GameInputDevice; +import openfl.ui.GameInputControl; +import lime.ui.Gamepad; +#elseif FLX_JOYSTICK_API +import openfl.events.JoystickEvent; +#end +import flixel.input.FlxInput.FlxInputState; +import flixel.input.actions.FlxAction.FlxActionAnalog; +import flixel.input.actions.FlxAction.FlxActionDigital; +import flixel.input.actions.FlxActionInput.FlxInputDevice; +import flixel.input.actions.FlxActionInputAnalog.FlxActionInputAnalogMouseMotion; +import flixel.input.actions.FlxActionInputDigital.FlxActionInputDigitalKeyboard; +import flixel.input.gamepad.FlxGamepad.FlxGamepadModel; +import flixel.input.keyboard.FlxKey; +import haxe.Json; +import flixel.input.actions.FlxActionInput.FlxInputDeviceID; +import steamwrap.data.ControllerConfig; + +import massive.munit.Assert; + +class FlxActionManagerTest extends FlxTest +{ + #if FLX_STEAMWRAP + private var steamManager:FlxActionManager; + #end + + var basicManager:FlxActionManager; + var sets:Array; + var analog:Array>; + var digital:Array>; + + var valueTest = ""; + var connectStr:String = ""; + var disconnectStr:String = ""; + + @Before + function before() + { + createFlxActionManager(); + sets = + [ + "MenuControls", + "MapControls", + "BattleControls" + ]; + analog = + [ + ["menu_move"], + ["scroll_map", "move_map"], + ["move"] + ]; + digital = [ + ["menu_up", "menu_down", "menu_left", "menu_right", "menu_select", "menu_menu", "menu_cancel", "menu_thing_1", "menu_thing_2", "menu_thing_3"], + ["map_select", "map_exit", "map_menu", "map_journal"], + ["punch", "kick", "jump"] + ]; + } + + @Test + function testInit() + { + Assert.isTrue(basicManager.numSets == 3); + + var t = new TestShell("init."); + + runFlxActionManagerInit(t); + + t.assertTrue("init.MenuControls.indexExists"); + t.assertTrue("init.MenuControls.nameMatches"); + t.assertTrue("init.MenuControls.setExists"); + + t.assertTrue("init.MapControls.indexExists"); + t.assertTrue("init.MapControls.nameMatches"); + t.assertTrue("init.MapControls.setExists"); + + t.assertTrue("init.BattleControls.indexExists"); + t.assertTrue("init.BattleControls.nameMatches"); + t.assertTrue("init.BattleControls.setExists"); + + t.destroy(); + } + + #if FLX_STEAMWRAP + @Test + function testInitSteam() + { + Assert.isTrue(steamManager.numSets == 3); + + var t = new TestShell("steam.init."); + + runFlxActionManagerInit(t, steamManager); + + t.assertTrue("steam.init.MenuControls.indexExists"); + t.assertTrue("steam.init.MenuControls.nameMatches"); + t.assertTrue("steam.init.MenuControls.setExists"); + + t.assertTrue("steam.init.MapControls.indexExists"); + t.assertTrue("steam.init.MapControls.nameMatches"); + t.assertTrue("steam.init.MapControls.setExists"); + + t.assertTrue("steam.init.BattleControls.indexExists"); + t.assertTrue("steam.init.BattleControls.nameMatches"); + t.assertTrue("steam.init.BattleControls.setExists"); + + t.destroy(); + } + #end + + @Test + function testActions() + { + var t = new TestShell("actions."); + + runFlxActionManagerActions(t); + + t.assertTrue("actions.MenuControls.hasDigital"); + t.assertTrue("actions.MenuControls.hasAnalog"); + t.assertTrue("actions.MenuControls.digital.menu_up.exists"); + t.assertTrue("actions.MenuControls.digital.menu_down.exists"); + t.assertTrue("actions.MenuControls.digital.menu_left.exists"); + t.assertTrue("actions.MenuControls.digital.menu_right.exists"); + t.assertTrue("actions.MenuControls.digital.menu_select.exists"); + t.assertTrue("actions.MenuControls.digital.menu_menu.exists"); + t.assertTrue("actions.MenuControls.digital.menu_cancel.exists"); + t.assertTrue("actions.MenuControls.digital.menu_thing_1.exists"); + t.assertTrue("actions.MenuControls.digital.menu_thing_2.exists"); + t.assertTrue("actions.MenuControls.digital.menu_thing_3.exists"); + t.assertTrue("actions.MenuControls.analog.menu_move.exists"); + + t.assertTrue("actions.MapControls.hasDigital"); + t.assertTrue("actions.MapControls.hasAnalog"); + t.assertTrue("actions.MapControls.digital.map_select.exists"); + t.assertTrue("actions.MapControls.digital.map_exit.exists"); + t.assertTrue("actions.MapControls.digital.map_menu.exists"); + t.assertTrue("actions.MapControls.digital.map_journal.exists"); + t.assertTrue("actions.MapControls.analog.scroll_map.exists"); + t.assertTrue("actions.MapControls.analog.move_map.exists"); + + t.assertTrue("actions.BattleControls.hasDigital"); + t.assertTrue("actions.BattleControls.hasAnalog"); + t.assertTrue("actions.BattleControls.digital.punch.exists"); + t.assertTrue("actions.BattleControls.digital.kick.exists"); + t.assertTrue("actions.BattleControls.digital.jump.exists"); + t.assertTrue("actions.BattleControls.analog.move.exists"); + + t.destroy(); + } + + #if FLX_STEAMWRAP + @Test + function testActionsSteam() + { + var t = new TestShell("steam.actions."); + + runFlxActionManagerActions(t, steamManager); + + t.assertTrue("steam.actions.MenuControls.hasDigital"); + t.assertTrue("steam.actions.MenuControls.hasAnalog"); + t.assertTrue("steam.actions.MenuControls.digital.menu_up.exists"); + t.assertTrue("steam.actions.MenuControls.digital.menu_down.exists"); + t.assertTrue("steam.actions.MenuControls.digital.menu_left.exists"); + t.assertTrue("steam.actions.MenuControls.digital.menu_right.exists"); + t.assertTrue("steam.actions.MenuControls.digital.menu_select.exists"); + t.assertTrue("steam.actions.MenuControls.digital.menu_menu.exists"); + t.assertTrue("steam.actions.MenuControls.digital.menu_cancel.exists"); + t.assertTrue("steam.actions.MenuControls.digital.menu_thing_1.exists"); + t.assertTrue("steam.actions.MenuControls.digital.menu_thing_2.exists"); + t.assertTrue("steam.actions.MenuControls.digital.menu_thing_3.exists"); + t.assertTrue("steam.actions.MenuControls.analog.menu_move.exists"); + + t.assertTrue("steam.actions.MapControls.hasDigital"); + t.assertTrue("steam.actions.MapControls.hasAnalog"); + t.assertTrue("steam.actions.MapControls.digital.map_select.exists"); + t.assertTrue("steam.actions.MapControls.digital.map_exit.exists"); + t.assertTrue("steam.actions.MapControls.digital.map_menu.exists"); + t.assertTrue("steam.actions.MapControls.digital.map_journal.exists"); + t.assertTrue("steam.actions.MapControls.analog.scroll_map.exists"); + t.assertTrue("steam.actions.MapControls.analog.move_map.exists"); + + t.assertTrue("steam.actions.BattleControls.hasDigital"); + t.assertTrue("steam.actions.BattleControls.hasAnalog"); + t.assertTrue("steam.actions.BattleControls.digital.punch.exists"); + t.assertTrue("steam.actions.BattleControls.digital.kick.exists"); + t.assertTrue("steam.actions.BattleControls.digital.jump.exists"); + t.assertTrue("steam.actions.BattleControls.analog.move.exists"); + + t.destroy(); + } + #end + + @Test + function testAddRemove() + { + var t = new TestShell("addRemove."); + + runFlxActionManagerAddRemove(t); + + t.assertTrue("addRemove.MenuControls.digital.extra.add"); + t.assertTrue("addRemove.MenuControls.digital.extra.remove"); + t.assertTrue("addRemove.MenuControls.analog.extra.add"); + t.assertTrue("addRemove.MenuControls.analog.extra.remove"); + + t.assertTrue("addRemove.MapControls.digital.extra.add"); + t.assertTrue("addRemove.MapControls.digital.extra.remove"); + t.assertTrue("addRemove.MapControls.analog.extra.add"); + t.assertTrue("addRemove.MapControls.analog.extra.remove"); + + t.assertTrue("addRemove.BattleControls.digital.extra.add"); + t.assertTrue("addRemove.BattleControls.digital.extra.remove"); + t.assertTrue("addRemove.BattleControls.analog.extra.add"); + t.assertTrue("addRemove.BattleControls.analog.extra.remove"); + + t.destroy(); + } + + @Test + function testMouse() + { + var t = new TestShell("device."); + + runFlxActionManagerDevice(MOUSE, t); + + t.assertTrue("device.MenuControls.activatedFor.MOUSE"); + t.assertTrue("device.MenuControls.notActivatedFor.KEYBOARD.but.MOUSE"); + t.assertTrue("device.MenuControls.notActivatedFor.GAMEPAD.but.MOUSE"); + t.assertTrue("device.MenuControls.deactivatedFor.MOUSE"); + t.assertTrue("device.MenuControls.stillNotActivatedFor.KEYBOARD.but.MOUSE"); + t.assertTrue("device.MenuControls.stillNotActivatedFor.GAMEPAD.but.MOUSE"); + + t.assertTrue("device.MapControls.activatedFor.MOUSE"); + t.assertTrue("device.MapControls.notActivatedFor.KEYBOARD.but.MOUSE"); + t.assertTrue("device.MapControls.notActivatedFor.GAMEPAD.but.MOUSE"); + t.assertTrue("device.MapControls.deactivatedFor.MOUSE"); + t.assertTrue("device.MapControls.stillNotActivatedFor.KEYBOARD.but.MOUSE"); + t.assertTrue("device.MapControls.stillNotActivatedFor.GAMEPAD.but.MOUSE"); + + t.assertTrue("device.BattleControls.activatedFor.MOUSE"); + t.assertTrue("device.BattleControls.notActivatedFor.KEYBOARD.but.MOUSE"); + t.assertTrue("device.BattleControls.notActivatedFor.GAMEPAD.but.MOUSE"); + t.assertTrue("device.BattleControls.deactivatedFor.MOUSE"); + t.assertTrue("device.BattleControls.stillNotActivatedFor.KEYBOARD.but.MOUSE"); + t.assertTrue("device.BattleControls.stillNotActivatedFor.GAMEPAD.but.MOUSE"); + + #if FLX_STEAMWRAP + t.assertTrue("device.MenuControls.notActivatedFor.STEAM_CONTROLLER.but.MOUSE"); + t.assertTrue("device.MenuControls.stillNotActivatedFor.STEAM_CONTROLLER.but.MOUSE"); + t.assertTrue("device.MapControls.notActivatedFor.STEAM_CONTROLLER.but.MOUSE"); + t.assertTrue("device.MapControls.stillNotActivatedFor.STEAM_CONTROLLER.but.MOUSE"); + t.assertTrue("device.BattleControls.notActivatedFor.STEAM_CONTROLLER.but.MOUSE"); + t.assertTrue("device.BattleControls.stillNotActivatedFor.STEAM_CONTROLLER.but.MOUSE"); + #end + + t.destroy(); + } + + @Test + function testKeyboard() + { + var t = new TestShell("device."); + + runFlxActionManagerDevice(KEYBOARD, t); + + t.assertTrue("device.MenuControls.activatedFor.KEYBOARD"); + t.assertTrue("device.MenuControls.notActivatedFor.MOUSE.but.KEYBOARD"); + t.assertTrue("device.MenuControls.notActivatedFor.GAMEPAD.but.KEYBOARD"); + t.assertTrue("device.MenuControls.deactivatedFor.KEYBOARD"); + t.assertTrue("device.MenuControls.stillNotActivatedFor.MOUSE.but.KEYBOARD"); + t.assertTrue("device.MenuControls.stillNotActivatedFor.GAMEPAD.but.KEYBOARD"); + + t.assertTrue("device.MapControls.activatedFor.KEYBOARD"); + t.assertTrue("device.MapControls.notActivatedFor.MOUSE.but.KEYBOARD"); + t.assertTrue("device.MapControls.notActivatedFor.GAMEPAD.but.KEYBOARD"); + t.assertTrue("device.MapControls.deactivatedFor.KEYBOARD"); + t.assertTrue("device.MapControls.stillNotActivatedFor.MOUSE.but.KEYBOARD"); + t.assertTrue("device.MapControls.stillNotActivatedFor.GAMEPAD.but.KEYBOARD"); + + t.assertTrue("device.BattleControls.activatedFor.KEYBOARD"); + t.assertTrue("device.BattleControls.notActivatedFor.MOUSE.but.KEYBOARD"); + t.assertTrue("device.BattleControls.notActivatedFor.GAMEPAD.but.KEYBOARD"); + t.assertTrue("device.BattleControls.deactivatedFor.KEYBOARD"); + t.assertTrue("device.BattleControls.stillNotActivatedFor.MOUSE.but.KEYBOARD"); + t.assertTrue("device.BattleControls.stillNotActivatedFor.GAMEPAD.but.KEYBOARD"); + + #if FLX_STEAMWRAP + t.assertTrue("device.MenuControls.notActivatedFor.STEAM_CONTROLLER.but.KEYBOARD"); + t.assertTrue("device.MenuControls.stillNotActivatedFor.STEAM_CONTROLLER.but.KEYBOARD"); + t.assertTrue("device.MapControls.notActivatedFor.STEAM_CONTROLLER.but.KEYBOARD"); + t.assertTrue("device.MapControls.stillNotActivatedFor.STEAM_CONTROLLER.but.KEYBOARD"); + t.assertTrue("device.BattleControls.notActivatedFor.STEAM_CONTROLLER.but.KEYBOARD"); + t.assertTrue("device.BattleControls.stillNotActivatedFor.STEAM_CONTROLLER.but.KEYBOARD"); + #end + + t.destroy(); + } + + @Test + function testGamepad() + { + #if flash + return; + #end + + var t = new TestShell("device."); + + runFlxActionManagerDevice(GAMEPAD, t); + + t.assertTrue("device.MenuControls.activatedFor.GAMEPAD"); + t.assertTrue("device.MenuControls.notActivatedFor.MOUSE.but.GAMEPAD"); + t.assertTrue("device.MenuControls.notActivatedFor.KEYBOARD.but.GAMEPAD"); + t.assertTrue("device.MenuControls.deactivatedFor.GAMEPAD"); + t.assertTrue("device.MenuControls.stillNotActivatedFor.MOUSE.but.GAMEPAD"); + t.assertTrue("device.MenuControls.stillNotActivatedFor.KEYBOARD.but.GAMEPAD"); + + t.assertTrue("device.MapControls.activatedFor.GAMEPAD"); + t.assertTrue("device.MapControls.notActivatedFor.MOUSE.but.GAMEPAD"); + t.assertTrue("device.MapControls.notActivatedFor.KEYBOARD.but.GAMEPAD"); + t.assertTrue("device.MapControls.deactivatedFor.GAMEPAD"); + t.assertTrue("device.MapControls.stillNotActivatedFor.MOUSE.but.GAMEPAD"); + t.assertTrue("device.MapControls.stillNotActivatedFor.KEYBOARD.but.GAMEPAD"); + + t.assertTrue("device.BattleControls.activatedFor.GAMEPAD"); + t.assertTrue("device.BattleControls.notActivatedFor.MOUSE.but.GAMEPAD"); + t.assertTrue("device.BattleControls.notActivatedFor.KEYBOARD.but.GAMEPAD"); + t.assertTrue("device.BattleControls.deactivatedFor.GAMEPAD"); + t.assertTrue("device.BattleControls.stillNotActivatedFor.MOUSE.but.GAMEPAD"); + t.assertTrue("device.BattleControls.stillNotActivatedFor.KEYBOARD.but.GAMEPAD"); + + #if FLX_STEAMWRAP + t.assertTrue("device.MenuControls.notActivatedFor.STEAM_CONTROLLER.but.GAMEPAD"); + t.assertTrue("device.MenuControls.stillNotActivatedFor.STEAM_CONTROLLER.but.GAMEPAD"); + t.assertTrue("device.MapControls.notActivatedFor.STEAM_CONTROLLER.but.GAMEPAD"); + t.assertTrue("device.MapControls.stillNotActivatedFor.STEAM_CONTROLLER.but.GAMEPAD"); + t.assertTrue("device.BattleControls.notActivatedFor.STEAM_CONTROLLER.but.GAMEPAD"); + t.assertTrue("device.BattleControls.stillNotActivatedFor.STEAM_CONTROLLER.but.GAMEPAD"); + #end + + t.destroy(); + } + + #if FLX_STEAMWRAP + @Test + function testSteamController() + { + var t = new TestShell("device."); + + runFlxActionManagerDevice(STEAM_CONTROLLER, t); + + t.assertTrue("device.MenuControls.activatedFor.STEAM_CONTROLLER"); + t.assertTrue("device.MenuControls.notActivatedFor.MOUSE.but.STEAM_CONTROLLER"); + t.assertTrue("device.MenuControls.notActivatedFor.KEYBOARD.but.STEAM_CONTROLLER"); + t.assertTrue("device.MenuControls.notActivatedFor.GAMEPAD.but.STEAM_CONTROLLER"); + t.assertTrue("device.MenuControls.deactivatedFor.STEAM_CONTROLLER"); + t.assertTrue("device.MenuControls.stillNotActivatedFor.MOUSE.but.STEAM_CONTROLLER"); + t.assertTrue("device.MenuControls.stillNotActivatedFor.KEYBOARD.but.STEAM_CONTROLLER"); + t.assertTrue("device.MenuControls.stillNotActivatedFor.GAMEPAD.but.STEAM_CONTROLLER"); + + t.assertTrue("device.MapControls.activatedFor.STEAM_CONTROLLER"); + t.assertTrue("device.MapControls.notActivatedFor.MOUSE.but.STEAM_CONTROLLER"); + t.assertTrue("device.MapControls.notActivatedFor.KEYBOARD.but.STEAM_CONTROLLER"); + t.assertTrue("device.MapControls.notActivatedFor.GAMEPAD.but.STEAM_CONTROLLER"); + t.assertTrue("device.MapControls.deactivatedFor.STEAM_CONTROLLER"); + t.assertTrue("device.MapControls.stillNotActivatedFor.MOUSE.but.STEAM_CONTROLLER"); + t.assertTrue("device.MapControls.stillNotActivatedFor.KEYBOARD.but.STEAM_CONTROLLER"); + t.assertTrue("device.MapControls.stillNotActivatedFor.GAMEPAD.but.STEAM_CONTROLLER"); + + t.assertTrue("device.BattleControls.activatedFor.STEAM_CONTROLLER"); + t.assertTrue("device.BattleControls.notActivatedFor.MOUSE.but.STEAM_CONTROLLER"); + t.assertTrue("device.BattleControls.notActivatedFor.KEYBOARD.but.STEAM_CONTROLLER"); + t.assertTrue("device.BattleControls.notActivatedFor.GAMEPAD.but.STEAM_CONTROLLER"); + t.assertTrue("device.BattleControls.deactivatedFor.STEAM_CONTROLLER"); + t.assertTrue("device.BattleControls.stillNotActivatedFor.MOUSE.but.STEAM_CONTROLLER"); + t.assertTrue("device.BattleControls.stillNotActivatedFor.KEYBOARD.but.STEAM_CONTROLLER"); + t.assertTrue("device.BattleControls.stillNotActivatedFor.GAMEPAD.but.STEAM_CONTROLLER"); + + t.destroy(); + } + #end + + @Test + function testAllDevices() + { + var t = new TestShell("device."); + + runFlxActionManagerDevice(ALL, t); + + t.assertTrue("device.MenuControls.activatedFor.ALL"); + t.assertTrue("device.MenuControls.activatedForAll.MOUSE"); + t.assertTrue("device.MenuControls.activatedForAll.KEYBOARD"); + t.assertTrue("device.MenuControls.activatedForAll.GAMEPAD"); + t.assertTrue("device.MenuControls.deactivatedFor.ALL"); + t.assertTrue("device.MenuControls.deactivatedForAll.MOUSE"); + t.assertTrue("device.MenuControls.deactivatedForAll.KEYBOARD"); + t.assertTrue("device.MenuControls.deactivatedForAll.GAMEPAD"); + + t.assertTrue("device.MapControls.activatedFor.ALL"); + t.assertTrue("device.MapControls.activatedForAll.MOUSE"); + t.assertTrue("device.MapControls.activatedForAll.KEYBOARD"); + t.assertTrue("device.MapControls.activatedForAll.GAMEPAD"); + t.assertTrue("device.MapControls.deactivatedFor.ALL"); + t.assertTrue("device.MapControls.deactivatedForAll.MOUSE"); + t.assertTrue("device.MapControls.deactivatedForAll.KEYBOARD"); + t.assertTrue("device.MapControls.deactivatedForAll.GAMEPAD"); + + t.assertTrue("device.BattleControls.activatedFor.ALL"); + t.assertTrue("device.BattleControls.activatedForAll.MOUSE"); + t.assertTrue("device.BattleControls.activatedForAll.KEYBOARD"); + t.assertTrue("device.BattleControls.activatedForAll.GAMEPAD"); + t.assertTrue("device.BattleControls.deactivatedFor.ALL"); + t.assertTrue("device.BattleControls.deactivatedForAll.MOUSE"); + t.assertTrue("device.BattleControls.deactivatedForAll.KEYBOARD"); + t.assertTrue("device.BattleControls.deactivatedForAll.GAMEPAD"); + + #if FLX_STEAMWRAP + t.assertTrue("device.MenuControls.activatedForAll.STEAM_CONTROLLER"); + t.assertTrue("device.MenuControls.deactivatedForAll.STEAM_CONTROLLER"); + t.assertTrue("device.MapControls.activatedForAll.STEAM_CONTROLLER"); + t.assertTrue("device.MapControls.deactivatedForAll.STEAM_CONTROLLER"); + t.assertTrue("device.BattleControls.activatedForAll.STEAM_CONTROLLER"); + t.assertTrue("device.BattleControls.deactivatedForAll.STEAM_CONTROLLER"); + #end + + t.destroy(); + } + + @Test + function testDeviceConnectedDisconnected() + { + #if flash + return; + #end + + var testManager = new FlxActionManager(); + var managerText = '{"actionSets":[{"name":"MenuControls","analogActions":["menu_move"],"digitalActions":["menu_up","menu_down","menu_left","menu_right","menu_select","menu_menu","menu_cancel","menu_thing_1","menu_thing_2","menu_thing_3"]},{"name":"MapControls","analogActions":["scroll_map","move_map"],"digitalActions":["map_select","map_exit","map_menu","map_journal"]},{"name":"BattleControls","analogActions":["move"],"digitalActions":["punch","kick","jump"]}]}'; + var actionsJSON = Json.parse(managerText); + testManager.initFromJSON(actionsJSON, null, null); + + var menuSet:Int = testManager.getSetIndex("MenuControls"); + testManager.activateSet(menuSet, FlxInputDevice.GAMEPAD, FlxInputDeviceID.ALL); + + connectStr = ""; + disconnectStr = ""; + + testManager.deviceConnected.add(deviceConnected); + testManager.deviceDisconnected.add(deviceRemoved); + + #if FLX_JOYSTICK_API + + FlxG.stage.dispatchEvent(new JoystickEvent(JoystickEvent.DEVICE_ADDED, false, false, 0, 0, 0, 0, 0)); + Assert.isTrue(connectStr == "gamepad_0_xinput"); + FlxG.stage.dispatchEvent(new JoystickEvent(JoystickEvent.DEVICE_ADDED, false, false, 1, 0, 2, 0, 0)); + Assert.isTrue(connectStr == "gamepad_0_xinput,gamepad_1_ps4"); + FlxG.stage.dispatchEvent(new JoystickEvent(JoystickEvent.DEVICE_REMOVED, false, false, 0, 0, 0, 0, 0)); + + #elseif (!flash && FLX_GAMEINPUT_API) + + //The model identifiers say "unknown" here because we're not able to spoof all the way down to SDL, from which the gamepads originate + + var xinput = makeFakeGamepad("0", "xinput", FlxGamepadModel.XINPUT); + Assert.isTrue(connectStr == "gamepad_1_unknown"); + + var ps4 = makeFakeGamepad("1", "wireless controller", FlxGamepadModel.PS4); + Assert.isTrue(connectStr == "gamepad_1_unknown,gamepad_2_unknown"); + + removeGamepad(xinput); + Assert.isTrue(disconnectStr == "gamepad_1_unknown"); + + removeGamepad(ps4); + Assert.isTrue(disconnectStr == "gamepad_1_unknown,gamepad_2_unknown"); + + #end + + testManager.deviceConnected.remove(deviceConnected); + testManager.deviceDisconnected.remove(deviceRemoved); + } + + private function deviceConnected(Device:FlxInputDevice, ID:Int, Model:String) + { + if (connectStr != "") connectStr += ","; + connectStr += (Std.string(Device) + "_" + ID + "_" + Model).toLowerCase(); + } + + private function deviceRemoved(Device:FlxInputDevice, ID:Int, Model:String) + { + if (disconnectStr != "") disconnectStr += ","; + disconnectStr += (Std.string(Device) + "_" + ID + "_" + Model).toLowerCase(); + } + + #if (!flash && FLX_GAMEINPUT_API) + private function removeGamepad(g:Gamepad) + { + @:privateAccess GameInput.__onGamepadDisconnect(g); + } + + private function makeFakeGamepad(id:String, name:String, model:FlxGamepadModel):Gamepad + { + var limegamepad = @:privateAccess new Gamepad(0); + @:privateAccess GameInput.__onGamepadConnect(limegamepad); + var gamepad = FlxG.gamepads.getByID(0); + gamepad.model = model; + var gid:GameInputDevice = @:privateAccess gamepad._device; + + @:privateAccess gid.id = id; + @:privateAccess gid.name = name; + + var control:GameInputControl = null; + + for (i in 0...6) + { + control = @:privateAccess new GameInputControl (gid, "AXIS_" + i, -1, 1); + @:privateAccess gid.__axis.set (i, control); + @:privateAccess gid.__controls.push (control); + } + + for (i in 0...15) + { + control = @:privateAccess new GameInputControl (gid, "BUTTON_" + i, 0, 1); + @:privateAccess gid.__button.set (i, control); + @:privateAccess gid.__controls.push (control); + } + + gamepad.update(); + return limegamepad; + } + #end + + @Test + function testAddRemoveSet() + { + var testManager = new FlxActionManager(); + var managerText = '{"actionSets":[{"name":"MenuControls","analogActions":["menu_move"],"digitalActions":["menu_up","menu_down","menu_left","menu_right","menu_select","menu_menu","menu_cancel","menu_thing_1","menu_thing_2","menu_thing_3"]},{"name":"MapControls","analogActions":["scroll_map","move_map"],"digitalActions":["map_select","map_exit","map_menu","map_journal"]},{"name":"BattleControls","analogActions":["move"],"digitalActions":["punch","kick","jump"]}]}'; + var actionsJSON = Json.parse(managerText); + + testManager.initFromJSON(actionsJSON, null, null); + + var setText = '{"name":"ExtraControls","analogActions":["extra_move"],"digitalActions":["extra_up","extra_down","extra_left","extra_right","extra_select","extra_menu","extra_cancel","extra_thing_1","extra_thing_2","extra_thing_3"]}'; + var json = Json.parse(setText); + var extraSet:FlxActionSet = @:privateAccess FlxActionSet.fromJSON(json, null, null); + + testManager.addSet(extraSet); + + var setIndex = testManager.getSetIndex("ExtraControls"); + var setName = testManager.getSetName(setIndex); + var setObject = testManager.getSet(setIndex); + + Assert.isTrue(setIndex == 3); + Assert.isTrue(setName == "ExtraControls"); + Assert.isTrue(setObject == extraSet); + + testManager.removeSet(extraSet); + + setObject = testManager.getSet(setIndex); + setName = testManager.getSetName(setIndex); + setIndex = testManager.getSetIndex("ExtraControls"); + + Assert.isTrue(setIndex == -1); + Assert.isTrue(setName == ""); + Assert.isTrue(setObject == null); + } + + @Test + function testExportToJSON() + { + var testManager = new FlxActionManager(); + var managerText = '{"actionSets":[{"name":"MenuControls","analogActions":["menu_move"],"digitalActions":["menu_up","menu_down","menu_left","menu_right","menu_select","menu_menu","menu_cancel","menu_thing_1","menu_thing_2","menu_thing_3"]},{"name":"MapControls","analogActions":["scroll_map","move_map"],"digitalActions":["map_select","map_exit","map_menu","map_journal"]},{"name":"BattleControls","analogActions":["move"],"digitalActions":["punch","kick","jump"]}]}'; + var actionsJSON = Json.parse(managerText); + + testManager.initFromJSON(actionsJSON, null, null); + + var testManager2 = new FlxActionManager(); + var outString = testManager.exportToJSON(); + var actionsJSON2 = Json.parse(outString); + + testManager2.initFromJSON(actionsJSON2, null, null); + + Assert.isTrue(testManager.numSets == testManager2.numSets); + + var setNames1:String = ""; + var setNames2:String = ""; + + var setDigitals1:String = ""; + var setDigitals2:String = ""; + + var setAnalogs1:String = ""; + var setAnalogs2:String = ""; + + for (i in 0...testManager.numSets) + { + setNames1 += testManager.getSetName(i); + setNames2 += testManager2.getSetName(i); + + var set1:FlxActionSet = testManager.getSet(i); + var set2:FlxActionSet = testManager2.getSet(i); + + for (j in 0...set1.digitalActions.length) + { + setDigitals1 += set1.digitalActions[j].name; + setDigitals2 += set2.digitalActions[j].name; + } + + for (j in 0...set1.analogActions.length) + { + setAnalogs1 += set1.analogActions[j].name; + setAnalogs2 += set2.analogActions[j].name; + } + } + + Assert.isTrue(setNames1 == setNames2); + Assert.isTrue(setDigitals1 == setDigitals2); + Assert.isTrue(setAnalogs1 == setAnalogs2); + } + + @Test + function testInputsChanged() + { + //This one's tricky! + + /* + SteamMock.init(); + SteamMock.initFlx(); + + valueTest = ""; + + var setName = "MenuControls"; + var setIndex = steamManager.getSetIndex(setName); + var set:FlxActionSet = steamManager.getSet(setIndex); + + //Set up fake steam handles since we don't have Steam to do it automatically + for (i in 0...set.digitalActions.length) + { + var d:FlxActionDigital = set.digitalActions[i]; + @:privateAccess d.steamHandle = i; + } + + var a:FlxActionAnalog = set.analogActions[0]; + @:privateAccess a.steamHandle = 99; + + var controller = 0; + var actionsChanged = ""; + + step(); + @:privateAccess steamManager.update(); + + var dOrigins:Array = + [ + LEFTPAD_DPADNORTH, + LEFTPAD_DPADSOUTH, + LEFTPAD_DPADWEST, + LEFTPAD_DPADEAST, + A, + START, + B, + X, + Y, + BACK + ]; + + var aOrigins:Array = + [ + LEFTSTICK_MOVE + ]; + + for (i in 0...set.digitalActions.length) + { + var d:FlxActionDigital = set.digitalActions[i]; + SteamMock.setDigitalActionOrigins(controller, setIndex, @:privateAccess d.steamHandle, [dOrigins[i]]); + } + for (i in 0...set.analogActions.length) + { + var a:FlxActionAnalog = set.analogActions[i]; + SteamMock.setAnalogActionOrigins(controller, setIndex, @:privateAccess a.steamHandle, [aOrigins[i]]); + } + + //Set up a signal callback for when inputs are changed (by our fake simulation of attaching a Steam Controller) + steamManager.inputsChanged.add( + function(arr:Array) + { + for (i in 0...arr.length) + { + actionsChanged += arr[i].name; + if (i != arr.length-1) + { + actionsChanged += ","; + } + } + } + ); + + //Activate this action set for Steam Controller 1 (handle 0), which should attach steam inputs to the actions under the hood, and trigger our signal + steamManager.activateSet(setIndex, FlxInputDevice.STEAM_CONTROLLER, controller); + + step(); + @:privateAccess steamManager.update(); + + //The Steam API explicitly recommends we activate the set continuously, so we will simulate that here + steamManager.activateSet(setIndex, FlxInputDevice.STEAM_CONTROLLER, controller); + + step(); + @:privateAccess steamManager.update(); + + var finalValue = actionsChanged; + + Assert.isTrue(finalValue == "menu_up,menu_down,menu_left,menu_right,menu_select,menu_menu,menu_cancel,menu_thing_1,menu_thing_2,menu_thing_3,menu_move"); + */ + } + + @Test + function testUpdateAndCallbacks() + { + var managerText = '{"actionSets":[{"name":"MenuControls","analogActions":["menu_move"],"digitalActions":["menu_up","menu_down","menu_left","menu_right","menu_select","menu_menu","menu_cancel","menu_thing_1","menu_thing_2","menu_thing_3"]},{"name":"MapControls","analogActions":["scroll_map","move_map"],"digitalActions":["map_select","map_exit","map_menu","map_journal"]},{"name":"BattleControls","analogActions":["move"],"digitalActions":["punch","kick","jump"]}]}'; + var actionsJSON = Json.parse(managerText); + var testManager = new FlxActionManager(); + testManager.initFromJSON(actionsJSON, null, null); + + var keys = [FlxKey.A, FlxKey.B, FlxKey.C, FlxKey.D, FlxKey.E, FlxKey.F, FlxKey.G, FlxKey.H, FlxKey.I, FlxKey.J]; + + var setIndex = testManager.getSetIndex("MenuControls"); + var set = testManager.getSet(setIndex); + testManager.activateSet(setIndex, FlxInputDevice.ALL, FlxInputDeviceID.ALL); + + for (i in 0...set.digitalActions.length) + { + var action:FlxActionDigital = set.digitalActions[i]; + action.add(new FlxActionInputDigitalKeyboard(keys[i], flixel.input.FlxInputState.JUST_PRESSED)); + action.callback = function(a:FlxActionDigital) + { + onCallback(a.name); + }; + } + + set.analogActions[0].add(new FlxActionInputAnalogMouseMotion(MOVED)); + set.analogActions[0].callback = function(a:FlxActionAnalog) + { + onCallback(a.name); + }; + + valueTest = ""; + + for (key in keys) + { + clearFlxKey(key, testManager); + clickFlxKey(key, true, testManager); + } + + step(); + @:privateAccess testManager.update(); + + moveMousePosition(100, 100, testManager); + + step(); + @:privateAccess testManager.update(); + + var finalValue = Std.string(valueTest); + + //cleanup + for (key in keys) + { + clearFlxKey(key, testManager); + } + moveMousePosition(0, 0, testManager); + + Assert.isTrue(finalValue == "menu_up,menu_down,menu_left,menu_right,menu_select,menu_menu,menu_cancel,menu_thing_1,menu_thing_2,menu_thing_3,menu_move"); + } + + private function createFlxActionManager() + { + basicManager = new FlxActionManager(); + + var actionsText = '{"actionSets":[{"name":"MenuControls","analogActions":["menu_move"],"digitalActions":["menu_up","menu_down","menu_left","menu_right","menu_select","menu_menu","menu_cancel","menu_thing_1","menu_thing_2","menu_thing_3"]},{"name":"MapControls","analogActions":["scroll_map","move_map"],"digitalActions":["map_select","map_exit","map_menu","map_journal"]},{"name":"BattleControls","analogActions":["move"],"digitalActions":["punch","kick","jump"]}]}'; + var actionsJSON = Json.parse(actionsText); + + basicManager.initFromJSON(actionsJSON, null, null); + + #if FLX_STEAMWRAP + steamManager = new FlxActionManager(); + + var vdfText = VDFString.get(); + var config = ControllerConfig.fromVDF(vdfText); + + steamManager.initSteam(config, null, null); + #end + } + + function runFlxActionManagerInit(test:TestShell, ?manager:FlxActionManager) + { + if (manager == null) manager = basicManager; + + for (i in 0...3) + { + var set = sets[i]; + var analogs = analog[i]; + var digitals = digital[i]; + + var setIndex:Int = manager.getSetIndex(set); + var setName:String = manager.getSetName(setIndex); + var setObject:FlxActionSet = manager.getSet(setIndex); + + test.prefix = set + "."; + + test.testBool(setIndex != -1, "indexExists"); + test.testBool(setName == set, "nameMatches"); + test.testBool(setObject != null, "setExists"); + } + } + + function runFlxActionManagerActions(test:TestShell, ?manager:FlxActionManager) + { + if (manager == null) manager = basicManager; + + for (i in 0...3) + { + var set = sets[i]; + var analogs = analog[i]; + var digitals = digital[i]; + + var setIndex:Int = manager.getSetIndex(set); + var setName:String = manager.getSetName(setIndex); + var setObject:FlxActionSet = manager.getSet(setIndex); + + test.prefix = set + "."; + + test.testBool(setObject.digitalActions != null && setObject.digitalActions.length > 0, "hasDigital"); + test.testBool(setObject.analogActions != null && setObject.analogActions.length > 0, "hasAnalog"); + + //Test digital actions exist + for (j in 0...setObject.digitalActions.length) + { + var d:FlxActionDigital = setObject.digitalActions[j]; + test.testBool(digitals.indexOf(d.name) != -1, "digital." + d.name + ".exists"); + } + + //Test analog actions exist + for (j in 0...setObject.analogActions.length) + { + var a:FlxActionAnalog = setObject.analogActions[j]; + test.testBool(analogs.indexOf(a.name) != -1, "analog." + a.name + ".exists"); + } + } + } + + function runFlxActionManagerAddRemove(test:TestShell, ?manager:FlxActionManager) + { + if (manager == null) manager = basicManager; + + for (i in 0...3) + { + var set = sets[i]; + var analogs = analog[i]; + var digitals = digital[i]; + + var setIndex:Int = manager.getSetIndex(set); + var setName:String = manager.getSetName(setIndex); + var setObject:FlxActionSet = manager.getSet(setIndex); + + test.prefix = set + "."; + + //Test add & remove digital actions + var extraDigital = new FlxActionDigital("extra"); + var result = manager.addAction(extraDigital, setIndex); + test.testBool(result && setObject.digitalActions.indexOf(extraDigital) != -1, "digital.extra.add"); + result = manager.removeAction(extraDigital, setIndex); + test.testBool(result && setObject.digitalActions.indexOf(extraDigital) == -1, "digital.extra.remove"); + + //Test add & remove analog actions + var extraAnalog = new FlxActionAnalog("extra"); + var result = manager.addAction(extraAnalog, setIndex); + test.testBool(result && setObject.analogActions.indexOf(extraAnalog) != -1, "analog.extra.add"); + result = manager.removeAction(extraAnalog, setIndex); + test.testBool(result && setObject.analogActions.indexOf(extraAnalog) == -1, "analog.extra.remove"); + } + } + + function runFlxActionManagerDevice(device:FlxInputDevice, test:TestShell, ?manager:FlxActionManager) + { + if (manager == null) manager = basicManager; + + for (i in 0...3) + { + var set = sets[i]; + var analogs = analog[i]; + var digitals = digital[i]; + + var setIndex:Int = manager.getSetIndex(set); + var setName:String = manager.getSetName(setIndex); + var setObject:FlxActionSet = manager.getSet(setIndex); + + test.prefix = set + "."; + + //Test activating action sets for a device + manager.deactivateSet(setIndex); + var dset = manager.getSetActivatedForDevice(device); + manager.activateSet(setIndex, device, FlxInputDeviceID.ALL); + var activatedSet = manager.getSetActivatedForDevice(device); + + //Test set is activated after we activate it for a specific device + test.testBool(setObject == activatedSet, "activatedFor." + device); + + var devices:Array = + [ + MOUSE, + KEYBOARD, + GAMEPAD, + #if FLX_STEAMWRAP + STEAM_CONTROLLER, + #end + ALL + ]; + + for (otherDevice in devices) + { + var activatedOtherSet = manager.getSetActivatedForDevice(otherDevice); + + if (device == ALL) + { + //Test set is activated for every device + test.testBool(activatedSet == activatedOtherSet, "activatedForAll." + otherDevice); + } + else if (otherDevice != device) + { + //Test set is NOT activated for every other device + test.testBool(activatedSet != activatedOtherSet, "notActivatedFor." + otherDevice + ".but." + device); + } + } + + manager.deactivateSet(setIndex); + activatedSet = manager.getSetActivatedForDevice(device); + + //Test set is deactivated after we deactivate it + test.testBool(setObject != activatedSet, "deactivatedFor." + device); + + for (otherDevice in devices) + { + var activatedOtherSet = manager.getSetActivatedForDevice(otherDevice); + + if (device == ALL) + { + //Test set is deactivated for every device + test.testBool(setObject != activatedOtherSet, "deactivatedForAll." + otherDevice); + } + else if (otherDevice != device) + { + //Test set is still not activated for every other device + test.testBool(setObject != activatedOtherSet, "stillNotActivatedFor." + otherDevice + ".but." + device); + } + } + } + } + + @:access(flixel.input.FlxKeyManager) + private function clickFlxKey(key:FlxKey, pressed:Bool, manager:FlxActionManager) + { + if (FlxG.keys == null || FlxG.keys._keyListMap == null) return; + + var input:FlxInput = FlxG.keys._keyListMap.get(key); + if (input == null) return; + + step(); + @:privateAccess manager.update(); + + if (pressed) + { + input.press(); + } + else + { + input.release(); + } + + @:privateAccess manager.update(); + + } + + @:access(flixel.input.FlxKeyManager) + private function clearFlxKey(key:FlxKey, manager:FlxActionManager) + { + var input:FlxInput = FlxG.keys._keyListMap.get(key); + if (input == null) return; + input.release(); + step(); + @:privateAccess manager.update(); + step(); + @:privateAccess manager.update(); + } + + private function moveMousePosition(X:Float, Y:Float, manager:FlxActionManager) + { + if (FlxG.mouse == null) return; + step(); + FlxG.mouse.setGlobalScreenPositionUnsafe(X, Y); + @:privateAccess manager.update(); + } + + private function onCallback(str:String) + { + if (valueTest != "") + { + valueTest += ","; + } + valueTest += str; + } +} diff --git a/tests/unit/src/flixel/input/actions/FlxActionSetTest.hx b/tests/unit/src/flixel/input/actions/FlxActionSetTest.hx new file mode 100644 index 0000000000..bcdcc6c243 --- /dev/null +++ b/tests/unit/src/flixel/input/actions/FlxActionSetTest.hx @@ -0,0 +1,369 @@ +package flixel.input.actions; + +import flixel.input.FlxInput.FlxInputState; +import flixel.input.actions.FlxAction.FlxActionAnalog; +import flixel.input.actions.FlxAction.FlxActionDigital; +import flixel.input.actions.FlxActionInputAnalog.FlxActionInputAnalogMouseMotion; +import flixel.input.actions.FlxActionInputDigital.FlxActionInputDigitalKeyboard; +import flixel.input.keyboard.FlxKey; +import haxe.Json; + +import massive.munit.Assert; + +class FlxActionSetTest extends FlxTest +{ + var valueTest:String = ""; + + @Before + function before() {} + + @Test + function testFromJSON() + { + var text = '{"name":"MenuControls","analogActions":["menu_move"],"digitalActions":["menu_up","menu_down","menu_left","menu_right","menu_select","menu_menu","menu_cancel","menu_thing_1","menu_thing_2","menu_thing_3"]}'; + var json = Json.parse(text); + var set:FlxActionSet = @:privateAccess FlxActionSet.fromJSON(json, null, null); + + Assert.isTrue(set.name == "MenuControls"); + Assert.isTrue(set.analogActions != null); + Assert.isTrue(set.analogActions.length == 1); + Assert.isTrue(set.digitalActions != null); + Assert.isTrue(set.digitalActions.length == 10); + + var analog = ["menu_move"]; + var digital = ["menu_up", "menu_down", "menu_left", "menu_right", "menu_select", "menu_menu", "menu_cancel", "menu_thing_1", "menu_thing_2", "menu_thing_3"]; + + var hasAnalog = false; + + for (a in analog) + { + hasAnalog = false; + for (aa in set.analogActions) + { + if (aa.name == a) + { + hasAnalog = true; + } + } + if (!hasAnalog) + { + break; + } + } + + Assert.isTrue(hasAnalog); + + var hasDigital = false; + + for (d in digital) + { + hasDigital = false; + for (dd in set.digitalActions) + { + if (dd.name == d) + { + hasDigital = true; + } + } + if (!hasDigital) + { + break; + } + } + + Assert.isTrue(hasDigital); + } + + @Test + function testToJSON() + { + var text = '{"name":"MenuControls","analogActions":["menu_move"],"digitalActions":["menu_up","menu_down","menu_left","menu_right","menu_select","menu_menu","menu_cancel","menu_thing_1","menu_thing_2","menu_thing_3"]}'; + var json = Json.parse(text); + var set:FlxActionSet = @:privateAccess FlxActionSet.fromJSON(json, null, null); + + var outJson = set.toJSON(); + + var out:{name:String, analogActions:Array<{type:Int, steamHandle:Int, name:String}>, digitalActions:Array<{type:Int, steamHandle:Int, name:String}>} = Json.parse(outJson); + + var name = "MenuControls"; + var analogActions = ["menu_move"]; + var digitalActions = ["menu_up", "menu_down", "menu_left", "menu_right", "menu_select", "menu_menu", "menu_cancel", "menu_thing_1", "menu_thing_2", "menu_thing_3"]; + + Assert.isTrue(out.name == name); + + var analogEquivalent = out.analogActions.length == analogActions.length; + var digitalEquivalent = out.digitalActions.length == digitalActions.length; + + if (analogEquivalent) + { + for (i in 0...analogActions.length) + { + var found = false; + for (ii in 0...out.analogActions.length) + { + if (out.analogActions[ii].name == analogActions[i]) + { + found = true; + } + } + if (!found) + { + analogEquivalent = false; + break; + } + } + } + + Assert.isTrue(analogEquivalent); + + if (digitalEquivalent) + { + for (i in 0...digitalActions.length) + { + var found = false; + for (ii in 0...out.digitalActions.length) + { + if (out.digitalActions[ii].name == digitalActions[i]) + { + found = true; + } + } + if (!found) + { + digitalEquivalent = false; + break; + } + } + } + + Assert.isTrue(digitalEquivalent); + } + + @Test + function testAddRemoveDigital() + { + var set = new FlxActionSet("test", [], []); + + Assert.isTrue(set.digitalActions.length == 0); + + var d1 = new FlxActionDigital("d1"); + var d2 = new FlxActionDigital("d2"); + + set.add(d1); + Assert.isTrue(set.digitalActions.length == 1); + set.add(d2); + Assert.isTrue(set.digitalActions.length == 2); + + set.remove(d1); + Assert.isTrue(set.digitalActions.length == 1); + Assert.isTrue(set.digitalActions[0] == d2); + set.remove(d2); + Assert.isTrue(set.digitalActions.length == 0); + } + + @Test + function testAddRemoveAnalog() + { + var set = new FlxActionSet("test", [], []); + + Assert.isTrue(set.analogActions.length == 0); + + var a1 = new FlxActionAnalog("a1"); + var a2 = new FlxActionAnalog("a2"); + + set.add(a1); + Assert.isTrue(set.analogActions.length == 1); + set.add(a2); + Assert.isTrue(set.analogActions.length == 2); + + set.remove(a1); + Assert.isTrue(set.analogActions.length == 1); + Assert.isTrue(set.analogActions[0] == a2); + set.remove(a2); + Assert.isTrue(set.analogActions.length == 0); + } + + @Test + function testUpdateAndCallbacks() + { + var text = '{"name":"MenuControls","analogActions":["menu_move"],"digitalActions":["menu_up","menu_down","menu_left","menu_right","menu_select","menu_menu","menu_cancel","menu_thing_1","menu_thing_2","menu_thing_3"]}'; + var json = Json.parse(text); + var set:FlxActionSet = @:privateAccess FlxActionSet.fromJSON(json, null, null); + + var keys = [FlxKey.A, FlxKey.B, FlxKey.C, FlxKey.D, FlxKey.E, FlxKey.F, FlxKey.G, FlxKey.H, FlxKey.I, FlxKey.J]; + + for (i in 0...set.digitalActions.length) + { + var action:FlxActionDigital = set.digitalActions[i]; + action.addKey(keys[i], FlxInputState.JUST_PRESSED); + action.callback = function(a:FlxActionDigital) + { + onCallback(a.name); + }; + } + + set.analogActions[0].addMouseMotion(MOVED); + set.analogActions[0].callback = function(a:FlxActionAnalog) + { + onCallback(a.name); + }; + + valueTest = ""; + + for (key in keys) + { + clearFlxKey(key, set); + clickFlxKey(key, true, set); + } + + step(); + set.update(); + + moveMousePosition(100, 100, set); + + step(); + set.update(); + + var finalValue = valueTest; + + for (key in keys) + { + clearFlxKey(key, set); + } + moveMousePosition(0, 0, set); + + Assert.isTrue(finalValue == "menu_up,menu_down,menu_left,menu_right,menu_select,menu_menu,menu_cancel,menu_thing_1,menu_thing_2,menu_thing_3,menu_move"); + } + + #if FLX_STEAMWRAP + @Test + #if travis @Ignore("Fails on Travis b/c of CFFI errors with Steamwrap, but works when tested locally") #end + function testAttachSteamController() + { + var text = '{"name":"MenuControls","analogActions":["menu_move"],"digitalActions":["menu_up","menu_down","menu_left","menu_right","menu_select","menu_menu","menu_cancel","menu_thing_1","menu_thing_2","menu_thing_3"]}'; + var json = Json.parse(text); + var set:FlxActionSet = @:privateAccess FlxActionSet.fromJSON(json, null, null); + + for (i in 0...set.digitalActions.length) + { + var d:FlxActionDigital = set.digitalActions[i]; + @:privateAccess d.steamHandle = i; + d.callback = function(a:FlxActionDigital) + { + onCallback(a.name); + } + } + + var a:FlxActionAnalog = set.analogActions[0]; + @:privateAccess a.steamHandle = 99; + a.callback = function(a:FlxActionAnalog) + { + onCallback(a.name + "_" + a.x + "x" + a.y); + } + + var controller = 0; + + set.attachSteamController(controller, true); + + valueTest = ""; + + for (i in 0...set.digitalActions.length) + { + clearSteamDigital(controller, i, set); + clickSteamDigital(controller, i, true, set); + } + + step(); + set.update(); + + moveSteamAnalog(controller, 99, 100, 100, set); + + var finalValue = valueTest; + + Assert.isTrue(finalValue == "menu_up,menu_down,menu_left,menu_right,menu_select,menu_menu,menu_cancel,menu_thing_1,menu_thing_2,menu_thing_3,menu_move_100x100"); + } + #end + + private function onCallback(str:String) + { + if (valueTest != "") + { + valueTest += ","; + } + valueTest += str; + } + + private function moveMousePosition(X:Float, Y:Float, set:FlxActionSet) + { + if (FlxG.mouse == null) return; + step(); + FlxG.mouse.setGlobalScreenPositionUnsafe(X, Y); + set.update(); + } + + @:access(flixel.input.FlxKeyManager) + private function clickFlxKey(key:FlxKey, pressed:Bool, set:FlxActionSet) + { + if (FlxG.keys == null || FlxG.keys._keyListMap == null) return; + + var input:FlxInput = FlxG.keys._keyListMap.get(key); + if (input == null) return; + + step(); + set.update(); + + if (pressed) + { + input.press(); + } + else + { + input.release(); + } + + set.update(); + + } + + @:access(flixel.input.FlxKeyManager) + private function clearFlxKey(key:FlxKey, set:FlxActionSet) + { + var input:FlxInput = FlxG.keys._keyListMap.get(key); + if (input == null) return; + input.release(); + step(); + set.update(); + step(); + set.update(); + } + + #if FLX_STEAMWRAP + private function moveSteamAnalog(controller:Int, actionHandle:Int, X:Float, Y:Float, set:FlxActionSet) + { + step(); + + SteamMock.setAnalogAction(controller, actionHandle, X, Y, true); + + set.update(); + } + + private function clickSteamDigital(controller:Int, actionHandle:Int, pressed:Bool, set:FlxActionSet) + { + step(); + set.update(); + + SteamMock.setDigitalAction(controller, actionHandle, pressed); + + set.update(); + } + + private function clearSteamDigital(controller:Int, actionHandle:Int, set:FlxActionSet) + { + SteamMock.setDigitalAction(controller, actionHandle, false); + step(); + set.update(); + step(); + set.update(); + } + #end +} \ No newline at end of file diff --git a/tests/unit/src/flixel/input/actions/FlxActionTest.hx b/tests/unit/src/flixel/input/actions/FlxActionTest.hx new file mode 100644 index 0000000000..0732273574 --- /dev/null +++ b/tests/unit/src/flixel/input/actions/FlxActionTest.hx @@ -0,0 +1,255 @@ +package flixel.input.actions; + +import flixel.input.FlxInput; +import flixel.input.actions.FlxAction.FlxActionAnalog; +import flixel.input.actions.FlxAction.FlxActionDigital; +import flixel.input.actions.FlxActionInputAnalog; +import flixel.input.actions.FlxActionInputDigital; +import flixel.input.FlxInput.FlxInputState; + +import massive.munit.Assert; + +class FlxActionTest extends FlxTest +{ + var digital:FlxActionDigital; + var analog:FlxActionAnalog; + + var digitalCalls:Int = 0; + var analogCalls:Int = 0; + + var dState:FlxInput; + var dInput:FlxActionInputDigital; + + var aInput:FlxActionInputAnalog; + + @Before + function before():Void + { + digital = new FlxActionDigital("digital", null); + analog = new FlxActionAnalog("analog", null); + + dState = new FlxInput(0); + dInput = new FlxActionInputDigitalIFlxInput(dState, FlxInputState.PRESSED); + + aInput = new FlxActionInputAnalogMousePosition(FlxAnalogState.MOVED, FlxAnalogAxis.EITHER); + + digital.add(dInput); + analog.add(aInput); + } + + @Test + function testAddRemoveInputs() + { + var oldInputs = digital.inputs.copy(); + digital.removeAll(false); + + var input1 = new FlxActionInputDigitalIFlxInput(null, FlxInputState.PRESSED); + var input2 = new FlxActionInputDigitalIFlxInput(null, FlxInputState.PRESSED); + var input3 = new FlxActionInputDigitalIFlxInput(null, FlxInputState.PRESSED); + digital.add(input1); + digital.add(input2); + digital.add(input3); + + Assert.isTrue(digital.inputs.length == 3); + + digital.remove(input1, false); + + Assert.isTrue(digital.inputs.length == 2); + Assert.isFalse(input1.destroyed); + + input1.destroy(); + + Assert.isTrue(input1.destroyed); + Assert.isTrue(digital.inputs[0] == input2); + + digital.removeAll(true); + + Assert.isTrue(digital.inputs.length == 0); + Assert.isTrue(input2.destroyed); + Assert.isTrue(input3.destroyed); + + digital.inputs = oldInputs; + } + + @Test + function testCallbacks() + { + //digital w/ callback + + var value = 0; + + var d:FlxActionDigital = new FlxActionDigital("dCallback", function (a:FlxActionDigital) + { + value++; + } + ); + d.add(dInput); + pulseDigital(); + d.check(); + + Assert.isTrue(value == 1); + + d.removeAll(false); + d.destroy(); + + //digital w/o callback + + value = 0; + + var d2:FlxActionDigital = new FlxActionDigital("dNoCallback", null); + d2.add(dInput); + pulseDigital(); + digital.check(); + + Assert.isTrue(value == 0); + + d2.removeAll(false); + d2.destroy(); + + //analog w/ callback + + value = 0; + + var a:FlxActionAnalog = new FlxActionAnalog("aCallback", function (a:FlxActionAnalog) + { + value++; + } + ); + a.add(aInput); + pulseAnalog(a); + var result = analog.check(); + + Assert.isTrue(value == 1); + + a.removeAll(false); + a.destroy(); + + //analog w/o callback + + value = 0; + + var a2:FlxActionAnalog = new FlxActionAnalog("aNoCallback", null); + a2.add(aInput); + pulseAnalog(a2); + analog.check(); + + Assert.isTrue(value == 0); + + a2.removeAll(false); + a2.destroy(); + } + + @Test + function testCheckAndTriggeredDigital() + { + clearDigital(); + + Assert.isFalse(digital.check()); + Assert.isFalse(digital.triggered); + + pulseDigital(); + + Assert.isTrue(digital.check()); + Assert.isTrue(digital.triggered); + } + + @Test + function testCheckAndTriggeredAnalog() + { + clearAnalog(); + + Assert.isFalse(analog.check()); + Assert.isFalse(analog.triggered); + + pulseAnalog(analog); + + Assert.isTrue(analog.check()); + Assert.isTrue(analog.triggered); + } + + @Test + function testDestroyAction() + { + var d:FlxActionDigital = new FlxActionDigital("test", function(_d:FlxActionDigital) + { + var blah = true; + } + ); + + d.destroy(); + + Assert.isTrue(d.callback == null); + Assert.isTrue(d.inputs == null); + } + + @Test + function testMatch() + { + var other = new FlxActionDigital(digital.name, null); + Assert.isTrue(digital.match(other)); + } + + @Test + function testNoInputs() + { + var oldInputsD = digital.inputs.copy(); + var oldInputsA = analog.inputs.copy(); + + digital.removeAll(false); + analog.removeAll(false); + + pulseDigital(); + + Assert.isFalse(digital.check()); + + pulseAnalog(analog); + + Assert.isFalse(analog.check()); + + digital.inputs = oldInputsD; + analog.inputs = oldInputsA; + } + + private function clearDigital() + { + dState.release(); + dState.update(); + + } + + private function pulseDigital() + { + step(); + dState.release(); + dState.update(); + step(); + dState.press(); + dState.update(); + } + + private function clearAnalog() + { + step(); + FlxG.mouse.setGlobalScreenPositionUnsafe(0, 0); + step(); + FlxG.mouse.setGlobalScreenPositionUnsafe(0, 0); + } + + @:access(flixel.input.mouse.FlxMouse) + private function pulseAnalog(a:FlxActionAnalog, X:Float = 10.0, Y:Float = 10.0) + { + FlxG.mouse.setGlobalScreenPositionUnsafe(0, 0); + step(); + a.update(); + FlxG.mouse.setGlobalScreenPositionUnsafe(X, Y); + step(); + a.update(); + } + + private function moveAnalog(a:FlxActionAnalog, X:Float, Y:Float) + { + step(); + FlxG.mouse.setGlobalScreenPositionUnsafe(X, Y); + a.update(); + } +} \ No newline at end of file diff --git a/tests/unit/src/flixel/input/actions/SteamMock.hx b/tests/unit/src/flixel/input/actions/SteamMock.hx new file mode 100644 index 0000000000..a21857cf49 --- /dev/null +++ b/tests/unit/src/flixel/input/actions/SteamMock.hx @@ -0,0 +1,220 @@ +package flixel.input.actions; + +import flixel.input.actions.FlxSteamController.FlxSteamControllerMetadata; +import flixel.util.FlxArrayUtil; +#if FLX_STEAMWRAP +import steamwrap.api.Controller; +import steamwrap.api.Steam; +#end + +class SteamMock +{ + #if FLX_STEAMWRAP + public static var digitalData:Map; + public static var analogData:Map; + + public static var digitalOrigins:Map>; + public static var analogOrigins:Map>; + + static var inited:Bool = false; + static var flxInited:Bool = false; + + @:access(steamwrap.api.Steam) + public static function init() + { + if (inited) return; + + digitalData = new Map(); + analogData = new Map(); + + digitalOrigins = new Map>(); + analogOrigins = new Map>(); + + Steam.controllers = new FakeController(function(str:String) + { + trace(str); + }); + + inited = true; + } + + public static function initFlx() + { + if (flxInited) return; + + var metaData = new FlxSteamControllerMetadata(); + + metaData.handle = 0; + metaData.actionSet = -1; + metaData.active = false; + metaData.connected.press(); + + @:privateAccess FlxSteamController.controllers = [metaData]; + + flxInited = true; + } + + @:access(flixel.input.actions.FlxSteamController) + public static function setDigitalAction(controller:Int, actionHandle:Int, bool:Bool) + { + if (!inited) init(); + + var key = controller + "_" + actionHandle; + + var data; + if (!digitalData.exists(key)) + { + data = new ControllerDigitalActionData(0); + digitalData.set(key, data); + } + data = digitalData.get(key); + + var newData = new ControllerDigitalActionData(bool ? 0x11 : 0x10); + + digitalData.set(key, newData); + } + + public static function setAnalogAction(controller:Int, actionHandle:Int, x:Float, y:Float, active:Bool, eMode:EControllerSourceMode = JOYSTICKMOVE) + { + if (!inited) init(); + + var key = controller + "_" + actionHandle; + + var data; + if (!analogData.exists(key)) + { + data = new ControllerAnalogActionData(); + analogData.set(key, data); + } + data = analogData.get(key); + + data.bActive = active ? 1 : 0; + data.eMode = eMode; + data.x = x; + data.y = y; + } + + public static function setDigitalActionOrigins(controller:Int, actionSet:Int, action:Int, origins:Array) + { + if (!inited) init(); + + var key = controller + "_" + actionSet + "_" + action; + digitalOrigins.set(key, origins); + } + + public static function setAnalogActionOrigins(controller:Int, actionSet:Int, action:Int, origins:Array) + { + if (!inited) init(); + + var key = controller + "_" + actionSet + "_" + action; + analogOrigins.set(key, origins); + } + #end +} + +#if FLX_STEAMWRAP +class FakeController extends Controller +{ + public function new(CustomTrace:String->Void) + { + super(CustomTrace); + } + + override function init() {} + + override function get_MAX_ORIGINS():Int + { + return 16; + } + + override function get_MAX_CONTROLLERS():Int + { + return 16; + } + + override public function getDigitalActionData(controller:Int, action:Int):ControllerDigitalActionData + { + var key = controller + "_" + action; + + return SteamMock.digitalData.get(key); + } + + override public function getAnalogActionData(controller:Int, action:Int, ?data:ControllerAnalogActionData):ControllerAnalogActionData + { + var key = controller + "_" + action; + + if (data == null) + { + data = new ControllerAnalogActionData(); + } + + var result = SteamMock.analogData.get(key); + + if (result == null) + { + return data; + } + + data.bActive = result.bActive; + data.eMode = result.eMode; + data.x = result.x; + data.y = result.y; + + return data; + } + + override public function getDigitalActionOrigins(controller:Int, actionSet:Int, action:Int, ?originsOut:Array):Int + { + var key = controller + "_" + actionSet + "_" + action; + + var first:Int = 0; + + if (SteamMock.digitalOrigins == null) return first; + var result = SteamMock.digitalOrigins.get(key); + if (result == null) return first; + + if (originsOut != null) + { + FlxArrayUtil.clearArray(originsOut); + } + else + { + originsOut = []; + } + + for (r in result) + { + originsOut.push(r); + } + + return originsOut.length; + } + + override public function getAnalogActionOrigins(controller:Int, actionSet:Int, action:Int, ?originsOut:Array):Int + { + var key = controller + "_" + actionSet + "_" + action; + + var first:Int = 0; + + if (SteamMock.analogOrigins == null) return first; + var result = SteamMock.analogOrigins.get(key); + if (result == null) return first; + + if (originsOut != null) + { + FlxArrayUtil.clearArray(originsOut); + } + else + { + originsOut = []; + } + + for (r in result) + { + originsOut.push(r); + } + + return originsOut.length; + } +} +#end diff --git a/tests/unit/src/flixel/input/actions/TestShell.hx b/tests/unit/src/flixel/input/actions/TestShell.hx new file mode 100644 index 0000000000..66212aec96 --- /dev/null +++ b/tests/unit/src/flixel/input/actions/TestShell.hx @@ -0,0 +1,114 @@ +package flixel.input.actions; + +import flixel.util.FlxArrayUtil; +import flixel.util.FlxDestroyUtil.IFlxDestroyable; +import haxe.PosInfos; + +import massive.munit.Assert; + +class TestShell implements IFlxDestroyable +{ + public var name:String; + public var results:Array; + public var prefix:String = ""; + + public function new(Name:String) + { + name = Name; + results = []; + } + + public function destroy() + { + FlxArrayUtil.clearArray(results); + } + + public function get(id:String):TestShellResult + { + for (result in results) + { + if (result.id == id) return result; + } + return + { + id:"unknown id(" + id + ")", + testedTrue:false, + testedFalse:false, + testedNull:false, + testedNotNull:false, + strValue:"unknown id, not tested!" + }; + } + + public function testBool(b:Bool, id:String) + { + test(id, b, !b, false, false, Std.string(b)); + } + + public function testIsNull(d:Dynamic, id:String) + { + test(id, false, false, d == null, d != null, d == null ? "null" : Std.string(d)); + } + + public function testIsNotNull(d:Dynamic, id:String) + { + test(id, false, false, d == null, d != null, d == null ? "null" : Std.string(d)); + } + + private function test(id:String, tTrue:Bool = false, tFalse:Bool = false, tNull:Bool = false, tNNull:Bool = false, strValue:String = "untested") + { + results.push + ( + { + id:name + prefix + id, + testedTrue:tTrue, + testedFalse:tFalse, + testedNull:tNull, + testedNotNull:tNNull, + strValue:strValue + } + ); + } + + public function assertTrue (id:String, ?info:PosInfos) + { + var value = get(id).testedTrue; + var strValue = get(id).strValue; + Assert.assertionCount++; + if (!value) Assert.fail("Expected TRUE but (" + id + ") was [" + strValue + "]", info); + } + + public function assertFalse (id:String, ?info:PosInfos) + { + var value = get(id).testedFalse; + var strValue = get(id).strValue; + Assert.assertionCount++; + if (!value) Assert.fail("Expected FALSE but (" + id + ") was [" + strValue + "]", info); + } + + public function assertNull (id:String, ?info:PosInfos) + { + var value = get(id).testedNull; + var strValue = get(id).strValue; + Assert.assertionCount++; + if (!value) Assert.fail("Expected NULL but (" + id + ") was [" + strValue + "]", info); + } + + public function assertNotNull (id:String, ?info:PosInfos) + { + var value = get(id).testedNotNull; + var strValue = get(id).strValue; + Assert.assertionCount++; + if (!value) Assert.fail("Expected NOT NULL but (" + id + ") was [" + strValue + "]", info); + } +} + +typedef TestShellResult = +{ + id:String, + testedTrue:Bool, + testedFalse:Bool, + testedNull:Bool, + testedNotNull:Bool, + strValue:String +} \ No newline at end of file diff --git a/tests/unit/src/flixel/input/actions/VDFString.hx b/tests/unit/src/flixel/input/actions/VDFString.hx new file mode 100644 index 0000000000..adf8b17499 --- /dev/null +++ b/tests/unit/src/flixel/input/actions/VDFString.hx @@ -0,0 +1,116 @@ +package flixel.input.actions; + +class VDFString +{ + public static function get():String + { + return + '"In Game Actions"' + '\n' + + '{' + '\n' + + ' "actions"' + '\n' + + ' {' + '\n' + + ' "BattleControls"' + '\n' + + ' {' + '\n' + + ' "title" "#battle_title"' + '\n' + + ' "Button"' + '\n' + + ' {' + '\n' + + ' "punch" "#battle_punch"' + '\n' + + ' "kick" "#battle_kick"' + '\n' + + ' "jump" "#battle_jump"' + '\n' + + ' }' + '\n' + + ' "StickPadGyro"' + '\n' + + ' {' + '\n' + + ' "move"' + '\n' + + ' {' + '\n' + + ' "title" "#battle_move"' + '\n' + + ' "input_mode" "joystick_move"' + '\n' + + ' }' + '\n' + + ' }' + '\n' + + ' }' + '\n' + + ' ' + '\n' + + ' "MapControls"' + '\n' + + ' {' + '\n' + + ' "title" "#map_title"' + '\n' + + ' "StickPadGyro"' + '\n' + + ' {' + '\n' + + ' "move_map"' + '\n' + + ' {' + '\n' + + ' "title" "#map_move"' + '\n' + + ' "input_mode" "absolute_mouse"' + '\n' + + ' }' + '\n' + + ' "scroll_map"' + '\n' + + ' {' + '\n' + + ' "title" "#map_scroll"' + '\n' + + ' "input_mode" "absolute_mouse"' + '\n' + + ' }' + '\n' + + ' }' + '\n' + + ' "Button"' + '\n' + + ' {' + '\n' + + ' "map_select" "#map_select"' + '\n' + + ' "map_exit" "#map_exit"' + '\n' + + ' "map_menu" "#map_menu"' + '\n' + + ' "map_journal" "#map_journal"' + '\n' + + ' }' + '\n' + + ' }' + '\n' + + ' ' + '\n' + + ' "MenuControls"' + '\n' + + ' {' + '\n' + + ' "title" "#menu_title"' + '\n' + + ' "StickPadGyro"' + '\n' + + ' {' + '\n' + + ' "menu_move"' + '\n' + + ' {' + '\n' + + ' "title" "#menu_move"' + '\n' + + ' "input_mode" "joystick_move"' + '\n' + + ' }' + '\n' + + ' }' + '\n' + + ' "Button"' + '\n' + + ' {' + '\n' + + ' "menu_up" "#menu_up"' + '\n' + + ' "menu_down" "#menu_down"' + '\n' + + ' "menu_left" "#menu_left"' + '\n' + + ' "menu_right" "#menu_right"' + '\n' + + ' "menu_select" "#menu_select"' + '\n' + + ' "menu_cancel" "#menu_cancel"' + '\n' + + ' "menu_thing_1" "#menu_thing_1"' + '\n' + + ' "menu_thing_2" "#menu_thing_2"' + '\n' + + ' "menu_thing_3" "#menu_thing_3"' + '\n' + + ' "menu_menu" "#menu_menu"' + '\n' + + ' }' + '\n' + + ' }' + '\n' + + ' }' + '\n' + + ' "localization"' + '\n' + + ' {' + '\n' + + ' "english"' + '\n' + + ' {' + '\n' + + ' "battle_title" "Battle controls"' + '\n' + + ' "battle_punch" "Punch"' + '\n' + + ' "battle_kick" "Kick"' + '\n' + + ' "battle_jump" "Jump"' + '\n' + + ' "battle_move" "Move"' + '\n' + + ' ' + '\n' + + ' "map_title" "Map controls"' + '\n' + + ' "map_scroll" "Scroll map"' + '\n' + + ' "map_move" "Move location"' + '\n' + + ' "map_select" "Select"' + '\n' + + ' "map_exit" "Exit"' + '\n' + + ' "map_menu" "Options Menu"' + '\n' + + ' "map_journal" "Journal"' + '\n' + + ' ' + '\n' + + ' "menu_title" "Menu controls"' + '\n' + + ' "menu_move" "Analog movement"' + '\n' + + ' "menu_up" "Cursor up"' + '\n' + + ' "menu_down" "Cursor down"' + '\n' + + ' "menu_left" "Cursor left"' + '\n' + + ' "menu_right" "Cursor right"' + '\n' + + ' "menu_select" "Select"' + '\n' + + ' "menu_cancel" "Cancel"' + '\n' + + ' "menu_thing_1" "Menu choice 1"' + '\n' + + ' "menu_thing_2" "Menu choice 2"' + '\n' + + ' "menu_thing_3" "Menu choice 3"' + '\n' + + ' "menu_menu" "Menu"' + '\n' + + ' }' + '\n' + + ' }' + '\n' + + '}' + '\n'; + } +} \ No newline at end of file