feat(controller): add inline remap modal with descriptor-based bindings (#21)

This commit is contained in:
2026-03-15 15:55:45 -07:00
committed by GitHub
parent 9eed37420e
commit 478869ff28
38 changed files with 3136 additions and 1431 deletions

View File

@@ -1168,12 +1168,103 @@ test('parses controller settings with logical bindings and tuning knobs', () =>
assert.equal(config.controller.repeatIntervalMs, 70);
assert.equal(config.controller.buttonIndices.select, 6);
assert.equal(config.controller.buttonIndices.leftStickPress, 9);
assert.equal(config.controller.bindings.toggleLookup, 'buttonWest');
assert.equal(config.controller.bindings.quitMpv, 'select');
assert.equal(config.controller.bindings.playCurrentAudio, 'none');
assert.equal(config.controller.bindings.toggleMpvPause, 'leftStickPress');
assert.equal(config.controller.bindings.leftStickHorizontal, 'rightStickX');
assert.equal(config.controller.bindings.rightStickVertical, 'leftStickY');
assert.deepEqual(config.controller.bindings.toggleLookup, { kind: 'button', buttonIndex: 2 });
assert.deepEqual(config.controller.bindings.quitMpv, { kind: 'button', buttonIndex: 6 });
assert.deepEqual(config.controller.bindings.playCurrentAudio, { kind: 'none' });
assert.deepEqual(config.controller.bindings.toggleMpvPause, { kind: 'button', buttonIndex: 9 });
assert.deepEqual(config.controller.bindings.leftStickHorizontal, {
kind: 'axis',
axisIndex: 3,
dpadFallback: 'horizontal',
});
assert.deepEqual(config.controller.bindings.rightStickVertical, {
kind: 'axis',
axisIndex: 1,
dpadFallback: 'none',
});
});
test('parses descriptor-based controller bindings', () => {
const dir = makeTempDir();
fs.writeFileSync(
path.join(dir, 'config.jsonc'),
`{
"controller": {
"bindings": {
"toggleLookup": { "kind": "button", "buttonIndex": 11 },
"closeLookup": { "kind": "axis", "axisIndex": 4, "direction": "negative" },
"playCurrentAudio": { "kind": "none" },
"leftStickHorizontal": { "kind": "axis", "axisIndex": 7, "dpadFallback": "none" },
"leftStickVertical": { "kind": "axis", "axisIndex": 2, "dpadFallback": "vertical" }
}
}
}`,
'utf-8',
);
const service = new ConfigService(dir);
const config = service.getConfig();
assert.deepEqual(config.controller.bindings.toggleLookup, {
kind: 'button',
buttonIndex: 11,
});
assert.deepEqual(config.controller.bindings.closeLookup, {
kind: 'axis',
axisIndex: 4,
direction: 'negative',
});
assert.deepEqual(config.controller.bindings.playCurrentAudio, { kind: 'none' });
assert.deepEqual(config.controller.bindings.leftStickHorizontal, {
kind: 'axis',
axisIndex: 7,
dpadFallback: 'none',
});
assert.deepEqual(config.controller.bindings.leftStickVertical, {
kind: 'axis',
axisIndex: 2,
dpadFallback: 'vertical',
});
});
test('controller descriptor config rejects malformed binding objects', () => {
const dir = makeTempDir();
fs.writeFileSync(
path.join(dir, 'config.jsonc'),
`{
"controller": {
"bindings": {
"toggleLookup": { "kind": "button", "buttonIndex": -1 },
"closeLookup": { "kind": "axis", "axisIndex": 1, "direction": "sideways" },
"leftStickHorizontal": { "kind": "axis", "axisIndex": 0, "dpadFallback": "diagonal" }
}
}
}`,
'utf-8',
);
const service = new ConfigService(dir);
const config = service.getConfig();
const warnings = service.getWarnings();
assert.deepEqual(
config.controller.bindings.toggleLookup,
DEFAULT_CONFIG.controller.bindings.toggleLookup,
);
assert.deepEqual(
config.controller.bindings.closeLookup,
DEFAULT_CONFIG.controller.bindings.closeLookup,
);
assert.deepEqual(
config.controller.bindings.leftStickHorizontal,
DEFAULT_CONFIG.controller.bindings.leftStickHorizontal,
);
assert.equal(warnings.some((warning) => warning.path === 'controller.bindings.toggleLookup'), true);
assert.equal(warnings.some((warning) => warning.path === 'controller.bindings.closeLookup'), true);
assert.equal(
warnings.some((warning) => warning.path === 'controller.bindings.leftStickHorizontal'),
true,
);
});
test('controller positive-number tuning rejects sub-unit values that floor to zero', () => {
@@ -1825,6 +1916,24 @@ test('template generator includes known keys', () => {
output,
/"triggerInputMode": "auto",? \/\/ How controller triggers are interpreted: auto, pressed-only, or thresholded analog\. Values: auto \| digital \| analog/,
);
assert.match(
output,
/"preferredGamepadId": "",? \/\/ Preferred controller id saved from the controller config modal\./,
);
assert.match(
output,
/"toggleLookup": \{\s*"kind": "button"[\s\S]*\},? \/\/ Controller binding descriptor for toggling lookup\. Use Alt\+C learn mode or set a raw button\/axis descriptor manually\./,
);
assert.match(
output,
/"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"/);
assert.match(
output,
/"dpadFallback": "horizontal",? \/\/ Optional D-pad fallback used when this analog controller action should also read D-pad input\. Values: none \| horizontal \| vertical/,
);
assert.match(output, /"port": 6678,? \/\/ Annotated subtitle websocket server port\./);
assert.match(
output,

View File

@@ -58,19 +58,19 @@ export const CORE_DEFAULT_CONFIG: Pick<
rightTrigger: 7,
},
bindings: {
toggleLookup: 'buttonSouth',
closeLookup: 'buttonEast',
toggleKeyboardOnlyMode: 'buttonNorth',
mineCard: 'buttonWest',
quitMpv: 'select',
previousAudio: 'none',
nextAudio: 'rightShoulder',
playCurrentAudio: 'leftShoulder',
toggleMpvPause: 'leftStickPress',
leftStickHorizontal: 'leftStickX',
leftStickVertical: 'leftStickY',
rightStickHorizontal: 'rightStickX',
rightStickVertical: 'rightStickY',
toggleLookup: { kind: 'button', buttonIndex: 0 },
closeLookup: { kind: 'button', buttonIndex: 1 },
toggleKeyboardOnlyMode: { kind: 'button', buttonIndex: 3 },
mineCard: { kind: 'button', buttonIndex: 2 },
quitMpv: { kind: 'button', buttonIndex: 6 },
previousAudio: { kind: 'none' },
nextAudio: { kind: 'button', buttonIndex: 5 },
playCurrentAudio: { kind: 'button', buttonIndex: 4 },
toggleMpvPause: { kind: 'button', buttonIndex: 9 },
leftStickHorizontal: { kind: 'axis', axisIndex: 0, dpadFallback: 'horizontal' },
leftStickVertical: { kind: 'axis', axisIndex: 1, dpadFallback: 'vertical' },
rightStickHorizontal: { kind: 'axis', axisIndex: 3, dpadFallback: 'none' },
rightStickVertical: { kind: 'axis', axisIndex: 4, dpadFallback: 'none' },
},
},
shortcuts: {

View File

@@ -4,20 +4,76 @@ import { ConfigOptionRegistryEntry } from './shared';
export function buildCoreConfigOptionRegistry(
defaultConfig: ResolvedConfig,
): ConfigOptionRegistryEntry[] {
const controllerButtonEnumValues = [
'none',
'select',
'buttonSouth',
'buttonEast',
'buttonNorth',
'buttonWest',
'leftShoulder',
'rightShoulder',
'leftStickPress',
'rightStickPress',
'leftTrigger',
'rightTrigger',
];
const discreteBindings = [
{
id: 'toggleLookup',
defaultValue: defaultConfig.controller.bindings.toggleLookup,
description: 'Controller binding descriptor for toggling lookup.',
},
{
id: 'closeLookup',
defaultValue: defaultConfig.controller.bindings.closeLookup,
description: 'Controller binding descriptor for closing lookup.',
},
{
id: 'toggleKeyboardOnlyMode',
defaultValue: defaultConfig.controller.bindings.toggleKeyboardOnlyMode,
description: 'Controller binding descriptor for toggling keyboard-only mode.',
},
{
id: 'mineCard',
defaultValue: defaultConfig.controller.bindings.mineCard,
description: 'Controller binding descriptor for mining the active card.',
},
{
id: 'quitMpv',
defaultValue: defaultConfig.controller.bindings.quitMpv,
description: 'Controller binding descriptor for quitting mpv.',
},
{
id: 'previousAudio',
defaultValue: defaultConfig.controller.bindings.previousAudio,
description: 'Controller binding descriptor for previous Yomitan audio.',
},
{
id: 'nextAudio',
defaultValue: defaultConfig.controller.bindings.nextAudio,
description: 'Controller binding descriptor for next Yomitan audio.',
},
{
id: 'playCurrentAudio',
defaultValue: defaultConfig.controller.bindings.playCurrentAudio,
description: 'Controller binding descriptor for playing the current Yomitan audio.',
},
{
id: 'toggleMpvPause',
defaultValue: defaultConfig.controller.bindings.toggleMpvPause,
description: 'Controller binding descriptor for toggling mpv play/pause.',
},
] as const;
const axisBindings = [
{
id: 'leftStickHorizontal',
defaultValue: defaultConfig.controller.bindings.leftStickHorizontal,
description: 'Axis binding descriptor used for left/right token selection.',
},
{
id: 'leftStickVertical',
defaultValue: defaultConfig.controller.bindings.leftStickVertical,
description: 'Axis binding descriptor used for primary popup scrolling.',
},
{
id: 'rightStickHorizontal',
defaultValue: defaultConfig.controller.bindings.rightStickHorizontal,
description: 'Axis binding descriptor reserved for alternate right-stick mappings.',
},
{
id: 'rightStickVertical',
defaultValue: defaultConfig.controller.bindings.rightStickVertical,
description: 'Axis binding descriptor used for popup page jumps.',
},
] as const;
return [
{
@@ -37,7 +93,7 @@ export function buildCoreConfigOptionRegistry(
path: 'controller.preferredGamepadId',
kind: 'string',
defaultValue: defaultConfig.controller.preferredGamepadId,
description: 'Preferred controller id saved from the controller selection modal.',
description: 'Preferred controller id saved from the controller config modal.',
},
{
path: 'controller.preferredGamepadLabel',
@@ -96,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',
@@ -163,96 +226,79 @@ export function buildCoreConfigOptionRegistry(
description: 'Raw button index used for controller R2 input.',
},
{
path: 'controller.bindings.toggleLookup',
kind: 'enum',
enumValues: controllerButtonEnumValues,
defaultValue: defaultConfig.controller.bindings.toggleLookup,
description: 'Controller binding for toggling lookup.',
},
{
path: 'controller.bindings.closeLookup',
kind: 'enum',
enumValues: controllerButtonEnumValues,
defaultValue: defaultConfig.controller.bindings.closeLookup,
description: 'Controller binding for closing lookup.',
},
{
path: 'controller.bindings.toggleKeyboardOnlyMode',
kind: 'enum',
enumValues: controllerButtonEnumValues,
defaultValue: defaultConfig.controller.bindings.toggleKeyboardOnlyMode,
description: 'Controller binding for toggling keyboard-only mode.',
},
{
path: 'controller.bindings.mineCard',
kind: 'enum',
enumValues: controllerButtonEnumValues,
defaultValue: defaultConfig.controller.bindings.mineCard,
description: 'Controller binding for mining the active card.',
},
{
path: 'controller.bindings.quitMpv',
kind: 'enum',
enumValues: controllerButtonEnumValues,
defaultValue: defaultConfig.controller.bindings.quitMpv,
description: 'Controller binding for quitting mpv.',
},
{
path: 'controller.bindings.previousAudio',
kind: 'enum',
enumValues: controllerButtonEnumValues,
defaultValue: defaultConfig.controller.bindings.previousAudio,
description: 'Controller binding for previous Yomitan audio.',
},
{
path: 'controller.bindings.nextAudio',
kind: 'enum',
enumValues: controllerButtonEnumValues,
defaultValue: defaultConfig.controller.bindings.nextAudio,
description: 'Controller binding for next Yomitan audio.',
},
{
path: 'controller.bindings.playCurrentAudio',
kind: 'enum',
enumValues: controllerButtonEnumValues,
defaultValue: defaultConfig.controller.bindings.playCurrentAudio,
description: 'Controller binding for playing the current Yomitan audio.',
},
{
path: 'controller.bindings.toggleMpvPause',
kind: 'enum',
enumValues: controllerButtonEnumValues,
defaultValue: defaultConfig.controller.bindings.toggleMpvPause,
description: 'Controller binding for toggling mpv play/pause.',
},
{
path: 'controller.bindings.leftStickHorizontal',
kind: 'enum',
enumValues: ['leftStickX', 'leftStickY', 'rightStickX', 'rightStickY'],
defaultValue: defaultConfig.controller.bindings.leftStickHorizontal,
description: 'Axis binding used for left/right token selection.',
},
{
path: 'controller.bindings.leftStickVertical',
kind: 'enum',
enumValues: ['leftStickX', 'leftStickY', 'rightStickX', 'rightStickY'],
defaultValue: defaultConfig.controller.bindings.leftStickVertical,
description: 'Axis binding used for primary popup scrolling.',
},
{
path: 'controller.bindings.rightStickHorizontal',
kind: 'enum',
enumValues: ['leftStickX', 'leftStickY', 'rightStickX', 'rightStickY'],
defaultValue: defaultConfig.controller.bindings.rightStickHorizontal,
description: 'Axis binding reserved for alternate right-stick mappings.',
},
{
path: 'controller.bindings.rightStickVertical',
kind: 'enum',
enumValues: ['leftStickX', 'leftStickY', 'rightStickX', 'rightStickY'],
defaultValue: defaultConfig.controller.bindings.rightStickVertical,
description: 'Axis binding used for popup page jumps.',
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. 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. When kind is "axis", set both axisIndex and direction.',
},
{
path: `controller.bindings.${binding.id}.buttonIndex`,
kind: 'number' as const,
defaultValue:
binding.defaultValue.kind === 'button' ? binding.defaultValue.buttonIndex : undefined,
description: 'Raw button index captured for this discrete controller action.',
},
{
path: `controller.bindings.${binding.id}.axisIndex`,
kind: 'number' as const,
defaultValue: binding.defaultValue.kind === 'axis' ? binding.defaultValue.axisIndex : undefined,
description: 'Raw axis index captured for this discrete controller action.',
},
{
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. Required when kind is "axis".',
},
]),
...axisBindings.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 axis descriptor manually.`,
},
{
path: `controller.bindings.${binding.id}.kind`,
kind: 'enum' as const,
enumValues: ['none', 'axis'],
defaultValue: binding.defaultValue.kind,
description: 'Analog binding input source kind.',
},
{
path: `controller.bindings.${binding.id}.axisIndex`,
kind: 'number' as const,
defaultValue: binding.defaultValue.kind === 'axis' ? binding.defaultValue.axisIndex : undefined,
description: 'Raw axis index captured for this analog controller action.',
},
{
path: `controller.bindings.${binding.id}.dpadFallback`,
kind: 'enum' as const,
enumValues: ['none', 'horizontal', 'vertical'],
defaultValue:
binding.defaultValue.kind === 'axis' ? binding.defaultValue.dpadFallback : undefined,
description: 'Optional D-pad fallback used when this analog controller action should also read D-pad input.',
},
]),
{
path: 'texthooker.launchAtStartup',
kind: 'boolean',

View File

@@ -38,7 +38,7 @@ const CORE_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
title: 'Controller Support',
description: [
'Gamepad support for the visible overlay while keyboard-only mode is active.',
'Use the selection modal to save a preferred controller by id for future launches.',
'Use Alt+C to pick a preferred controller and remap actions inline with learn mode.',
'Trigger input mode can be auto, digital-only, or analog-thresholded depending on the controller.',
'Override controller.buttonIndices when your pad reports non-standard raw button numbers.',
],

View File

@@ -1,28 +1,141 @@
import type {
ControllerAxisBinding,
ControllerAxisBindingConfig,
ControllerAxisDirection,
ControllerButtonBinding,
ControllerButtonIndicesConfig,
ControllerDpadFallback,
ControllerDiscreteBindingConfig,
ResolvedControllerAxisBinding,
ResolvedControllerDiscreteBinding,
} from '../../types';
import { ResolveContext } from './context';
import { asBoolean, asNumber, asString, isObject } from './shared';
const CONTROLLER_BUTTON_BINDINGS = [
'none',
'select',
'buttonSouth',
'buttonEast',
'buttonNorth',
'buttonWest',
'leftShoulder',
'rightShoulder',
'leftStickPress',
'rightStickPress',
'leftTrigger',
'rightTrigger',
] as const;
const CONTROLLER_AXIS_BINDINGS = ['leftStickX', 'leftStickY', 'rightStickX', 'rightStickY'] as const;
const CONTROLLER_AXIS_INDEX_BY_BINDING: Record<ControllerAxisBinding, number> = {
leftStickX: 0,
leftStickY: 1,
rightStickX: 3,
rightStickY: 4,
};
const CONTROLLER_BUTTON_INDEX_KEY_BY_BINDING: Record<
Exclude<ControllerButtonBinding, 'none'>,
keyof Required<ControllerButtonIndicesConfig>
> = {
select: 'select',
buttonSouth: 'buttonSouth',
buttonEast: 'buttonEast',
buttonNorth: 'buttonNorth',
buttonWest: 'buttonWest',
leftShoulder: 'leftShoulder',
rightShoulder: 'rightShoulder',
leftStickPress: 'leftStickPress',
rightStickPress: 'rightStickPress',
leftTrigger: 'leftTrigger',
rightTrigger: 'rightTrigger',
};
const CONTROLLER_AXIS_FALLBACK_BY_SLOT = {
leftStickHorizontal: 'horizontal',
leftStickVertical: 'vertical',
rightStickHorizontal: 'none',
rightStickVertical: 'none',
} as const satisfies Record<string, ControllerDpadFallback>;
function isControllerAxisDirection(value: unknown): value is ControllerAxisDirection {
return value === 'negative' || value === 'positive';
}
function isControllerDpadFallback(value: unknown): value is ControllerDpadFallback {
return value === 'none' || value === 'horizontal' || value === 'vertical';
}
function resolveLegacyDiscreteBinding(
value: ControllerButtonBinding,
buttonIndices: Required<ControllerButtonIndicesConfig>,
): ResolvedControllerDiscreteBinding {
if (value === 'none') {
return { kind: 'none' };
}
return {
kind: 'button',
buttonIndex: buttonIndices[CONTROLLER_BUTTON_INDEX_KEY_BY_BINDING[value]],
};
}
function resolveLegacyAxisBinding(
value: ControllerAxisBinding,
slot: keyof typeof CONTROLLER_AXIS_FALLBACK_BY_SLOT,
): ResolvedControllerAxisBinding {
return {
kind: 'axis',
axisIndex: CONTROLLER_AXIS_INDEX_BY_BINDING[value],
dpadFallback: CONTROLLER_AXIS_FALLBACK_BY_SLOT[slot],
};
}
function parseDiscreteBindingObject(value: unknown): ResolvedControllerDiscreteBinding | null {
if (!isObject(value) || typeof value.kind !== 'string') return null;
if (value.kind === 'none') {
return { kind: 'none' };
}
if (value.kind === 'button') {
return typeof value.buttonIndex === 'number' && Number.isInteger(value.buttonIndex) && value.buttonIndex >= 0
? { kind: 'button', buttonIndex: value.buttonIndex }
: null;
}
if (value.kind === 'axis') {
return typeof value.axisIndex === 'number' &&
Number.isInteger(value.axisIndex) &&
value.axisIndex >= 0 &&
isControllerAxisDirection(value.direction)
? { kind: 'axis', axisIndex: value.axisIndex, direction: value.direction }
: null;
}
return null;
}
function parseAxisBindingObject(
value: unknown,
slot: keyof typeof CONTROLLER_AXIS_FALLBACK_BY_SLOT,
): ResolvedControllerAxisBinding | null {
if (isObject(value) && value.kind === 'none') {
return { kind: 'none' };
}
if (!isObject(value) || value.kind !== 'axis') return null;
if (typeof value.axisIndex !== 'number' || !Number.isInteger(value.axisIndex) || value.axisIndex < 0) {
return null;
}
if (value.dpadFallback !== undefined && !isControllerDpadFallback(value.dpadFallback)) {
return null;
}
return {
kind: 'axis',
axisIndex: value.axisIndex,
dpadFallback: value.dpadFallback ?? CONTROLLER_AXIS_FALLBACK_BY_SLOT[slot],
};
}
export function applyCoreDomainConfig(context: ResolveContext): void {
const { src, resolved, warn } = context;
const controllerButtonBindings = [
'none',
'select',
'buttonSouth',
'buttonEast',
'buttonNorth',
'buttonWest',
'leftShoulder',
'rightShoulder',
'leftStickPress',
'rightStickPress',
'leftTrigger',
'rightTrigger',
] as const;
const controllerAxisBindings = [
'leftStickX',
'leftStickY',
'rightStickX',
'rightStickY',
] as const;
if (isObject(src.texthooker)) {
const launchAtStartup = asBoolean(src.texthooker.launchAtStartup);
@@ -251,19 +364,27 @@ export function applyCoreDomainConfig(context: ResolveContext): void {
] as const;
for (const key of buttonBindingKeys) {
const value = asString(src.controller.bindings[key]);
const bindingValue = src.controller.bindings[key];
const legacyValue = asString(bindingValue);
if (
value !== undefined &&
controllerButtonBindings.includes(value as (typeof controllerButtonBindings)[number])
legacyValue !== undefined &&
CONTROLLER_BUTTON_BINDINGS.includes(legacyValue as (typeof CONTROLLER_BUTTON_BINDINGS)[number])
) {
resolved.controller.bindings[key] =
value as (typeof resolved.controller.bindings)[typeof key];
} else if (src.controller.bindings[key] !== undefined) {
resolved.controller.bindings[key] = resolveLegacyDiscreteBinding(
legacyValue as ControllerButtonBinding,
resolved.controller.buttonIndices,
);
continue;
}
const parsedObject = parseDiscreteBindingObject(bindingValue);
if (parsedObject) {
resolved.controller.bindings[key] = parsedObject;
} else if (bindingValue !== undefined) {
warn(
`controller.bindings.${key}`,
src.controller.bindings[key],
bindingValue,
resolved.controller.bindings[key],
`Expected one of: ${controllerButtonBindings.join(', ')}.`,
"Expected legacy controller button name or binding object with kind 'none', 'button', or 'axis'.",
);
}
}
@@ -276,19 +397,31 @@ export function applyCoreDomainConfig(context: ResolveContext): void {
] as const;
for (const key of axisBindingKeys) {
const value = asString(src.controller.bindings[key]);
const bindingValue = src.controller.bindings[key];
const legacyValue = asString(bindingValue);
if (
value !== undefined &&
controllerAxisBindings.includes(value as (typeof controllerAxisBindings)[number])
legacyValue !== undefined &&
CONTROLLER_AXIS_BINDINGS.includes(legacyValue as (typeof CONTROLLER_AXIS_BINDINGS)[number])
) {
resolved.controller.bindings[key] =
value as (typeof resolved.controller.bindings)[typeof key];
} else if (src.controller.bindings[key] !== undefined) {
resolved.controller.bindings[key] = resolveLegacyAxisBinding(
legacyValue as ControllerAxisBinding,
key,
);
continue;
}
if (legacyValue === 'none') {
resolved.controller.bindings[key] = { kind: 'none' };
continue;
}
const parsedObject = parseAxisBindingObject(bindingValue, key);
if (parsedObject) {
resolved.controller.bindings[key] = parsedObject;
} else if (bindingValue !== undefined) {
warn(
`controller.bindings.${key}`,
src.controller.bindings[key],
bindingValue,
resolved.controller.bindings[key],
`Expected one of: ${controllerAxisBindings.join(', ')}.`,
"Expected legacy controller axis name ('none' allowed) or binding object with kind 'axis'.",
);
}
}