fix(controller): save remaps per profile, gate modals on enabled (#69)

This commit is contained in:
2026-05-16 20:43:27 -07:00
committed by GitHub
parent 49f89e6452
commit 5250ca8214
31 changed files with 1639 additions and 463 deletions
+64
View File
@@ -75,3 +75,67 @@ test('applyControllerConfigUpdate detaches updated binding values from the patch
assert.deepEqual(next.bindings?.toggleLookup, { kind: 'button', buttonIndex: 7 });
});
test('applyControllerConfigUpdate merges per-controller profile binding leaves', () => {
const next = applyControllerConfigUpdate(
{
profiles: {
'pad-1': {
label: 'Pad 1',
bindings: {
toggleLookup: { kind: 'button', buttonIndex: 0 },
closeLookup: { kind: 'button', buttonIndex: 1 },
},
},
},
},
{
profiles: {
'pad-1': {
bindings: {
toggleLookup: { kind: 'button', buttonIndex: 11 },
},
},
'pad-2': {
label: 'Pad 2',
bindings: {
mineCard: { kind: 'button', buttonIndex: 8 },
},
},
},
},
);
assert.deepEqual(next.profiles?.['pad-1']?.bindings?.toggleLookup, {
kind: 'button',
buttonIndex: 11,
});
assert.deepEqual(next.profiles?.['pad-1']?.bindings?.closeLookup, {
kind: 'button',
buttonIndex: 1,
});
assert.deepEqual(next.profiles?.['pad-2']?.bindings?.mineCard, {
kind: 'button',
buttonIndex: 8,
});
});
test('applyControllerConfigUpdate ignores reserved profile ids', () => {
const reservedProfiles = Object.create(null) as NonNullable<
Parameters<typeof applyControllerConfigUpdate>[1]['profiles']
>;
reservedProfiles.__proto__ = { label: 'polluted' };
reservedProfiles['constructor'] = { label: 'ctor' };
reservedProfiles['prototype'] = { label: 'proto' };
reservedProfiles['pad-1'] = { label: 'Pad 1' };
const next = applyControllerConfigUpdate(undefined, {
profiles: reservedProfiles,
});
assert.equal(Object.getPrototypeOf(next.profiles), Object.prototype);
assert.equal(Object.hasOwn(next.profiles ?? {}, '__proto__'), false);
assert.equal(Object.hasOwn(next.profiles ?? {}, 'constructor'), false);
assert.equal(Object.hasOwn(next.profiles ?? {}, 'prototype'), false);
assert.equal(next.profiles?.['pad-1']?.label, 'Pad 1');
});
+87 -15
View File
@@ -2,6 +2,66 @@ import type { ControllerConfigUpdate, RawConfig } from '../types';
type RawControllerConfig = NonNullable<RawConfig['controller']>;
type RawControllerBindings = NonNullable<RawControllerConfig['bindings']>;
type RawControllerButtonIndices = NonNullable<RawControllerConfig['buttonIndices']>;
type RawControllerProfiles = NonNullable<RawControllerConfig['profiles']>;
type RawControllerProfile = NonNullable<RawControllerProfiles[string]>;
const RESERVED_CONTROLLER_PROFILE_IDS = new Set(['__proto__', 'constructor', 'prototype']);
function mergeBindingPatch(
currentBindings: RawControllerBindings | undefined,
updateBindings: RawControllerBindings | undefined,
): RawControllerBindings | undefined {
if (!currentBindings && !updateBindings) return undefined;
const nextBindings: RawControllerBindings = {
...(currentBindings ?? {}),
};
for (const [key, value] of Object.entries(updateBindings ?? {}) as Array<
[keyof RawControllerBindings, RawControllerBindings[keyof RawControllerBindings] | undefined]
>) {
if (value === undefined) continue;
(nextBindings as Record<string, unknown>)[key] = structuredClone(value);
}
return nextBindings;
}
function mergeButtonIndexPatch(
currentButtonIndices: RawControllerButtonIndices | undefined,
updateButtonIndices: RawControllerButtonIndices | undefined,
): RawControllerButtonIndices | undefined {
if (!currentButtonIndices && !updateButtonIndices) return undefined;
return {
...(currentButtonIndices ?? {}),
...(updateButtonIndices ?? {}),
};
}
function mergeControllerProfile(
currentProfile: RawControllerProfile | undefined,
updateProfile: RawControllerProfile,
): RawControllerProfile {
const nextProfile: RawControllerProfile = {
...(currentProfile ?? {}),
...updateProfile,
};
const buttonIndices = mergeButtonIndexPatch(
currentProfile?.buttonIndices,
updateProfile.buttonIndices,
);
if (buttonIndices) {
nextProfile.buttonIndices = buttonIndices;
}
const bindings = mergeBindingPatch(currentProfile?.bindings, updateProfile.bindings);
if (bindings) {
nextProfile.bindings = bindings;
}
return nextProfile;
}
export function applyControllerConfigUpdate(
currentController: RawConfig['controller'] | undefined,
@@ -12,26 +72,38 @@ export function applyControllerConfigUpdate(
...update,
};
if (currentController?.buttonIndices || update.buttonIndices) {
nextController.buttonIndices = {
...(currentController?.buttonIndices ?? {}),
...(update.buttonIndices ?? {}),
};
const buttonIndices = mergeButtonIndexPatch(
currentController?.buttonIndices,
update.buttonIndices,
);
if (buttonIndices) {
nextController.buttonIndices = buttonIndices;
}
if (currentController?.bindings || update.bindings) {
const nextBindings: RawControllerBindings = {
...(currentController?.bindings ?? {}),
};
const bindings = mergeBindingPatch(currentController?.bindings, update.bindings);
if (bindings) {
nextController.bindings = bindings;
}
for (const [key, value] of Object.entries(update.bindings ?? {}) as Array<
[keyof RawControllerBindings, RawControllerBindings[keyof RawControllerBindings] | undefined]
if (currentController?.profiles || update.profiles) {
const nextProfiles: RawControllerProfiles = {};
for (const [profileId, profile] of Object.entries(currentController?.profiles ?? {}) as Array<
[string, RawControllerProfile]
>) {
if (value === undefined) continue;
(nextBindings as Record<string, unknown>)[key] = structuredClone(value);
if (RESERVED_CONTROLLER_PROFILE_IDS.has(profileId)) continue;
nextProfiles[profileId] = profile;
}
nextController.bindings = nextBindings;
for (const [profileId, profileUpdate] of Object.entries(update.profiles ?? {}) as Array<
[string, RawControllerProfile | undefined]
>) {
if (RESERVED_CONTROLLER_PROFILE_IDS.has(profileId)) continue;
if (profileUpdate === undefined) continue;
nextProfiles[profileId] = mergeControllerProfile(
currentController?.profiles?.[profileId],
profileUpdate,
);
}
nextController.profiles = nextProfiles;
}
return nextController;