package funkin.input; import flixel.FlxG; import flixel.input.FlxInput; import flixel.input.FlxInput.FlxInputState; import flixel.input.FlxKeyManager; import flixel.input.gamepad.FlxGamepad; import flixel.input.gamepad.FlxGamepadInputID; import flixel.input.keyboard.FlxKey; import flixel.input.keyboard.FlxKeyboard.FlxKeyInput; import flixel.input.keyboard.FlxKeyList; import flixel.util.FlxSignal.FlxTypedSignal; import funkin.play.notes.NoteDirection; import funkin.util.FlxGamepadUtil; import haxe.Int64; import lime.ui.Gamepad as LimeGamepad; import lime.ui.GamepadAxis as LimeGamepadAxis; import lime.ui.GamepadButton as LimeGamepadButton; import lime.ui.KeyCode; import lime.ui.KeyModifier; import openfl.events.KeyboardEvent; import openfl.ui.Keyboard; /** * A precise input manager that: * - Records the exact timestamp of when a key was pressed or released * - Only records key presses for keys bound to game inputs (up/down/left/right) */ class PreciseInputManager extends FlxKeyManager { public static var instance(get, null):PreciseInputManager; static function get_instance():PreciseInputManager { return instance ?? (instance = new PreciseInputManager()); } static final DIRECTIONS:Array = [NoteDirection.LEFT, NoteDirection.DOWN, NoteDirection.UP, NoteDirection.RIGHT]; public var onInputPressed:FlxTypedSignalVoid>; public var onInputReleased:FlxTypedSignalVoid>; /** * The list of keys that are bound to game inputs (up/down/left/right). */ var _keyList:Array; /** * The direction that a given key is bound to. */ var _keyListDir:Map; /** * A FlxGamepadID->Array, with FlxGamepadInputID being the counterpart to FlxKey. */ var _buttonList:Map>; var _buttonListArray:Array>; var _buttonListMap:Map>>; /** * A FlxGamepadID->FlxGamepadInputID->NoteDirection, with FlxGamepadInputID being the counterpart to FlxKey. */ var _buttonListDir:Map>; /** * The timestamp at which a given note direction was last pressed. */ var _dirPressTimestamps:Map; /** * The timestamp at which a given note direction was last released. */ var _dirReleaseTimestamps:Map; var _deviceBinds:MapInt64->Void, onButtonUp:LimeGamepadButton->Int64->Void }>; public function new() { super(PreciseInputList.new); _deviceBinds = []; _keyList = []; // _keyListMap // _keyListArray _keyListDir = new Map(); _buttonList = []; _buttonListMap = []; _buttonListArray = []; _buttonListDir = new Map>(); _dirPressTimestamps = new Map(); _dirReleaseTimestamps = new Map(); // Keyboard FlxG.stage.removeEventListener(KeyboardEvent.KEY_DOWN, onKeyDown); FlxG.stage.removeEventListener(KeyboardEvent.KEY_UP, onKeyUp); FlxG.stage.application.window.onKeyDownPrecise.add(handleKeyDown); FlxG.stage.application.window.onKeyUpPrecise.add(handleKeyUp); preventDefaultKeys = getPreventDefaultKeys(); onInputPressed = new FlxTypedSignalVoid>(); onInputReleased = new FlxTypedSignalVoid>(); } public static function getKeysForDirection(controls:Controls, noteDirection:NoteDirection) { return switch (noteDirection) { case NoteDirection.LEFT: controls.getKeysForAction(NOTE_LEFT); case NoteDirection.DOWN: controls.getKeysForAction(NOTE_DOWN); case NoteDirection.UP: controls.getKeysForAction(NOTE_UP); case NoteDirection.RIGHT: controls.getKeysForAction(NOTE_RIGHT); }; } public static function getButtonsForDirection(controls:Controls, noteDirection:NoteDirection) { return switch (noteDirection) { case NoteDirection.LEFT: controls.getButtonsForAction(NOTE_LEFT); case NoteDirection.DOWN: controls.getButtonsForAction(NOTE_DOWN); case NoteDirection.UP: controls.getButtonsForAction(NOTE_UP); case NoteDirection.RIGHT: controls.getButtonsForAction(NOTE_RIGHT); }; } /** * Convert from int to Int64. */ static final NS_PER_MS:Int64 = Constants.NS_PER_MS; /** * Returns a precise timestamp, measured in nanoseconds. * Timestamp is only useful for comparing against other timestamps. * * @return Int64 */ @:access(lime._internal.backend.native.NativeCFFI) public static function getCurrentTimestamp():Int64 { #if html5 // NOTE: This timestamp isn't that precise on standard HTML5 builds. // This is because of browser safeguards against timing attacks. // See https://web.dev/coop-coep to enable headers which allow for more precise timestamps. return haxe.Int64.fromFloat(js.Browser.window.performance.now()) * NS_PER_MS; #elseif cpp // NOTE: If the game hard crashes on this line, rebuild Lime! // `lime rebuild windows -clean` return lime._internal.backend.native.NativeCFFI.lime_sdl_get_ticks() * NS_PER_MS; #else throw "Eric didn't implement precise timestamps on this platform!"; #end } static function getPreventDefaultKeys():Array { return []; } /** * Call this whenever the user's inputs change. */ public function initializeKeys(controls:Controls):Void { clearKeys(); for (noteDirection in DIRECTIONS) { var keys = getKeysForDirection(controls, noteDirection); for (key in keys) { var input = new FlxKeyInput(key); _keyList.push(key); _keyListArray.push(input); _keyListMap.set(key, input); _keyListDir.set(key, noteDirection); } } } public function initializeButtons(controls:Controls, gamepad:FlxGamepad):Void { clearButtons(); var limeGamepad = FlxGamepadUtil.getLimeGamepad(gamepad); var callbacks = { onButtonDown: handleButtonDown.bind(gamepad), onButtonUp: handleButtonUp.bind(gamepad) }; limeGamepad.onButtonDownPrecise.add(callbacks.onButtonDown); limeGamepad.onButtonUpPrecise.add(callbacks.onButtonUp); for (noteDirection in DIRECTIONS) { var buttons = getButtonsForDirection(controls, noteDirection); for (button in buttons) { var input = new FlxInput(button); var buttonListEntry = _buttonList.get(gamepad.id); if (buttonListEntry == null) _buttonList.set(gamepad.id, buttonListEntry = []); buttonListEntry.push(button); _buttonListArray.push(input); var buttonListMapEntry = _buttonListMap.get(gamepad.id); if (buttonListMapEntry == null) _buttonListMap.set(gamepad.id, buttonListMapEntry = new Map>()); buttonListMapEntry.set(button, input); var buttonListDirEntry = _buttonListDir.get(gamepad.id); if (buttonListDirEntry == null) _buttonListDir.set(gamepad.id, buttonListDirEntry = new Map()); buttonListDirEntry.set(button, noteDirection); } } } /** * Get the time, in nanoseconds, since the given note direction was last pressed. * @param noteDirection The note direction to check. * @return An Int64 representing the time since the given note direction was last pressed. */ public function getTimeSincePressed(noteDirection:NoteDirection):Int64 { return getCurrentTimestamp() - _dirPressTimestamps.get(noteDirection); } /** * Get the time, in nanoseconds, since the given note direction was last released. * @param noteDirection The note direction to check. * @return An Int64 representing the time since the given note direction was last released. */ public function getTimeSinceReleased(noteDirection:NoteDirection):Int64 { return getCurrentTimestamp() - _dirReleaseTimestamps.get(noteDirection); } // TODO: Why doesn't this work? // @:allow(funkin.input.PreciseInputManager.PreciseInputList) public function getInputByKey(key:FlxKey):FlxKeyInput { return _keyListMap.get(key); } public function getInputByButton(gamepad:FlxGamepad, button:FlxGamepadInputID):FlxInput { return _buttonListMap.get(gamepad.id).get(button); } public function getDirectionForKey(key:FlxKey):NoteDirection { return _keyListDir.get(key); } public function getDirectionForButton(gamepad:FlxGamepad, button:FlxGamepadInputID):NoteDirection { return _buttonListDir.get(gamepad.id).get(button); } function getButton(gamepad:FlxGamepad, button:FlxGamepadInputID):FlxInput { return _buttonListMap.get(gamepad.id).get(button); } function updateButtonStates(gamepad:FlxGamepad, button:FlxGamepadInputID, down:Bool):Void { var input = getButton(gamepad, button); if (input == null) return; if (down) { input.press(); } else { input.release(); } } function handleKeyDown(keyCode:KeyCode, _:KeyModifier, timestamp:Int64):Void { var key:FlxKey = convertKeyCode(keyCode); if (_keyList.indexOf(key) == -1) return; // TODO: Remove this line with SDL3 when timestamps change meaning. // This is because SDL3's timestamps are measured in nanoseconds, not milliseconds. timestamp *= Constants.NS_PER_MS; updateKeyStates(key, true); if (getInputByKey(key)?.justPressed ?? false) { onInputPressed.dispatch( { noteDirection: getDirectionForKey(key), timestamp: timestamp }); _dirPressTimestamps.set(getDirectionForKey(key), timestamp); } } function handleKeyUp(keyCode:KeyCode, _:KeyModifier, timestamp:Int64):Void { var key:FlxKey = convertKeyCode(keyCode); if (_keyList.indexOf(key) == -1) return; // TODO: Remove this line with SDL3 when timestamps change meaning. // This is because SDL3's timestamps ar e measured in nanoseconds, not milliseconds. timestamp *= Constants.NS_PER_MS; updateKeyStates(key, false); if (getInputByKey(key)?.justReleased ?? false) { onInputReleased.dispatch( { noteDirection: getDirectionForKey(key), timestamp: timestamp }); _dirReleaseTimestamps.set(getDirectionForKey(key), timestamp); } } function handleButtonDown(gamepad:FlxGamepad, button:LimeGamepadButton, timestamp:Int64):Void { var buttonId:FlxGamepadInputID = FlxGamepadUtil.getInputID(gamepad, button); var buttonListEntry = _buttonList.get(gamepad.id); if (buttonListEntry == null || buttonListEntry.indexOf(buttonId) == -1) return; // TODO: Remove this line with SDL3 when timestamps change meaning. // This is because SDL3's timestamps ar e measured in nanoseconds, not milliseconds. timestamp *= Constants.NS_PER_MS; updateButtonStates(gamepad, buttonId, true); if (getInputByButton(gamepad, buttonId)?.justPressed ?? false) { onInputPressed.dispatch( { noteDirection: getDirectionForButton(gamepad, buttonId), timestamp: timestamp }); _dirPressTimestamps.set(getDirectionForButton(gamepad, buttonId), timestamp); } } function handleButtonUp(gamepad:FlxGamepad, button:LimeGamepadButton, timestamp:Int64):Void { var buttonId:FlxGamepadInputID = FlxGamepadUtil.getInputID(gamepad, button); var buttonListEntry = _buttonList.get(gamepad.id); if (buttonListEntry == null || buttonListEntry.indexOf(buttonId) == -1) return; // TODO: Remove this line with SDL3 when timestamps change meaning. // This is because SDL3's timestamps ar e measured in nanoseconds, not milliseconds. timestamp *= Constants.NS_PER_MS; updateButtonStates(gamepad, buttonId, false); if (getInputByButton(gamepad, buttonId)?.justReleased ?? false) { onInputReleased.dispatch( { noteDirection: getDirectionForButton(gamepad, buttonId), timestamp: timestamp }); _dirReleaseTimestamps.set(getDirectionForButton(gamepad, buttonId), timestamp); } } static function convertKeyCode(input:KeyCode):FlxKey { @:privateAccess { return Keyboard.__convertKeyCode(input); } } function clearKeys():Void { _keyListArray = []; _keyListMap.clear(); _keyListDir.clear(); } function clearButtons():Void { _buttonListArray = []; _buttonListDir.clear(); for (gamepad in _deviceBinds.keys()) { var callbacks = _deviceBinds.get(gamepad); var limeGamepad = FlxGamepadUtil.getLimeGamepad(gamepad); limeGamepad.onButtonDownPrecise.remove(callbacks.onButtonDown); limeGamepad.onButtonUpPrecise.remove(callbacks.onButtonUp); } _deviceBinds.clear(); } public override function destroy():Void { // Keyboard FlxG.stage.application.window.onKeyDownPrecise.remove(handleKeyDown); FlxG.stage.application.window.onKeyUpPrecise.remove(handleKeyUp); clearKeys(); clearButtons(); } } class PreciseInputList extends FlxKeyList { var _preciseInputManager:PreciseInputManager; public function new(state:FlxInputState, preciseInputManager:FlxKeyManager) { super(state, preciseInputManager); _preciseInputManager = cast preciseInputManager; } static function getKeysForDir(noteDir:NoteDirection):Array { return PreciseInputManager.getKeysForDirection(PlayerSettings.player1.controls, noteDir); } function isKeyValid(key:FlxKey):Bool { @:privateAccess { return _preciseInputManager._keyListMap.exists(key); } } public function checkFlxKey(key:FlxKey):Bool { if (isKeyValid(key)) return check(cast key); return false; } public function checkDir(noteDir:NoteDirection):Bool { for (key in getKeysForDir(noteDir)) { if (check(_preciseInputManager.getInputByKey(key)?.ID)) return true; } return false; } public var NOTE_LEFT(get, never):Bool; function get_NOTE_LEFT():Bool return checkDir(NoteDirection.LEFT); public var NOTE_DOWN(get, never):Bool; function get_NOTE_DOWN():Bool return checkDir(NoteDirection.DOWN); public var NOTE_UP(get, never):Bool; function get_NOTE_UP():Bool return checkDir(NoteDirection.UP); public var NOTE_RIGHT(get, never):Bool; function get_NOTE_RIGHT():Bool return checkDir(NoteDirection.RIGHT); } typedef PreciseInputEvent = { /** * The direction of the input. */ noteDirection:NoteDirection, /** * The timestamp of the input. Measured in nanoseconds. */ timestamp:Int64, };