mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-09 04:19:27 -07:00
fix: address controller remap review feedback
This commit is contained in:
@@ -81,43 +81,43 @@
|
||||
"rightStickPress": 10, // Raw button index used for controller R3 input.
|
||||
"leftTrigger": 6, // Raw button index used for controller L2 input.
|
||||
"rightTrigger": 7 // Raw button index used for controller R2 input.
|
||||
}, // Button indices setting.
|
||||
}, // Semantic button-name reference mapping used for legacy configs and debug output. Updating it does not rewrite existing raw binding descriptors.
|
||||
"bindings": {
|
||||
"toggleLookup": {
|
||||
"kind": "button", // Discrete binding input source kind. Values: none | button | axis
|
||||
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||
"buttonIndex": 0 // Raw button index captured for this discrete controller action.
|
||||
}, // Controller binding descriptor for toggling lookup. Use Alt+C learn mode or set a raw button/axis descriptor manually.
|
||||
}, // Controller binding descriptor for toggling lookup. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
"closeLookup": {
|
||||
"kind": "button", // Discrete binding input source kind. Values: none | button | axis
|
||||
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||
"buttonIndex": 1 // Raw button index captured for this discrete controller action.
|
||||
}, // Controller binding descriptor for closing lookup. Use Alt+C learn mode or set a raw button/axis descriptor manually.
|
||||
}, // Controller binding descriptor for closing lookup. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
"toggleKeyboardOnlyMode": {
|
||||
"kind": "button", // Discrete binding input source kind. Values: none | button | axis
|
||||
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||
"buttonIndex": 3 // Raw button index captured for this discrete controller action.
|
||||
}, // Controller binding descriptor for toggling keyboard-only mode. Use Alt+C learn mode or set a raw button/axis descriptor manually.
|
||||
}, // Controller binding descriptor for toggling keyboard-only mode. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
"mineCard": {
|
||||
"kind": "button", // Discrete binding input source kind. Values: none | button | axis
|
||||
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||
"buttonIndex": 2 // Raw button index captured for this discrete controller action.
|
||||
}, // Controller binding descriptor for mining the active card. Use Alt+C learn mode or set a raw button/axis descriptor manually.
|
||||
}, // Controller binding descriptor for mining the active card. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
"quitMpv": {
|
||||
"kind": "button", // Discrete binding input source kind. Values: none | button | axis
|
||||
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||
"buttonIndex": 6 // Raw button index captured for this discrete controller action.
|
||||
}, // Controller binding descriptor for quitting mpv. Use Alt+C learn mode or set a raw button/axis descriptor manually.
|
||||
}, // Controller binding descriptor for quitting mpv. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
"previousAudio": {
|
||||
"kind": "none" // Discrete binding input source kind. Values: none | button | axis
|
||||
}, // Controller binding descriptor for previous Yomitan audio. Use Alt+C learn mode or set a raw button/axis descriptor manually.
|
||||
"kind": "none" // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||
}, // Controller binding descriptor for previous Yomitan audio. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
"nextAudio": {
|
||||
"kind": "button", // Discrete binding input source kind. Values: none | button | axis
|
||||
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||
"buttonIndex": 5 // Raw button index captured for this discrete controller action.
|
||||
}, // Controller binding descriptor for next Yomitan audio. Use Alt+C learn mode or set a raw button/axis descriptor manually.
|
||||
}, // Controller binding descriptor for next Yomitan audio. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
"playCurrentAudio": {
|
||||
"kind": "button", // Discrete binding input source kind. Values: none | button | axis
|
||||
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||
"buttonIndex": 4 // Raw button index captured for this discrete controller action.
|
||||
}, // Controller binding descriptor for playing the current Yomitan audio. Use Alt+C learn mode or set a raw button/axis descriptor manually.
|
||||
}, // Controller binding descriptor for playing the current Yomitan audio. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
"toggleMpvPause": {
|
||||
"kind": "button", // Discrete binding input source kind. Values: none | button | axis
|
||||
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||
"buttonIndex": 9 // Raw button index captured for this discrete controller action.
|
||||
}, // Controller binding descriptor for toggling mpv play/pause. Use Alt+C learn mode or set a raw button/axis descriptor manually.
|
||||
}, // Controller binding descriptor for toggling mpv play/pause. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
"leftStickHorizontal": {
|
||||
"kind": "axis", // Analog binding input source kind. Values: none | axis
|
||||
"axisIndex": 0, // Raw axis index captured for this analog controller action.
|
||||
@@ -138,7 +138,7 @@
|
||||
"axisIndex": 4, // Raw axis index captured for this analog controller action.
|
||||
"dpadFallback": "none" // Optional D-pad fallback used when this analog controller action should also read D-pad input. Values: none | horizontal | vertical
|
||||
} // Axis binding descriptor used for popup page jumps. Use Alt+C learn mode or set a raw axis descriptor manually.
|
||||
} // Bindings setting.
|
||||
} // Raw controller binding descriptors saved by Alt+C learn mode. For discrete axis bindings, kind "axis" requires axisIndex and direction.
|
||||
}, // Gamepad support for the visible overlay while keyboard-only mode is active.
|
||||
|
||||
// ==========================================
|
||||
|
||||
@@ -517,6 +517,7 @@ Important behavior:
|
||||
- `Alt+C` opens the controller config modal, where you can save the selected controller and remap actions inline.
|
||||
- Click `Learn`, then press the next fresh button, trigger, or stick direction you want to bind for that overlay action.
|
||||
- `Alt+Shift+C` opens a live debug modal showing raw axes/button values plus a ready-to-copy `buttonIndices` config block.
|
||||
- `controller.buttonIndices` is a semantic reference/legacy mapping. Changing it does not rewrite the raw numeric descriptor values already stored under `controller.bindings`.
|
||||
- Turning keyboard-only mode off clears the keyboard-only token highlight state.
|
||||
- Closing the Yomitan popup clears the temporary native text-selection fill, but keeps controller token selection active.
|
||||
|
||||
@@ -584,10 +585,26 @@ Default logical mapping:
|
||||
|
||||
Discrete bindings may use raw button indices or raw axis directions, and analog bindings use raw axis indices with optional D-pad fallback. The `Alt+C` learn flow writes those descriptors for you, so manual edits are only needed when you want to script or copy exact mappings.
|
||||
|
||||
If you bind a discrete action to an axis manually, include `direction`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"controller": {
|
||||
"bindings": {
|
||||
"toggleLookup": { "kind": "axis", "axisIndex": 5, "direction": "positive" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Treat `controller.buttonIndices` as reference-only unless you are still using legacy semantic bindings or copying values from the debug modal. Updating `controller.buttonIndices` alone does not rewrite the hardcoded raw numeric values already present in `controller.bindings`. If you need a real remap, prefer the `Alt+C` learn flow so both the source and the descriptor shape stay correct.
|
||||
|
||||
If you choose to bind `L2` or `R2` manually, set `triggerInputMode` to `analog` and tune `triggerDeadzone` when your controller reports triggers as analog values instead of digital pressed/not-pressed buttons. `auto` accepts either style and remains the default.
|
||||
|
||||
If your controller reports non-standard raw button numbers, override `controller.buttonIndices` using values from the `Alt+Shift+C` debug modal.
|
||||
|
||||
If you update this controller documentation or the generated controller examples, run `bun run docs:test` and `bun run docs:build` before merging.
|
||||
|
||||
Tune `scrollPixelsPerSecond`, `horizontalJumpPixels`, deadzones, repeat timing, and `buttonIndices` to match your controller. See [config.example.jsonc](/config.example.jsonc) for the full generated comments for every controller field.
|
||||
|
||||
### Manual Card Update Shortcuts
|
||||
|
||||
@@ -81,43 +81,43 @@
|
||||
"rightStickPress": 10, // Raw button index used for controller R3 input.
|
||||
"leftTrigger": 6, // Raw button index used for controller L2 input.
|
||||
"rightTrigger": 7 // Raw button index used for controller R2 input.
|
||||
}, // Button indices setting.
|
||||
}, // Semantic button-name reference mapping used for legacy configs and debug output. Updating it does not rewrite existing raw binding descriptors.
|
||||
"bindings": {
|
||||
"toggleLookup": {
|
||||
"kind": "button", // Discrete binding input source kind. Values: none | button | axis
|
||||
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||
"buttonIndex": 0 // Raw button index captured for this discrete controller action.
|
||||
}, // Controller binding descriptor for toggling lookup. Use Alt+C learn mode or set a raw button/axis descriptor manually.
|
||||
}, // Controller binding descriptor for toggling lookup. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
"closeLookup": {
|
||||
"kind": "button", // Discrete binding input source kind. Values: none | button | axis
|
||||
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||
"buttonIndex": 1 // Raw button index captured for this discrete controller action.
|
||||
}, // Controller binding descriptor for closing lookup. Use Alt+C learn mode or set a raw button/axis descriptor manually.
|
||||
}, // Controller binding descriptor for closing lookup. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
"toggleKeyboardOnlyMode": {
|
||||
"kind": "button", // Discrete binding input source kind. Values: none | button | axis
|
||||
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||
"buttonIndex": 3 // Raw button index captured for this discrete controller action.
|
||||
}, // Controller binding descriptor for toggling keyboard-only mode. Use Alt+C learn mode or set a raw button/axis descriptor manually.
|
||||
}, // Controller binding descriptor for toggling keyboard-only mode. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
"mineCard": {
|
||||
"kind": "button", // Discrete binding input source kind. Values: none | button | axis
|
||||
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||
"buttonIndex": 2 // Raw button index captured for this discrete controller action.
|
||||
}, // Controller binding descriptor for mining the active card. Use Alt+C learn mode or set a raw button/axis descriptor manually.
|
||||
}, // Controller binding descriptor for mining the active card. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
"quitMpv": {
|
||||
"kind": "button", // Discrete binding input source kind. Values: none | button | axis
|
||||
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||
"buttonIndex": 6 // Raw button index captured for this discrete controller action.
|
||||
}, // Controller binding descriptor for quitting mpv. Use Alt+C learn mode or set a raw button/axis descriptor manually.
|
||||
}, // Controller binding descriptor for quitting mpv. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
"previousAudio": {
|
||||
"kind": "none" // Discrete binding input source kind. Values: none | button | axis
|
||||
}, // Controller binding descriptor for previous Yomitan audio. Use Alt+C learn mode or set a raw button/axis descriptor manually.
|
||||
"kind": "none" // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||
}, // Controller binding descriptor for previous Yomitan audio. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
"nextAudio": {
|
||||
"kind": "button", // Discrete binding input source kind. Values: none | button | axis
|
||||
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||
"buttonIndex": 5 // Raw button index captured for this discrete controller action.
|
||||
}, // Controller binding descriptor for next Yomitan audio. Use Alt+C learn mode or set a raw button/axis descriptor manually.
|
||||
}, // Controller binding descriptor for next Yomitan audio. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
"playCurrentAudio": {
|
||||
"kind": "button", // Discrete binding input source kind. Values: none | button | axis
|
||||
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||
"buttonIndex": 4 // Raw button index captured for this discrete controller action.
|
||||
}, // Controller binding descriptor for playing the current Yomitan audio. Use Alt+C learn mode or set a raw button/axis descriptor manually.
|
||||
}, // Controller binding descriptor for playing the current Yomitan audio. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
"toggleMpvPause": {
|
||||
"kind": "button", // Discrete binding input source kind. Values: none | button | axis
|
||||
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||
"buttonIndex": 9 // Raw button index captured for this discrete controller action.
|
||||
}, // Controller binding descriptor for toggling mpv play/pause. Use Alt+C learn mode or set a raw button/axis descriptor manually.
|
||||
}, // Controller binding descriptor for toggling mpv play/pause. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||
"leftStickHorizontal": {
|
||||
"kind": "axis", // Analog binding input source kind. Values: none | axis
|
||||
"axisIndex": 0, // Raw axis index captured for this analog controller action.
|
||||
@@ -138,7 +138,7 @@
|
||||
"axisIndex": 4, // Raw axis index captured for this analog controller action.
|
||||
"dpadFallback": "none" // Optional D-pad fallback used when this analog controller action should also read D-pad input. Values: none | horizontal | vertical
|
||||
} // Axis binding descriptor used for popup page jumps. Use Alt+C learn mode or set a raw axis descriptor manually.
|
||||
} // Bindings setting.
|
||||
} // Raw controller binding descriptors saved by Alt+C learn mode. For discrete axis bindings, kind "axis" requires axisIndex and direction.
|
||||
}, // Gamepad support for the visible overlay while keyboard-only mode is active.
|
||||
|
||||
// ==========================================
|
||||
|
||||
@@ -1926,7 +1926,7 @@ test('template generator includes known keys', () => {
|
||||
);
|
||||
assert.match(
|
||||
output,
|
||||
/"kind": "button",? \/\/ Discrete binding input source kind\. Values: none \| button \| axis/,
|
||||
/"kind": "button",? \/\/ Discrete binding input source kind\. When kind is "axis", set both axisIndex and direction\. Values: none \| button \| axis/,
|
||||
);
|
||||
assert.match(output, /"toggleLookup": \{\s*"kind": "button"/);
|
||||
assert.match(output, /"leftStickHorizontal": \{\s*"kind": "axis"/);
|
||||
|
||||
@@ -152,6 +152,13 @@ export function buildCoreConfigOptionRegistry(
|
||||
defaultValue: defaultConfig.controller.repeatIntervalMs,
|
||||
description: 'Repeat interval for held controller actions.',
|
||||
},
|
||||
{
|
||||
path: 'controller.buttonIndices',
|
||||
kind: 'object',
|
||||
defaultValue: defaultConfig.controller.buttonIndices,
|
||||
description:
|
||||
'Semantic button-name reference mapping used for legacy configs and debug output. Updating it does not rewrite existing raw binding descriptors.',
|
||||
},
|
||||
{
|
||||
path: 'controller.buttonIndices.select',
|
||||
kind: 'number',
|
||||
@@ -218,19 +225,27 @@ export function buildCoreConfigOptionRegistry(
|
||||
defaultValue: defaultConfig.controller.buttonIndices.rightTrigger,
|
||||
description: 'Raw button index used for controller R2 input.',
|
||||
},
|
||||
{
|
||||
path: 'controller.bindings',
|
||||
kind: 'object',
|
||||
defaultValue: defaultConfig.controller.bindings,
|
||||
description:
|
||||
'Raw controller binding descriptors saved by Alt+C learn mode. For discrete axis bindings, kind "axis" requires axisIndex and direction.',
|
||||
},
|
||||
...discreteBindings.flatMap((binding) => [
|
||||
{
|
||||
path: `controller.bindings.${binding.id}`,
|
||||
kind: 'object' as const,
|
||||
defaultValue: binding.defaultValue,
|
||||
description: `${binding.description} Use Alt+C learn mode or set a raw button/axis descriptor manually.`,
|
||||
description: `${binding.description} Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.`,
|
||||
},
|
||||
{
|
||||
path: `controller.bindings.${binding.id}.kind`,
|
||||
kind: 'enum' as const,
|
||||
enumValues: ['none', 'button', 'axis'],
|
||||
defaultValue: binding.defaultValue.kind,
|
||||
description: 'Discrete binding input source kind.',
|
||||
description:
|
||||
'Discrete binding input source kind. When kind is "axis", set both axisIndex and direction.',
|
||||
},
|
||||
{
|
||||
path: `controller.bindings.${binding.id}.buttonIndex`,
|
||||
@@ -249,8 +264,10 @@ export function buildCoreConfigOptionRegistry(
|
||||
path: `controller.bindings.${binding.id}.direction`,
|
||||
kind: 'enum' as const,
|
||||
enumValues: ['negative', 'positive'],
|
||||
defaultValue: binding.defaultValue.kind === 'axis' ? binding.defaultValue.direction : undefined,
|
||||
description: 'Axis direction captured for this discrete controller action.',
|
||||
defaultValue:
|
||||
binding.defaultValue.kind === 'axis' ? binding.defaultValue.direction : undefined,
|
||||
description:
|
||||
'Axis direction captured for this discrete controller action. Required when kind is "axis".',
|
||||
},
|
||||
]),
|
||||
...axisBindings.flatMap((binding) => [
|
||||
|
||||
@@ -421,7 +421,7 @@ export function applyCoreDomainConfig(context: ResolveContext): void {
|
||||
`controller.bindings.${key}`,
|
||||
bindingValue,
|
||||
resolved.controller.bindings[key],
|
||||
"Expected legacy controller axis name or binding object with kind 'axis'.",
|
||||
"Expected legacy controller axis name ('none' allowed) or binding object with kind 'axis'.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
dialog,
|
||||
screen,
|
||||
} from 'electron';
|
||||
import { applyControllerConfigUpdate } from './main/controller-config-update.js';
|
||||
|
||||
function getPasswordStoreArg(argv: string[]): string | null {
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
@@ -3457,8 +3458,9 @@ const { registerIpcRuntimeHandlers } = composeIpcRuntimeHandlers({
|
||||
getConfiguredShortcuts: () => getConfiguredShortcuts(),
|
||||
getControllerConfig: () => getResolvedConfig().controller,
|
||||
saveControllerConfig: (update) => {
|
||||
const currentRawConfig = configService.getRawConfig();
|
||||
configService.patchRawConfig({
|
||||
controller: update,
|
||||
controller: applyControllerConfigUpdate(currentRawConfig.controller, update),
|
||||
});
|
||||
},
|
||||
saveControllerPreference: ({ preferredGamepadId, preferredGamepadLabel }) => {
|
||||
|
||||
54
src/main/controller-config-update.test.ts
Normal file
54
src/main/controller-config-update.test.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { applyControllerConfigUpdate } from './controller-config-update.js';
|
||||
|
||||
test('applyControllerConfigUpdate replaces binding descriptors instead of deep-merging them', () => {
|
||||
const next = applyControllerConfigUpdate(
|
||||
{
|
||||
preferredGamepadId: 'pad-1',
|
||||
bindings: {
|
||||
toggleLookup: { kind: 'axis', axisIndex: 4, direction: 'positive' },
|
||||
closeLookup: { kind: 'button', buttonIndex: 1 },
|
||||
},
|
||||
},
|
||||
{
|
||||
bindings: {
|
||||
toggleLookup: { kind: 'button', buttonIndex: 11 },
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
assert.deepEqual(next.bindings?.toggleLookup, { kind: 'button', buttonIndex: 11 });
|
||||
assert.deepEqual(next.bindings?.closeLookup, { kind: 'button', buttonIndex: 1 });
|
||||
});
|
||||
|
||||
test('applyControllerConfigUpdate merges buttonIndices while replacing only updated binding leaves', () => {
|
||||
const next = applyControllerConfigUpdate(
|
||||
{
|
||||
buttonIndices: {
|
||||
select: 6,
|
||||
buttonSouth: 0,
|
||||
},
|
||||
bindings: {
|
||||
toggleLookup: { kind: 'button', buttonIndex: 0 },
|
||||
closeLookup: { kind: 'button', buttonIndex: 1 },
|
||||
},
|
||||
},
|
||||
{
|
||||
buttonIndices: {
|
||||
buttonSouth: 9,
|
||||
},
|
||||
bindings: {
|
||||
closeLookup: { kind: 'none' },
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
assert.deepEqual(next.buttonIndices, {
|
||||
select: 6,
|
||||
buttonSouth: 9,
|
||||
});
|
||||
assert.deepEqual(next.bindings?.toggleLookup, { kind: 'button', buttonIndex: 0 });
|
||||
assert.deepEqual(next.bindings?.closeLookup, { kind: 'none' });
|
||||
});
|
||||
38
src/main/controller-config-update.ts
Normal file
38
src/main/controller-config-update.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { ControllerConfigUpdate, RawConfig } from '../types';
|
||||
|
||||
type RawControllerConfig = NonNullable<RawConfig['controller']>;
|
||||
type RawControllerBindings = NonNullable<RawControllerConfig['bindings']>;
|
||||
|
||||
export function applyControllerConfigUpdate(
|
||||
currentController: RawConfig['controller'] | undefined,
|
||||
update: ControllerConfigUpdate,
|
||||
): RawControllerConfig {
|
||||
const nextController: RawControllerConfig = {
|
||||
...(currentController ?? {}),
|
||||
...update,
|
||||
};
|
||||
|
||||
if (currentController?.buttonIndices || update.buttonIndices) {
|
||||
nextController.buttonIndices = {
|
||||
...(currentController?.buttonIndices ?? {}),
|
||||
...(update.buttonIndices ?? {}),
|
||||
};
|
||||
}
|
||||
|
||||
if (currentController?.bindings || update.bindings) {
|
||||
const nextBindings: RawControllerBindings = {
|
||||
...(currentController?.bindings ?? {}),
|
||||
};
|
||||
|
||||
for (const [key, value] of Object.entries(update.bindings ?? {}) as Array<
|
||||
[keyof RawControllerBindings, RawControllerBindings[keyof RawControllerBindings] | undefined]
|
||||
>) {
|
||||
if (value === undefined) continue;
|
||||
(nextBindings as Record<string, unknown>)[key] = JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
|
||||
nextController.bindings = nextBindings;
|
||||
}
|
||||
|
||||
return nextController;
|
||||
}
|
||||
@@ -96,3 +96,34 @@ test('controller binding capture emits axis binding for continuous learn mode',
|
||||
binding: { kind: 'axis', axisIndex: 3, dpadFallback: 'horizontal' },
|
||||
});
|
||||
});
|
||||
|
||||
test('controller binding capture ignores button presses for continuous learn mode', () => {
|
||||
const capture = createControllerBindingCapture({
|
||||
triggerDeadzone: 0.5,
|
||||
stickDeadzone: 0.2,
|
||||
});
|
||||
|
||||
capture.arm(
|
||||
{
|
||||
actionId: 'leftStickHorizontal',
|
||||
bindingType: 'axis',
|
||||
dpadFallback: 'horizontal',
|
||||
},
|
||||
createSnapshot(),
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
capture.poll(
|
||||
createSnapshot({
|
||||
buttons: [{ value: 1, pressed: true, touched: true }],
|
||||
}),
|
||||
),
|
||||
null,
|
||||
);
|
||||
|
||||
assert.deepEqual(capture.poll(createSnapshot({ axes: [0, 0, 0.75, 0, 0] })), {
|
||||
actionId: 'leftStickHorizontal',
|
||||
bindingType: 'axis',
|
||||
binding: { kind: 'axis', axisIndex: 2, dpadFallback: 'horizontal' },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -112,22 +112,12 @@ export function createControllerBindingCapture(options: {
|
||||
for (let index = 0; index < snapshot.buttons.length; index += 1) {
|
||||
if (!isActiveButton(snapshot.buttons[index], options.triggerDeadzone)) continue;
|
||||
if (blockedButtons.has(index)) continue;
|
||||
if (target.bindingType === 'axis') continue;
|
||||
|
||||
const result: ControllerBindingCaptureResult =
|
||||
target.bindingType === 'discrete'
|
||||
? {
|
||||
const result: ControllerBindingCaptureResult = {
|
||||
actionId: target.actionId,
|
||||
bindingType: 'discrete',
|
||||
binding: { kind: 'button', buttonIndex: index },
|
||||
}
|
||||
: {
|
||||
actionId: target.actionId,
|
||||
bindingType: 'axis',
|
||||
binding: {
|
||||
kind: 'axis',
|
||||
axisIndex: index,
|
||||
dpadFallback: target.dpadFallback,
|
||||
},
|
||||
};
|
||||
cancel();
|
||||
return result;
|
||||
|
||||
@@ -257,6 +257,82 @@ test('gamepad controller allows keyboard-mode toggle while other actions stay ga
|
||||
assert.deepEqual(calls, ['toggle-keyboard-mode']);
|
||||
});
|
||||
|
||||
test('gamepad controller re-evaluates interaction gating after toggling keyboard mode', () => {
|
||||
const calls: string[] = [];
|
||||
let keyboardModeEnabled = true;
|
||||
const buttons = Array.from({ length: 8 }, () => ({ value: 0, pressed: false, touched: false }));
|
||||
buttons[0] = { value: 1, pressed: true, touched: true };
|
||||
buttons[3] = { value: 1, pressed: true, touched: true };
|
||||
|
||||
const controller = createGamepadController({
|
||||
getGamepads: () => [createGamepad('pad-1', { buttons })],
|
||||
getConfig: () => createControllerConfig(),
|
||||
getKeyboardModeEnabled: () => keyboardModeEnabled,
|
||||
getLookupWindowOpen: () => false,
|
||||
getInteractionBlocked: () => false,
|
||||
toggleKeyboardMode: () => {
|
||||
calls.push('toggle-keyboard-mode');
|
||||
keyboardModeEnabled = false;
|
||||
},
|
||||
toggleLookup: () => calls.push('toggle-lookup'),
|
||||
closeLookup: () => {},
|
||||
moveSelection: () => {},
|
||||
mineCard: () => {},
|
||||
quitMpv: () => {},
|
||||
previousAudio: () => {},
|
||||
nextAudio: () => {},
|
||||
playCurrentAudio: () => {},
|
||||
toggleMpvPause: () => {},
|
||||
scrollPopup: () => {},
|
||||
jumpPopup: () => {},
|
||||
onState: () => {},
|
||||
});
|
||||
|
||||
controller.poll(0);
|
||||
|
||||
assert.deepEqual(calls, ['toggle-keyboard-mode']);
|
||||
});
|
||||
|
||||
test('gamepad controller resets edge state when active controller changes', () => {
|
||||
const calls: string[] = [];
|
||||
let currentGamepads = [
|
||||
createGamepad('pad-1', {
|
||||
buttons: [{ value: 1, pressed: true, touched: true }],
|
||||
}),
|
||||
];
|
||||
|
||||
const controller = createGamepadController({
|
||||
getGamepads: () => currentGamepads,
|
||||
getConfig: () => createControllerConfig(),
|
||||
getKeyboardModeEnabled: () => true,
|
||||
getLookupWindowOpen: () => false,
|
||||
getInteractionBlocked: () => false,
|
||||
toggleKeyboardMode: () => {},
|
||||
toggleLookup: () => calls.push('toggle-lookup'),
|
||||
closeLookup: () => {},
|
||||
moveSelection: () => {},
|
||||
mineCard: () => {},
|
||||
quitMpv: () => {},
|
||||
previousAudio: () => {},
|
||||
nextAudio: () => {},
|
||||
playCurrentAudio: () => {},
|
||||
toggleMpvPause: () => {},
|
||||
scrollPopup: () => {},
|
||||
jumpPopup: () => {},
|
||||
onState: () => {},
|
||||
});
|
||||
|
||||
controller.poll(0);
|
||||
currentGamepads = [
|
||||
createGamepad('pad-2', {
|
||||
buttons: [{ value: 1, pressed: true, touched: true }],
|
||||
}),
|
||||
];
|
||||
controller.poll(50);
|
||||
|
||||
assert.deepEqual(calls, ['toggle-lookup', 'toggle-lookup']);
|
||||
});
|
||||
|
||||
test('gamepad controller does not toggle keyboard mode when controller support is disabled', () => {
|
||||
const calls: string[] = [];
|
||||
const buttons = Array.from({ length: 8 }, () => ({ value: 0, pressed: false, touched: false }));
|
||||
|
||||
@@ -52,8 +52,7 @@ function createFakeElement() {
|
||||
}
|
||||
|
||||
test('controller config form renders rows and dispatches learn clear reset callbacks', () => {
|
||||
const globals = globalThis as typeof globalThis & { document?: unknown };
|
||||
const previousDocument = globals.document;
|
||||
const previousDocumentDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'document');
|
||||
Object.defineProperty(globalThis, 'document', {
|
||||
configurable: true,
|
||||
value: {
|
||||
@@ -83,7 +82,7 @@ test('controller config form renders rows and dispatches learn clear reset callb
|
||||
rightStickVertical: { kind: 'axis', axisIndex: 4, dpadFallback: 'none' },
|
||||
}) as never,
|
||||
getLearningActionId: () => 'toggleLookup',
|
||||
onLearn: (actionId) => calls.push(`learn:${actionId}`),
|
||||
onLearn: (actionId, bindingType) => calls.push(`learn:${actionId}:${bindingType}`),
|
||||
onClear: (actionId) => calls.push(`clear:${actionId}`),
|
||||
onReset: (actionId) => calls.push(`reset:${actionId}`),
|
||||
});
|
||||
@@ -97,13 +96,20 @@ test('controller config form renders rows and dispatches learn clear reset callb
|
||||
firstRow.children[2].children[0].dispatch('click');
|
||||
firstRow.children[2].children[1].dispatch('click');
|
||||
firstRow.children[2].children[2].dispatch('click');
|
||||
const firstAxisRow = container.children[13];
|
||||
firstAxisRow.children[2].children[0].dispatch('click');
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
'learn:toggleLookup',
|
||||
'learn:toggleLookup:discrete',
|
||||
'clear:toggleLookup',
|
||||
'reset:toggleLookup',
|
||||
'learn:leftStickHorizontal:axis',
|
||||
]);
|
||||
} finally {
|
||||
Object.defineProperty(globalThis, 'document', { configurable: true, value: previousDocument });
|
||||
if (previousDocumentDescriptor) {
|
||||
Object.defineProperty(globalThis, 'document', previousDocumentDescriptor);
|
||||
} else {
|
||||
Reflect.deleteProperty(globalThis, 'document');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -29,7 +29,10 @@ export function createControllerSelectModal(
|
||||
let lastRenderedDevicesKey = '';
|
||||
let lastRenderedActiveGamepadId: string | null = null;
|
||||
let lastRenderedPreferredId = '';
|
||||
let learningActionId: keyof NonNullable<typeof ctx.state.controllerConfig>['bindings'] | null = null;
|
||||
type ControllerBindingKey = keyof NonNullable<typeof ctx.state.controllerConfig>['bindings'];
|
||||
type ControllerBindingValue =
|
||||
NonNullable<NonNullable<typeof ctx.state.controllerConfig>['bindings']>[ControllerBindingKey];
|
||||
let learningActionId: ControllerBindingKey | null = null;
|
||||
let bindingCapture: ReturnType<typeof createControllerBindingCapture> | null = null;
|
||||
|
||||
const controllerConfigForm = createControllerConfigForm({
|
||||
@@ -178,8 +181,8 @@ export function createControllerSelectModal(
|
||||
}
|
||||
|
||||
async function saveBinding(
|
||||
actionId: keyof NonNullable<typeof ctx.state.controllerConfig>['bindings'],
|
||||
binding: NonNullable<NonNullable<typeof ctx.state.controllerConfig>['bindings']>[typeof actionId],
|
||||
actionId: ControllerBindingKey,
|
||||
binding: ControllerBindingValue,
|
||||
): Promise<void> {
|
||||
const definition = getControllerBindingDefinition(actionId);
|
||||
try {
|
||||
@@ -242,7 +245,7 @@ export function createControllerSelectModal(
|
||||
buttons: ctx.state.controllerRawButtons,
|
||||
});
|
||||
if (result) {
|
||||
void saveBinding(result.actionId as keyof NonNullable<typeof ctx.state.controllerConfig>['bindings'], result.binding as never);
|
||||
void saveBinding(result.actionId as ControllerBindingKey, result.binding as ControllerBindingValue);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user