Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
4a7694c
feat: start implementing functionality for multi controller
RoushilS Jul 21, 2026
a21de87
fix: working version
RoushilS Jul 21, 2026
4ec88c7
feat: dropdown for gamepad selection
RoushilS Jul 21, 2026
20e199a
feat: logical gamepad slot assigning
RoushilS Jul 21, 2026
a454314
feat: add slot parameter
RoushilS Jul 22, 2026
8f97243
feat: implement playerslot across subclasses
RoushilS Jul 22, 2026
0e436c8
feat: sync with dropdsown
RoushilS Jul 22, 2026
2671b5b
feat: use correct slot...
RoushilS Jul 22, 2026
2f600b6
update keysused to be a method
RoushilS Jul 22, 2026
fee25b4
style:fix
RoushilS Jul 22, 2026
81b5456
fix: fix test
RoushilS Jul 22, 2026
421ca1a
feat: refactor controller input
RoushilS Jul 28, 2026
d49ebac
style:fix
RoushilS Jul 28, 2026
501ee74
Update InputSystem.test.ts
RoushilS Jul 28, 2026
0fee914
style:fix
RoushilS Jul 28, 2026
d27a733
fix: comment review
RoushilS Aug 4, 2026
c4883f3
style:fix
RoushilS Aug 4, 2026
00d574b
Merge branch 'dev' into roushils/151/multi-controller-support
RoushilS Aug 5, 2026
9432087
feat: add nametag differentiator
RoushilS Aug 5, 2026
36cfd35
Merge branch 'dev' into roushils/151/multi-controller-support
RoushilS Aug 10, 2026
5ef0658
fix: anyone can edit scheme bindings
RoushilS Aug 18, 2026
7c996e3
format:fix
RoushilS Aug 18, 2026
4268a0d
Merge branch 'dev' into roushils/151/multi-controller-support
RoushilS Aug 18, 2026
32323d2
Merge branch 'dev' into roushils/151/multi-controller-support
PepperLola Aug 19, 2026
970e8d8
fix: allow gpindex to be null
RoushilS Aug 19, 2026
2f95a16
Merge branch 'roushils/151/multi-controller-support' of https://githu…
RoushilS Aug 19, 2026
e8f870b
format:fix
RoushilS Aug 19, 2026
f9c76c6
fix: nametag and indexing
RoushilS Aug 20, 2026
df8b173
fix: replace any with SynthesisBrain
RoushilS Aug 20, 2026
00afa11
fix: refactor test along with new indexing system
RoushilS Aug 20, 2026
14091ca
fix: re-commit
RoushilS Aug 20, 2026
e8201b3
Revert "fix: re-commit"
RoushilS Aug 20, 2026
e7a5562
fix: refactor slot system.
RoushilS Aug 21, 2026
7b18b3a
chore: format
RoushilS Aug 21, 2026
4d6dddc
fix: correct outdated comments
RoushilS Aug 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions fission/src/mirabuf/ScoringZoneSceneObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import ScoreTracker from "@/systems/match_mode/ScoreTracker"
import type { ScoringZonePreferences } from "@/systems/preferences/PreferenceTypes"
import World from "@/systems/World"
import { findListDifference } from "@/util/Utility"
import MirabufSceneObject from "./MirabufSceneObject"
import { RigidNodeAssociate } from "./MirabufSceneObject"
import type MirabufSceneObject from "./MirabufSceneObject"
import type { RigidNodeAssociate } from "./MirabufSceneObject"
import ZoneSceneObject from "./ZoneSceneObject"

