diff --git a/fission/src/systems/analytics/AnalyticsSystem.ts b/fission/src/systems/analytics/AnalyticsSystem.ts index b9cc1c9152..9bf07940b0 100644 --- a/fission/src/systems/analytics/AnalyticsSystem.ts +++ b/fission/src/systems/analytics/AnalyticsSystem.ts @@ -74,6 +74,7 @@ export interface AnalyticsEvents { // Scheme Events "Scheme Applied": { isCustomized: boolean + schemeId: string schemeName: string } diff --git a/fission/src/systems/input/DefaultInputs.ts b/fission/src/systems/input/DefaultInputs.ts index 511cef68f2..84884bc17d 100644 --- a/fission/src/systems/input/DefaultInputs.ts +++ b/fission/src/systems/input/DefaultInputs.ts @@ -3,6 +3,7 @@ import { TouchControlsAxes } from "@/ui/components/TouchControls" import type { InputScheme, ModifierState } from "./InputTypes" import AxisInput from "./inputs/AxisInput" import ButtonInput from "./inputs/ButtonInput" +import * as uuid from "uuid" type InputSupplier = () => InputScheme /** The purpose of this class is to store any defaults related to the input system. */ @@ -15,6 +16,7 @@ class DefaultInputs { meta: false, } return { + schemeId: "7e3893a0-bfe1-48f5-b9cb-d0a2e8735aff", schemeName: "Ernie", descriptiveName: "WASD", customized: false, @@ -46,6 +48,7 @@ class DefaultInputs { meta: false, } return { + schemeId: "d3679b65-3660-444b-984e-5f97a1a0f0a1", schemeName: "Bert", descriptiveName: "WSIK", customized: false, @@ -77,6 +80,7 @@ class DefaultInputs { meta: false, } return { + schemeId: "2552cda2-8784-46b6-9661-227b2b6a323f", schemeName: "Luna", descriptiveName: "Arrow Keys", customized: false, @@ -102,6 +106,7 @@ class DefaultInputs { public static jax: InputSupplier = () => { return { + schemeId: "48d1db69-2a71-4244-9b21-cf544af19d5d", schemeName: "Jax", descriptiveName: "Full Controller", customized: false, @@ -129,6 +134,7 @@ class DefaultInputs { /** We like this guy */ public static hunter: InputSupplier = () => { return { + schemeId: "1cd109b1-91cc-4458-a5d9-bdaa9c5c38d2", schemeName: "Hunter", descriptiveName: "Left Stick", customized: false, @@ -151,6 +157,7 @@ class DefaultInputs { public static carmela: InputSupplier = () => { return { + schemeId: "fcce7958-1471-420e-b886-4541b236faff", schemeName: "Carmela", descriptiveName: "Right Stick", customized: false, @@ -173,6 +180,7 @@ class DefaultInputs { public static brandon: InputSupplier = () => { return { + schemeId: "416b39da-6019-49ed-b850-b94e350c61f3", schemeName: "Brandon", descriptiveName: "Touch Controls", customized: false, @@ -187,6 +195,7 @@ class DefaultInputs { } public static julian: InputSupplier = () => { return { + schemeId: "39b7c8e4-2253-4f07-aa97-35cad79c473e", schemeName: "Julian", descriptiveName: "Touch Controls", customized: false, @@ -208,6 +217,7 @@ class DefaultInputs { meta: false, } return { + schemeId: "7e9a4167-e2a0-47dd-a596-60e043177c42", schemeName: "Felix", descriptiveName: "WASD + Arrows (Swerve)", customized: false, @@ -236,6 +246,7 @@ class DefaultInputs { public static gizmo: InputSupplier = () => { return { + schemeId: "8cbe6b7e-10ca-4d15-b26b-feedf690f762", schemeName: "Gizmo", descriptiveName: "Dual Stick (Swerve)", customized: false, @@ -295,6 +306,7 @@ class DefaultInputs { break } return { + schemeId: uuid.v4(), schemeName: "", descriptiveName: "", customized: true, diff --git a/fission/src/systems/input/InputSchemeManager.ts b/fission/src/systems/input/InputSchemeManager.ts index 628ef5ff0c..67455416d8 100644 --- a/fission/src/systems/input/InputSchemeManager.ts +++ b/fission/src/systems/input/InputSchemeManager.ts @@ -84,9 +84,9 @@ class InputSchemeManager { } public static rebindOldBrainSchemes() { - const schemesByName = new Map(this.allInputSchemes.map(s => [s.schemeName, s] as const)) + const schemesById = new Map(this.allInputSchemes.map(s => [s.schemeId, s] as const)) for (const [brainIndex, scheme] of InputSystem.brainIndexSchemeMap) { - const reverted = schemesByName.get(scheme.schemeName) + const reverted = schemesById.get(scheme.schemeId) if (reverted && scheme.customized) { InputSystem.setBrainIndexSchemeMapping(brainIndex, reverted) } @@ -95,18 +95,7 @@ class InputSchemeManager { /** Creates an array of every input scheme that is either a default or customized by the user. Custom themes will appear on top. */ public static get allInputSchemes(): InputScheme[] { - // Start with custom input schemes - const allSchemes: InputScheme[] = [] - - this.customInputSchemes.forEach(s => allSchemes.push(s)) - - // Add default schemes if they have not been customized - this.defaultInputSchemes.forEach(defaultScheme => { - if (allSchemes.some(s => s.schemeName === defaultScheme.schemeName)) return - allSchemes.push(defaultScheme) - }) - - return allSchemes + return [...this.customInputSchemes, ...this.defaultInputSchemes] } /** Creates an array of every input scheme that is not currently in use by a robot */ @@ -115,9 +104,10 @@ class InputSchemeManager { // Remove schemes that have conflicts const usedKeyMap = new Map() + // maps scheme ids to availability const result: Record = {} for (const scheme of InputSystem.brainIndexSchemeMap.values()) { - result[scheme.schemeName] = { + result[scheme.schemeId] = { scheme, status: InputSchemeUseType.IN_USE, } @@ -127,9 +117,9 @@ class InputSchemeManager { .forEach(key => { const entry = usedKeyMap.get(key) if (entry != null) { - entry.push(scheme.schemeName) + entry.push(scheme.schemeId) } else { - usedKeyMap.set(key, [scheme.schemeName]) + usedKeyMap.set(key, [scheme.schemeId]) } }) }) @@ -140,13 +130,13 @@ class InputSchemeManager { input.keysUsed.flatMap(key => usedKeyMap.get(key) ?? []) ) if (conflictingSchemes.length > 0) { - result[scheme.schemeName] ??= { + result[scheme.schemeId] ??= { scheme, status: InputSchemeUseType.CONFLICT, conflictingSchemeNames: [...new Set(conflictingSchemes)].join(", "), } } else { - result[scheme.schemeName] ??= { + result[scheme.schemeId] ??= { scheme, status: InputSchemeUseType.AVAILABLE, } diff --git a/fission/src/systems/input/InputSystem.ts b/fission/src/systems/input/InputSystem.ts index 9c2ee9b88c..8f590267c6 100644 --- a/fission/src/systems/input/InputSystem.ts +++ b/fission/src/systems/input/InputSystem.ts @@ -38,6 +38,7 @@ class InputSystem extends WorldSystem { this.brainIndexSchemeMap.set(index, scheme) World.analyticsSystem?.event("Scheme Applied", { isCustomized: scheme.customized, + schemeId: scheme.schemeId, schemeName: scheme.schemeName, }) } diff --git a/fission/src/systems/input/InputTypes.ts b/fission/src/systems/input/InputTypes.ts index 137a74ee8c..825725dd60 100644 --- a/fission/src/systems/input/InputTypes.ts +++ b/fission/src/systems/input/InputTypes.ts @@ -23,6 +23,8 @@ export type ModifierState = Readonly<{ }> export type InputScheme = { + // UUID + schemeId: string schemeName: string descriptiveName: string customized: boolean diff --git a/fission/src/systems/preferences/PreferencesSystem.ts b/fission/src/systems/preferences/PreferencesSystem.ts index 9df7718cff..ad66c0c8a1 100644 --- a/fission/src/systems/preferences/PreferencesSystem.ts +++ b/fission/src/systems/preferences/PreferencesSystem.ts @@ -1,3 +1,5 @@ +import DefaultInputs from "../input/DefaultInputs" +import * as UUID from "uuid" import { defaultFieldPreferences, defaultGraphicsPreferences, @@ -184,7 +186,32 @@ class PreferencesSystem { unmigratedKeys.forEach(key => { const userPreferences = saved[USER_PREFERENCE_KEY] as Record - userPreferences[key] = saved[key] + + switch (key) { + case "InputSchemes": { + // migrate to uuids + const defaultSchemes = DefaultInputs.defaultInputCopies + userPreferences[key] = saved[key].map(scheme => { + if (scheme.schemeId) return scheme + + if (scheme.customized) { + scheme.schemeId = UUID.v4() + return scheme + } + + const matchingDefaultScheme = + defaultSchemes.find(s => s.descriptiveName === scheme.descriptiveName) ?? + defaultSchemes.find(s => s.schemeName === scheme.schemeName) + + scheme.schemeId = matchingDefaultScheme?.schemeId ?? UUID.v4() + + return scheme + }) + break + } + default: + userPreferences[key] = saved[key] + } delete saved[key] }) diff --git a/fission/src/test/InputSystem.test.ts b/fission/src/test/InputSystem.test.ts index d444e1b91a..58f500a8dd 100644 --- a/fission/src/test/InputSystem.test.ts +++ b/fission/src/test/InputSystem.test.ts @@ -82,7 +82,7 @@ describe("Input Scheme Manager Checks", () => { const rebound = InputSystem.getBrainIndexSchemeMapping(brainIndex)! expect(rebound).not.toBe(edited) - expect(rebound.schemeName).toBe(DefaultInputs.ernie().schemeName) + expect(rebound.schemeId).toBe(DefaultInputs.ernie().schemeId) }) }) diff --git a/fission/src/ui/StateProvider.tsx b/fission/src/ui/StateProvider.tsx index 1a04ec6df8..cf8a2751e8 100644 --- a/fission/src/ui/StateProvider.tsx +++ b/fission/src/ui/StateProvider.tsx @@ -1,11 +1,26 @@ import type React from "react" -import { useMemo, useState } from "react" +import { useMemo, useState, useReducer } from "react" import type { AppMode } from "@/systems/AppMode" import type { InputScheme } from "@/systems/input/InputTypes" +import * as UUID from "uuid" import { StateContext, type StateProviderProps } from "./helpers/StateProviderHelpers" +function updateScheme( + newScheme: InputScheme | undefined, + previousScheme: InputScheme | undefined +): InputScheme | undefined { + if (newScheme === undefined) return undefined + if (previousScheme === undefined) return newScheme + + return { + ...newScheme, + customized: true, + schemeId: previousScheme.customized ? previousScheme.schemeId : UUID.v4(), + } +} + export const StateProvider: React.FC = ({ children }) => { - const [selectedScheme, setSelectedScheme] = useState(undefined) + const [selectedScheme, setSelectedScheme] = useReducer(updateScheme, undefined) const [appMode, setAppMode] = useState("Configure") const stateContextValue = useMemo( diff --git a/fission/src/ui/helpers/StateProviderHelpers.ts b/fission/src/ui/helpers/StateProviderHelpers.ts index 2483ad77d4..ecfe04f44d 100644 --- a/fission/src/ui/helpers/StateProviderHelpers.ts +++ b/fission/src/ui/helpers/StateProviderHelpers.ts @@ -9,6 +9,7 @@ export interface StateProviderProps { export interface AppState { // ConfigureInputs stuff selectedScheme?: InputScheme + // setSelectedScheme will handle updating schemeId and customized setSelectedScheme: (_scheme: InputScheme | undefined) => void // Top bar mode selector appMode: AppMode diff --git a/fission/src/ui/panels/configuring/assembly-config/interfaces/ControlsConfigInterface.tsx b/fission/src/ui/panels/configuring/assembly-config/interfaces/ControlsConfigInterface.tsx index f82af60fea..44d8034226 100644 --- a/fission/src/ui/panels/configuring/assembly-config/interfaces/ControlsConfigInterface.tsx +++ b/fission/src/ui/panels/configuring/assembly-config/interfaces/ControlsConfigInterface.tsx @@ -5,7 +5,7 @@ import { setSpotlightAssembly } from "@/mirabuf/MirabufSceneObject.ts" import ChooseInputSchemePanel from "@/panels/configuring/ChooseInputSchemePanel.tsx" import { CloseType, useUIContext } from "@/ui/helpers/UIProviderHelpers.ts" import ConfigureSchemeInterface from "@/panels/configuring/assembly-config/interfaces/inputs/ConfigureSchemeInterface.tsx" -import { useMemo, useState } from "react" +import { useEffect, useMemo, useState } from "react" import type SynthesisBrain from "@/systems/simulation/synthesis_brain/SynthesisBrain.ts" import { Stack } from "@mui/material" import ConfirmChangesModal from "@/modals/configuring/ConfirmChangesModal.tsx" @@ -20,7 +20,15 @@ const ControlsConfigInterface: ConfigurationSubpanelComponent = ({ const [isEditing, setIsEditing] = useState(false) const brainIndex = useMemo(() => (selectedAssembly.brain as SynthesisBrain).brainIndex, [selectedAssembly]) - const scheme = useMemo(() => InputSystem.getBrainIndexSchemeMapping(brainIndex), [brainIndex]) + + const [scheme, setScheme] = useState(InputSystem.getBrainIndexSchemeMapping(brainIndex)) + useEffect(() => { + setScheme(InputSystem.getBrainIndexSchemeMapping(brainIndex)) + }, [brainIndex]) + useEffect(() => { + if (scheme === undefined) return + InputSystem.setBrainIndexSchemeMapping(brainIndex, scheme) + }, [scheme]) const { openPanel, closePanel, openModal } = useUIContext() return ( @@ -59,6 +67,7 @@ const ControlsConfigInterface: ConfigurationSubpanelComponent = ({ )} diff --git a/fission/src/ui/panels/configuring/assembly-config/interfaces/inputs/ConfigureInputsInterface.tsx b/fission/src/ui/panels/configuring/assembly-config/interfaces/inputs/ConfigureInputsInterface.tsx index 77bee8e721..f27ead5458 100644 --- a/fission/src/ui/panels/configuring/assembly-config/interfaces/inputs/ConfigureInputsInterface.tsx +++ b/fission/src/ui/panels/configuring/assembly-config/interfaces/inputs/ConfigureInputsInterface.tsx @@ -38,9 +38,8 @@ const ConfigureInputsInterface: React.FC { const { openModal, closePanel } = useUIContext() - const { selectedScheme: currentSelectedScheme, setSelectedScheme: setGlobalSelectedScheme } = useStateContext() + const { selectedScheme, setSelectedScheme } = useStateContext() - const [selectedScheme, setSelectedScheme] = useState(currentSelectedScheme) const [schemes, setSchemes] = useState(InputSchemeManager.allInputSchemes) const saveEvent = useCallback(() => { @@ -66,11 +65,10 @@ const ConfigureInputsInterface: React.FC { setSelectedScheme(undefined) - setGlobalSelectedScheme(undefined) unsubscribeConfig() unsubscribeInput() } - }, [saveEvent, handleSchemeChange, setGlobalSelectedScheme]) + }, [saveEvent, handleSchemeChange]) const schemeOptionMap = useMemo(() => { const map = new Map() @@ -134,6 +132,7 @@ const ConfigureInputsInterface: React.FC setSelectedScheme(undefined)} diff --git a/fission/src/ui/panels/configuring/assembly-config/interfaces/inputs/ConfigureSchemeInterface.tsx b/fission/src/ui/panels/configuring/assembly-config/interfaces/inputs/ConfigureSchemeInterface.tsx index 65a7e5d0ed..cd1b64c51a 100644 --- a/fission/src/ui/panels/configuring/assembly-config/interfaces/inputs/ConfigureSchemeInterface.tsx +++ b/fission/src/ui/panels/configuring/assembly-config/interfaces/inputs/ConfigureSchemeInterface.tsx @@ -1,6 +1,6 @@ import { Divider, Stack } from "@mui/material" import type React from "react" -import { useCallback, useEffect, useReducer, useRef, useState } from "react" +import { useCallback, useEffect, useRef } from "react" import Checkbox from "@/components/Checkbox.tsx" import EventSystem from "@/systems/EventSystem.ts" import InputSchemeManager from "@/systems/input/InputSchemeManager" @@ -14,6 +14,7 @@ import type { CleanupRegisterFunction } from "@/panels/configuring/assembly-conf interface ConfigSchemeProps { selectedScheme: InputScheme + setSelectedScheme: (scheme: InputScheme) => void panelId?: string registerCleanupFunction: CleanupRegisterFunction onBack?: () => void @@ -21,14 +22,12 @@ interface ConfigSchemeProps { const ConfigureSchemeInterface: React.FC = ({ selectedScheme, + setSelectedScheme, panelId, onBack, registerCleanupFunction, }) => { - const [useGamepad, setUseGamepad] = useState(selectedScheme.usesGamepad) - const [useTouchControls, setUseTouchControls] = useState(selectedScheme.usesTouchControls) const scrollRef = useRef(null) - const [_, update] = useReducer(x => !x, false) const saveEvent = useCallback(() => { InputSchemeManager.saveSchemes(panelId) }, [panelId]) @@ -96,29 +95,26 @@ const ConfigureSchemeInterface: React.FC = ({ {/** Toggle the input scheme between controller and keyboard mode */} { - setUseGamepad(val) - if (val) { - setUseTouchControls(false) - selectedScheme.usesTouchControls = false - } - selectedScheme.usesGamepad = val - selectedScheme.customized = true + setSelectedScheme({ + ...selectedScheme, + usesGamepad: val, + usesTouchControls: val ? false : selectedScheme.usesTouchControls, + customized: true, + }) }} tooltip="Supported controllers: Xbox one, Xbox 360." /> { - setUseTouchControls(val) - if (val) { - setUseGamepad(false) - selectedScheme.usesGamepad = false - } - selectedScheme.usesTouchControls = val - selectedScheme.customized = true + setSelectedScheme({ + ...selectedScheme, + usesTouchControls: val, + usesGamepad: val ? false : selectedScheme.usesGamepad, + }) }} tooltip="Enable on-screen touch controls (only for mobile devices)." /> @@ -131,10 +127,10 @@ const ConfigureSchemeInterface: React.FC = ({ { - selectedScheme.customized = true + setSelectedScheme({ ...selectedScheme }) }} /> ) @@ -145,9 +141,10 @@ const ConfigureSchemeInterface: React.FC = ({ .map(input => parseInt(input.inputName.replace("joint ", ""))) .filter(val => !isNaN(val)) const newJointIndex = Math.max(0, ...existingJointIndexes) + 1 - selectedScheme.inputs.push(AxisInput.unbound(`joint ${newJointIndex}`)) - selectedScheme.customized = true - update() + setSelectedScheme({ + ...selectedScheme, + inputs: [...selectedScheme.inputs, AxisInput.unbound(`joint ${newJointIndex}`)], + }) }} > Add Joint Control diff --git a/fission/src/ui/panels/configuring/initial-config/InputSchemeSelection.tsx b/fission/src/ui/panels/configuring/initial-config/InputSchemeSelection.tsx index 28a7c08915..0a3821edb5 100644 --- a/fission/src/ui/panels/configuring/initial-config/InputSchemeSelection.tsx +++ b/fission/src/ui/panels/configuring/initial-config/InputSchemeSelection.tsx @@ -36,13 +36,13 @@ const SchemeSelector: React.FC = ({ if (scheme.usesTouchControls && !isTouch) return null return ( - +