Skip to content

Commit 1ce0aee

Browse files
sshbioclaude
andcommitted
Add gamepad controller support for physical match control
Introduces a GamepadController provider that maps PS5 DualSense (and compatible) gamepad buttons to game commands (HALT, STOP, kick-offs, free kicks, timeouts, continue actions, auto-continue toggle). Adds a GamepadStatus toolbar component showing connection state, active button, and a button-to-action reference tooltip. Also updates frontend protobuf generated files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent e83a7d0 commit 1ce0aee

23 files changed

+381
-19
lines changed

frontend/src/App.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import ProtocolList from "@/components/protocol/ProtocolList.vue";
99
import {useQuasar} from "quasar";
1010
import {useUiStateStore} from "@/store/uiState";
1111
import {useProtocolStore} from "@/store/protocolState";
12+
import GamepadStatus from "@/components/GamepadStatus.vue";
1213
1314
const uiStore = useUiStateStore()
1415
const protocolStore = useProtocolStore()
@@ -89,6 +90,7 @@ const dev = computed(() => {
8990
<q-toggle dense flat round class="q-mx-sm" @click="toggleShortcuts" :model-value="showShortcuts" color="black">
9091
Show Shortcuts
9192
</q-toggle>
93+
<GamepadStatus class="q-mx-xs"/>
9294
<StatusMessageButton/>
9395
<q-btn dense flat round icon="menu" @click="toggleRightDrawer"/>
9496
</q-toolbar>
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<script setup lang="ts">
2+
import {computed, inject} from 'vue'
3+
import type {GamepadController} from "@/providers/gamepadController";
4+
import {GAMEPAD_BUTTON_ACTIONS, GAMEPAD_BUTTON_LABELS} from "@/providers/gamepadController";
5+
6+
const gamepadController = inject<GamepadController>('gamepad-controller')!
7+
8+
const connected = computed(() => gamepadController.state.connected)
9+
const gamepadId = computed(() => {
10+
// Shorten the raw gamepad id for display
11+
const id = gamepadController.state.gamepadId
12+
if (!id) return ''
13+
// Try to extract a human-friendly name from strings like:
14+
// "DualSense Wireless Controller (STANDARD GAMEPAD Vendor: 054c Product: 0ce6)"
15+
const parenIdx = id.indexOf('(')
16+
return parenIdx > 0 ? id.slice(0, parenIdx).trim() : id
17+
})
18+
19+
const activeButton = computed(() => gamepadController.state.activeButton)
20+
const activeButtonLabel = computed(() => {
21+
const b = activeButton.value
22+
if (b === null) return null
23+
return GAMEPAD_BUTTON_LABELS[b] ?? `Button ${b}`
24+
})
25+
const activeActionLabel = computed(() => {
26+
const b = activeButton.value
27+
if (b === null) return null
28+
return GAMEPAD_BUTTON_ACTIONS[b] ?? null
29+
})
30+
31+
// Gamepad reference button layout for the tooltip
32+
const buttonMap = Object.entries(GAMEPAD_BUTTON_LABELS)
33+
.filter(([idx]) => GAMEPAD_BUTTON_ACTIONS[Number(idx)] !== undefined)
34+
.map(([idx, label]) => ({
35+
button: label,
36+
action: GAMEPAD_BUTTON_ACTIONS[Number(idx)],
37+
}))
38+
</script>
39+
40+
<template>
41+
<!-- Gamepad icon shown in the toolbar -->
42+
<q-btn
43+
dense flat round
44+
:icon="connected ? 'sports_esports' : 'videogame_asset_off'"
45+
:color="connected ? 'white' : 'grey-5'"
46+
:title="connected ? `Connected: ${gamepadId}` : 'No gamepad connected'"
47+
>
48+
<!-- Active button flash badge -->
49+
<q-badge v-if="activeButton !== null" floating color="amber" text-color="black">
50+
{{ activeButtonLabel }}
51+
</q-badge>
52+
53+
<!-- Tooltip with full button map -->
54+
<q-tooltip anchor="bottom right" self="top right" :offset="[0, 8]" max-width="360px">
55+
<div class="text-subtitle2 q-mb-xs">
56+
<q-icon name="sports_esports" class="q-mr-xs"/>
57+
Gamepad Controls
58+
</div>
59+
60+
<div v-if="!connected" class="text-caption text-grey-4">
61+
Connect a PS5 DualSense or compatible gamepad to use physical controls.
62+
</div>
63+
64+
<template v-else>
65+
<div class="text-caption text-grey-3 q-mb-sm">{{ gamepadId }}</div>
66+
67+
<q-markup-table dense flat dark class="gamepad-table">
68+
<thead>
69+
<tr>
70+
<th class="text-left">Button</th>
71+
<th class="text-left">Action</th>
72+
</tr>
73+
</thead>
74+
<tbody>
75+
<tr
76+
v-for="entry in buttonMap"
77+
:key="entry.button"
78+
:class="{'text-amber': activeButtonLabel === entry.button}"
79+
>
80+
<td>
81+
<q-chip dense size="sm" color="grey-8" text-color="white">{{ entry.button }}</q-chip>
82+
</td>
83+
<td class="text-caption">{{ entry.action }}</td>
84+
</tr>
85+
</tbody>
86+
</q-markup-table>
87+
</template>
88+
</q-tooltip>
89+
</q-btn>
90+
91+
<!-- Live action indicator when a button is pressed -->
92+
<transition name="fade">
93+
<q-chip
94+
v-if="activeButton !== null && activeActionLabel"
95+
dense
96+
color="amber"
97+
text-color="black"
98+
icon="sports_esports"
99+
class="q-mx-xs"
100+
>
101+
{{ activeActionLabel }}
102+
</q-chip>
103+
</transition>
104+
</template>
105+
106+
<style scoped>
107+
.gamepad-table {
108+
min-width: 280px;
109+
}
110+
111+
.fade-enter-active,
112+
.fade-leave-active {
113+
transition: opacity 0.15s ease;
114+
}
115+
116+
.fade-enter-from,
117+
.fade-leave-to {
118+
opacity: 0;
119+
}
120+
</style>

frontend/src/plugins/control/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,18 @@ import type {App} from "vue";
66
import {useProtocolStore} from "@/store/protocolState";
77
import {ManualActions} from "@/providers/manualActions";
88
import {Shortcuts} from "@/providers/shortcuts";
9+
import {GamepadController} from "@/providers/gamepadController";
910

1011
export const control = {
1112
install(app: App) {
1213
const controlApi = new ControlApi()
1314
const manualActions = new ManualActions(controlApi)
1415
const shortcuts = new Shortcuts(manualActions, controlApi)
16+
const gamepadController = new GamepadController(manualActions, controlApi)
1517
app.provide('control-api', controlApi)
1618
app.provide('command-actions', manualActions)
1719
app.provide('shortcuts', shortcuts)
20+
app.provide('gamepad-controller', gamepadController)
1821

1922
const matchStateStore = useMatchStateStore()
2023
controlApi.RegisterConsumer((output: OutputJson) => {

frontend/src/proto/api/ssl_gc_api_pb.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// @generated by protoc-gen-es v2.2.3 with parameter "target=ts,json_types=true"
22
// @generated from file api/ssl_gc_api.proto (syntax proto2)
3-
/* eslint-disable */
3+
44

55
import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv1";
66
import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv1";

frontend/src/proto/ci/autoref/ssl_autoref_ci_pb.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// @generated by protoc-gen-es v2.2.3 with parameter "target=ts,json_types=true"
22
// @generated from file ci/autoref/ssl_autoref_ci.proto (syntax proto2)
3-
/* eslint-disable */
3+
44

55
import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv1";
66
import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv1";

frontend/src/proto/ci/ssl_gc_ci_pb.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// @generated by protoc-gen-es v2.2.3 with parameter "target=ts,json_types=true"
22
// @generated from file ci/ssl_gc_ci.proto (syntax proto2)
3-
/* eslint-disable */
3+
44

55
import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv1";
66
import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv1";

frontend/src/proto/engine/ssl_gc_engine_config_pb.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// @generated by protoc-gen-es v2.2.3 with parameter "target=ts,json_types=true"
22
// @generated from file engine/ssl_gc_engine_config.proto (syntax proto2)
3-
/* eslint-disable */
3+
44

55
import type { GenEnum, GenFile, GenMessage } from "@bufbuild/protobuf/codegenv1";
66
import { enumDesc, fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv1";

frontend/src/proto/engine/ssl_gc_engine_pb.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// @generated by protoc-gen-es v2.2.3 with parameter "target=ts,json_types=true"
22
// @generated from file engine/ssl_gc_engine.proto (syntax proto2)
3-
/* eslint-disable */
3+
44

55
import type { GenEnum, GenFile, GenMessage } from "@bufbuild/protobuf/codegenv1";
66
import { enumDesc, fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv1";

frontend/src/proto/geom/ssl_gc_geometry_pb.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// @generated by protoc-gen-es v2.2.3 with parameter "target=ts,json_types=true"
22
// @generated from file geom/ssl_gc_geometry.proto (syntax proto2)
3-
/* eslint-disable */
3+
44

55
import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv1";
66
import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv1";

frontend/src/proto/rcon/ssl_gc_rcon_autoref_pb.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// @generated by protoc-gen-es v2.2.3 with parameter "target=ts,json_types=true"
22
// @generated from file rcon/ssl_gc_rcon_autoref.proto (syntax proto2)
3-
/* eslint-disable */
3+
44

55
import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv1";
66
import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv1";

0 commit comments

Comments
 (0)