class ScoringZoneSceneObject extends ZoneSceneObject<ScoringZonePreferences> {
Expand Down
4 changes: 4 additions & 0 deletions fission/src/systems/input/DefaultInputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ class DefaultInputs {
descriptiveName: "Full Controller",
customized: false,
usesGamepad: true,
playerSlot: 0,
supportedDrivetrains: [DriveType.ARCADE, DriveType.TANK],
usesTouchControls: false,
inputs: [
Expand Down Expand Up @@ -133,6 +134,7 @@ class DefaultInputs {
descriptiveName: "Left Stick",
customized: false,
usesGamepad: true,
playerSlot: 0,
usesTouchControls: false,
supportedDrivetrains: [DriveType.ARCADE],
inputs: [
Expand All @@ -155,6 +157,7 @@ class DefaultInputs {
descriptiveName: "Right Stick",
customized: false,
usesGamepad: true,
playerSlot: 0,
supportedDrivetrains: [DriveType.ARCADE],
usesTouchControls: false,
inputs: [
Expand Down Expand Up @@ -240,6 +243,7 @@ class DefaultInputs {
descriptiveName: "Dual Stick (Swerve)",
customized: false,
usesGamepad: true,
playerSlot: 0,
usesTouchControls: false,
supportedDrivetrains: [DriveType.SWERVE],
inputs: [
Expand Down
5 changes: 3 additions & 2 deletions fission/src/systems/input/InputSchemeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ class InputSchemeManager {
status: InputSchemeUseType.IN_USE,
}
scheme?.inputs?.forEach(input => {
input.keysUsed
input
.keysUsed(scheme.playerSlot ?? 0)
.filter(key => key != null)
.forEach(key => {
const entry = usedKeyMap.get(key)
Expand All @@ -137,7 +138,7 @@ class InputSchemeManager {

allSchemes.forEach(scheme => {
const conflictingSchemes = scheme.inputs.flatMap(input =>
input.keysUsed.flatMap(key => usedKeyMap.get(key) ?? [])
input.keysUsed(scheme.playerSlot ?? 0).flatMap(key => usedKeyMap.get(key) ?? [])
)
if (conflictingSchemes.length > 0) {
result[scheme.schemeName] ??= {
Expand Down
68 changes: 52 additions & 16 deletions fission/src/systems/input/InputSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ class InputSystem extends WorldSystem {
/** Whether the command palette is currently open, which blocks robot input */
private static _isCommandPaletteOpen: boolean = false

private static _gpIndex: number | null
public static gamepad: Gamepad | null
private static _gpIndexes: number[] = []
public static gamepads: (Gamepad | null)[] = []

/** Normalized joystick positions (-1 to 1) set by TouchControls component via react-joystick-component */
private static _leftJoystickPos: { x: number; y: number } = { x: 0, y: 0 }
Expand Down Expand Up @@ -93,8 +93,15 @@ class InputSystem extends WorldSystem {

public update(_: number): void {
// Fetch current gamepad information
if (InputSystem._gpIndex == null) InputSystem.gamepad = null
else InputSystem.gamepad = navigator.getGamepads()[InputSystem._gpIndex]
const rawGamepads = navigator.getGamepads()

for (const lookupIndex of InputSystem._gpIndexes) {
if (lookupIndex == null || rawGamepads[lookupIndex] == null) {
InputSystem.gamepads[lookupIndex] = null
} else {
InputSystem.gamepads[lookupIndex] = rawGamepads[lookupIndex]
}
}

if (!document.hasFocus()) this.clearKeyData()

Expand Down Expand Up @@ -156,7 +163,10 @@ class InputSystem extends WorldSystem {
)
}

InputSystem._gpIndex = event.gamepad.index
const newIndex = event.gamepad.index
if (!InputSystem._gpIndexes.includes(newIndex)) {
InputSystem._gpIndexes.push(newIndex)
}
}

/* Called once when a gamepad is first disconnected */
Expand All @@ -165,7 +175,13 @@ class InputSystem extends WorldSystem {
console.log("Gamepad disconnected from index %d: %s", event.gamepad.index, event.gamepad.id)
}

InputSystem._gpIndex = null
const removedIndex = event.gamepad.index

InputSystem._gpIndexes = InputSystem._gpIndexes.filter(idx => idx !== removedIndex)

if (InputSystem.gamepads[removedIndex]) {
InputSystem.gamepads[removedIndex] = null
}
}

/**
Expand Down Expand Up @@ -197,7 +213,7 @@ class InputSystem extends WorldSystem {

if (targetScheme == null || targetInput == null) return 0

return targetInput.getValue(targetScheme.usesGamepad, targetScheme.usesTouchControls)
return targetInput.getValue(targetScheme.usesGamepad, targetScheme.usesTouchControls, targetScheme.playerSlot)
}

/**
Expand All @@ -216,16 +232,27 @@ class InputSystem extends WorldSystem {
)
}

/**
* @param {number} playerSlot The logical player slot.
* @returns {Gamepad | null} The gamepad in that slot, or null if the slot is unoccupied.
*/
public static getGamepadBySlot(playerSlot: number): Gamepad | null {
const rawIndex = InputSystem._gpIndexes[playerSlot]
if (rawIndex == null) return null
return InputSystem.gamepads[rawIndex] ?? null

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const rawIndex = InputSystem._gpIndexes[playerSlot]
if (rawIndex == null) return null
return InputSystem.gamepads[rawIndex] ?? null
return InputSystem.gamepads[playerSlot]

Given how _gpIndexes is constructed, InputSystem._gpIndexes[playerSlot] will just return playerSlot, meaning you can skip the lookup entirely. The ?? null is also only important if InputSystem.gamepads[playerSlot] could return undefined and you want to guarantee the function returns null; in this case I don't think that's possible.

}

/**
* @param {number} axisNumber The joystick axis index. Must be an integer.
* @param {number} playerSlot The logical player slot for the gamepad (0 = first connected). Must be an integer.
* @returns {number} A number between -1 and 1 based on the position of this axis or 0 if no gamepad is connected or the axis is not found.
*/
public static getGamepadAxis(axisNumber: number): number {
if (InputSystem.gamepad == null) return 0

if (axisNumber < 0 || axisNumber >= InputSystem.gamepad.axes.length) return 0
public static getGamepadAxis(axisNumber: number, playerSlot: number = 0): number {
const targetGamepad = InputSystem.getGamepadBySlot(playerSlot)
if (targetGamepad == null) return 0
if (axisNumber < 0 || axisNumber >= targetGamepad.axes.length) return 0

const value = InputSystem.gamepad.axes[axisNumber]
const value = targetGamepad.axes[axisNumber]

// Return value with a deadband
return Math.abs(value) < 0.15 ? 0 : value
Expand All @@ -234,19 +261,28 @@ class InputSystem extends WorldSystem {
/**
*
* @param {number} buttonNumber - The gamepad button index. Must be an integer.
* @param {number} playerSlot - The logical player slot for the gamepad (0 = first connected). Must be an integer.
* @returns {boolean} True if the button is pressed, false if not, a gamepad isn't connected, or the button can't be found.
*/
public static isGamepadButtonPressed(buttonNumber: number): boolean {
if (InputSystem.gamepad == null) return false
public static isGamepadButtonPressed(buttonNumber: number, playerSlot: number = 0): boolean {
const targetGamepad = InputSystem.getGamepadBySlot(playerSlot)
if (targetGamepad == null) return false

if (buttonNumber < 0 || buttonNumber >= InputSystem.gamepad.buttons.length) return false
if (buttonNumber < 0 || buttonNumber >= targetGamepad.buttons.length) return false

const button = InputSystem.gamepad.buttons[buttonNumber]
const button = targetGamepad.buttons[buttonNumber]
if (button == null) return false

return button.pressed
}

/**
* @returns {number} The number of currently connected gamepads, effectively all useable slots
*/
public static getConnectedPlayerCount(): number {
return InputSystem._gpIndexes.length
}

/** Returns a number between -1 and 1 from the touch controls */
public static getTouchControlsAxis(axisType: TouchControlsAxes): number {
if (axisType === TouchControlsAxes.LEFT_X) return InputSystem._leftJoystickPos.x
Expand Down
1 change: 1 addition & 0 deletions fission/src/systems/input/InputTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export type InputScheme = {
descriptiveName: string
customized: boolean
usesGamepad: boolean
playerSlot?: number
usesTouchControls: boolean
supportedDrivetrains: DriveType[]
inputs: Input[]
Expand Down
16 changes: 8 additions & 8 deletions fission/src/systems/input/inputs/AxisInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export default class AxisInput extends Input {
* @returns {number} KEYBOARD: 1 if positive pressed, -1 if negative pressed, or 0 if none or both are pressed.
* @returns {number} GAMEPAD: a number between -1 and 1 with a deadband in the middle.
*/
getValue(useGamepad: boolean, useTouchControls: boolean): number {
getValue(useGamepad: boolean, useTouchControls: boolean, playerSlot: number = 0): number {
const matchModeType = MatchMode.getInstance().getMatchModeType()
if (matchModeType === MatchModeType.MATCH_ENDED || matchModeType === MatchModeType.AUTONOMOUS) {
return 0
Expand All @@ -161,12 +161,12 @@ export default class AxisInput extends Input {
if (useGamepad) {
// Gamepad joystick axis
if (!this.useGamepadButtons)
return InputSystem.getGamepadAxis(this.gamepadAxisNumber) * (this.joystickInverted ? -1 : 1)
return InputSystem.getGamepadAxis(this.gamepadAxisNumber, playerSlot) * (this.joystickInverted ? -1 : 1)

// Gamepad button axis
return (
(InputSystem.isGamepadButtonPressed(this.posGamepadButton) ? 1 : 0) -
(InputSystem.isGamepadButtonPressed(this.negGamepadButton) ? 1 : 0)
(InputSystem.isGamepadButtonPressed(this.posGamepadButton, playerSlot) ? 1 : 0) -
(InputSystem.isGamepadButtonPressed(this.negGamepadButton, playerSlot) ? 1 : 0)
)
}

Expand All @@ -181,13 +181,13 @@ export default class AxisInput extends Input {
)
}

get keysUsed(): KeyDescriptor[] {
keysUsed(playerSlot: number = 0): KeyDescriptor[] {
return [
this.describeKey(this.posKeyCode, this.posKeyModifiers),
this.describeKey(this.negKeyCode, this.negKeyModifiers),
this.describeGamepadBtn(this.posGamepadButton),
this.describeGamepadBtn(this.negGamepadButton),
this.describeGamepadAxis(this.gamepadAxisNumber),
this.describeGamepadBtn(this.posGamepadButton, playerSlot),
this.describeGamepadBtn(this.negGamepadButton, playerSlot),
this.describeGamepadAxis(this.gamepadAxisNumber, playerSlot),
this.describeTouchAxis(this.touchControlAxis),
]
}
Expand Down
11 changes: 7 additions & 4 deletions fission/src/systems/input/inputs/ButtonInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,23 +31,26 @@ export default class ButtonInput extends Input {
* @param useGamepad Looks at the gamepad if true and the keyboard if false.
* @returns 1 if pressed, 0 if not pressed or not found.
*/
getValue(useGamepad: boolean): number {
getValue(useGamepad: boolean, _useTouchControls: boolean = false, playerSlot: number = 0): number {
const matchModeType = MatchMode.getInstance().getMatchModeType()
if (matchModeType === MatchModeType.MATCH_ENDED || matchModeType === MatchModeType.AUTONOMOUS) {
return 0
}

// Gamepad button input
if (useGamepad) {
return InputSystem.isGamepadButtonPressed(this.gamepadButton) ? 1 : 0
return InputSystem.isGamepadButtonPressed(this.gamepadButton, playerSlot) ? 1 : 0
}

// Keyboard button input
return InputSystem.isKeyPressed(this.keyCode, this.keyModifiers) ? 1 : 0
}

get keysUsed(): KeyDescriptor[] {
return [this.describeKey(this.keyCode, this.keyModifiers), this.describeGamepadBtn(this.gamepadButton)]
keysUsed(playerSlot: number = 0): KeyDescriptor[] {
return [
this.describeKey(this.keyCode, this.keyModifiers),
this.describeGamepadBtn(this.gamepadButton, playerSlot),
]
}

static onGamepad(inputName: InputName, gamepadButton: number) {
Expand Down
15 changes: 9 additions & 6 deletions fission/src/systems/input/inputs/Input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,12 @@ export default abstract class Input {
}

// Returns the current value of the input. Range depends on input type
abstract getValue(useGamepad: boolean, useTouchControls: boolean): number
abstract getValue(useGamepad: boolean, useTouchControls: boolean, playerSlot?: number): number

abstract get keysUsed(): KeyDescriptor[]
/**
* @param {number} playerSlot - The logical player slot the owning scheme reads from.
*/
abstract keysUsed(playerSlot?: number): KeyDescriptor[]

protected describeKey(id: KeyCode, modifiers?: ModifierState): KeyDescriptor {
if (id == "") {
Expand All @@ -42,17 +45,17 @@ export default abstract class Input {
}
return `${inputDriveTypeAssociations[this.inputName] ?? ""}_${id}` as KeyDescriptor
}
protected describeGamepadBtn(button: number): KeyDescriptor {
protected describeGamepadBtn(button: number, playerSlot: number = 0): KeyDescriptor {
if (button == -1) {
return null
}
return `${inputDriveTypeAssociations[this.inputName] ?? ""}_gamepadBtn${button}` as KeyDescriptor
return `slot${playerSlot}_${inputDriveTypeAssociations[this.inputName] ?? ""}_gamepadBtn${button}` as KeyDescriptor
}
protected describeGamepadAxis(axis: number): KeyDescriptor {
protected describeGamepadAxis(axis: number, playerSlot: number = 0): KeyDescriptor {
if (axis == -1) {
return null
}
return `${inputDriveTypeAssociations[this.inputName] ?? ""}_gamepadAxis${axis}` as KeyDescriptor
return `slot${playerSlot}_${inputDriveTypeAssociations[this.inputName] ?? ""}_gamepadAxis${axis}` as KeyDescriptor
}
protected describeTouchAxis(axis: TouchControlsAxes): KeyDescriptor {
if (axis == TouchControlsAxes.NONE) {
Expand Down
5 changes: 3 additions & 2 deletions fission/src/test/InputSystem.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ describe("Gamepad Input Check", () => {
test("Disconnect event", () => {
// The connection event is implicitly tested by registering a fake gamepad
window.dispatchEvent(Object.assign(new Event("gamepaddisconnected"), { gamepad: fakeGamepad }))
expect(InputSystem["_gpIndex"]).toBeNull()
expect(InputSystem["_gpIndexes"]).toHaveLength(0)
})

test("Get input with gamepad scheme", () => {
Expand All @@ -299,7 +299,8 @@ describe("Default Input Scheme Checks", () => {
DefaultInputs.defaultInputCopies.forEach(scheme => {
const usedKeys = new Map<KeyDescriptor, number>()
scheme.inputs.forEach(input => {
input.keysUsed
input
.keysUsed(scheme.playerSlot ?? 0)
.filter(key => key != null)
.forEach(key => usedKeys.set(key, (usedKeys.get(key) ?? 0) + 1))
usedKeys.forEach((count, key) => {
Expand Down
2 changes: 1 addition & 1 deletion fission/src/test/mirabuf/ProtectedZoneSceneObject.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ContactType } from "@/mirabuf/ZoneTypes"
import { MatchModeType } from "@/systems/match_mode/MatchModeTypes"
import ScoreTracker from "@/systems/match_mode/ScoreTracker"
import type { ProtectedZonePreferences } from "@/systems/preferences/PreferenceTypes"
import MirabufSceneObject from "../../mirabuf/MirabufSceneObject"
import type MirabufSceneObject from "../../mirabuf/MirabufSceneObject"
import ProtectedZoneSceneObject from "../../mirabuf/ProtectedZoneSceneObject"
import { createBodyMock } from "../mocks/jolt"
import JOLT from "@/util/loading/JoltSyncLoader"
Expand Down
Loading
Loading