import { useState, useEffect, useRef, createContext, useContext } from 'react' import { UserOverridesConfig } from 'contro-max/build/types/store' import { AllKeyCodes } from 'contro-max/build/types/keyCodes' import { contro as controEx } from '../controls' import { hideModal } from '../globalState' import PixelartIcon, { pixelartIcons } from './PixelartIcon' import KeybindingsCustom, { CustomCommandsMap } from './KeybindingsCustom' import { BindingActionsContext } from './KeybindingsScreenProvider' import Button from './Button' import Screen from './Screen' import Keybinding from './Keybinding' import styles from './KeybindingsScreen.module.css' type HandleClick = (group: string, action: string, index: number, type: string | null) => void type setBinding = (data: any, group: string, command: string, buttonIndex: number) => void export const Context = createContext({ isPS: false as boolean | undefined, userConfig: controEx?.userConfig ?? {} as UserOverridesConfig | undefined, setUserConfig (config) { }, handleClick: (() => { }) as HandleClick, bindsMap: { keyboard: {} as any, gamepad: {} as any } }) export default ({ contro, isPS, }: { contro: typeof controEx, isPS?: boolean }) => { const containerRef = useRef(null) const bindsMap = useRef({ keyboard: {} as any, gamepad: {} as any }) const { commands } = contro.inputSchema const [userConfig, setUserConfig] = useState(contro.userConfig ?? {}) const [awaitingInputType, setAwaitingInputType] = useState(null as null | 'keyboard' | 'gamepad') const [groupName, setGroupName] = useState('') const [actionName, setActionName] = useState('') const [buttonNum, setButtonNum] = useState(0) const { updateBinds } = useContext(BindingActionsContext) const [customCommands, setCustomCommands] = useState(userConfig.custom as CustomCommandsMap ?? {}) const updateCurrBind = (group: string, action: string) => { setGroupName(prev => group) setActionName(prev => action) } const handleClick: HandleClick = (group, action, index, type) => { (document.activeElement as HTMLElement)?.blur() setAwaitingInputType(type as any) updateCurrBind(group, action) setButtonNum(prev => index) } const setBinding: setBinding = (data, group, command, buttonIndex) => { setUserConfig(prev => { const newConfig = { ...prev } newConfig[group] ??= {} newConfig[group][command] ??= {} // keys and buttons should always exist in commands const type = 'code' in data ? 'keys' : 'button' in data ? 'gamepad' : null if (type) { newConfig[group][command][type] ??= group === 'custom' ? [] : [...contro.inputSchema.commands[group][command][type]] newConfig[group][command][type]![buttonIndex] = data.code ?? data.button } return newConfig }) } const resetBinding = (group: string, command: string, inputType: string) => { if (!userConfig?.[group]?.[command]) return setUserConfig(prev => { const newConfig = { ...prev } const prop = inputType === 'keyboard' ? 'keys' : 'gamepad' newConfig[group][command][prop] = undefined return newConfig }) } useEffect(() => { updateBinds(userConfig) setCustomCommands({ ...userConfig.custom as CustomCommandsMap }) updateBindMap() }, [userConfig]) const updateBinding = (data: any) => { if ((!data.state && awaitingInputType) || !awaitingInputType) { setAwaitingInputType(null) return } if ('code' in data) { if (data.state && [...contro.pressedKeys].includes(data.code)) return if (data.code === 'Escape' || ['Mouse0', 'Mouse1', 'Mouse2'].includes(data.code)) { setAwaitingInputType(null) return } const pressedModifiers = [...contro.pressedKeys].filter(key => /^(Meta|Control|Alt|Shift)?$/.test(key)) setBinding( { code: pressedModifiers.length ? `${pressedModifiers[0]}+${data.code}` : data.code, state: true }, groupName, actionName, buttonNum ) } if ('button' in data) { contro.enabled = false void Promise.resolve().then(() => { contro.enabled = true }) setBinding(data, groupName, actionName, buttonNum) } } const updateBindMap = () => { bindsMap.current = { keyboard: {} as any, gamepad: {} as any } if (commands) { for (const [group, actions] of Object.entries(commands)) { for (const [action, { keys, gamepad }] of Object.entries(actions)) { if (keys) { let currKeys if (userConfig?.[group]?.[action]?.keys) { currKeys = userConfig[group][action].keys } else { currKeys = keys } for (const [index, key] of currKeys.entries()) { bindsMap.current.keyboard[key] ??= [] if (!bindsMap.current.keyboard[key].some(obj => obj.group === group && obj.action === action && obj.index === index)) { bindsMap.current.keyboard[key].push({ group, action, index }) } } } if (gamepad) { let currButtons if (userConfig?.[group]?.[action]?.gamepad) { currButtons = userConfig[group][action].gamepad } else { currButtons = gamepad } if (currButtons.length > 0) { bindsMap.current.gamepad[currButtons[0]] ??= [] bindsMap.current.gamepad[currButtons[0]].push({ group, action, index: 0 }) } } } } } } // fill binds map useEffect(() => { updateBindMap() }, []) useEffect(() => { if (!awaitingInputType) return contro.on('pressedKeyOrButtonChanged', updateBinding) const preventDefault = (e) => e.preventDefault() document.addEventListener('keydown', preventDefault, { passive: false }) return () => { contro.off('pressedKeyOrButtonChanged', updateBinding) document.removeEventListener('keydown', preventDefault) } }, [groupName, actionName, awaitingInputType]) return {awaitingInputType && }
{Object.entries(commands).map(([group, actions], index) => { if (group === 'custom') return null return
{group}
{group === 'general' ? (
Note: Left, right and middle click keybindings are hardcoded and cannot be changed currently.
) : null} {Object.entries(actions).map(([action, { keys, gamepad }]) => { return
{parseActionName(action)}
})}
})}
} export const ButtonWithMatchesAlert = ({ group, action, index, inputType, keys, gamepad, }) => { const { isPS, userConfig, handleClick, bindsMap } = useContext(Context) const [buttonSign, setButtonSign] = useState('') useEffect(() => { const type = inputType === 'keyboard' ? 'keys' : 'gamepad' const customValue = userConfig?.[group]?.[action]?.[type]?.[index] if (customValue) { setButtonSign(customValue) } else if (type === 'keys') { setButtonSign(keys?.length ? keys[index] : '') } else { setButtonSign(gamepad?.[0] ?? '') } }, [userConfig, isPS]) return
{userConfig?.[group]?.[action]?.[inputType === 'keyboard' ? 'keys' : 'gamepad']?.some(key => Object.keys(bindsMap[inputType]).includes(key) && bindsMap[inputType][key].length > 1 && bindsMap[inputType][key].some(prop => prop.index === index && prop.group === group && prop.action === action)) ? ( //@ts-format-ignore-region
This bind is already in use.
) //@ts-format-ignore-endregion : null}
} export const AwaitingInputOverlay = ({ isGamepad }) => { return
e.preventDefault()} >
{isGamepad ? 'Press the button on the gamepad ' : 'Press the key, side mouse button '} or ESC to cancel.
} const parseActionName = (action: string) => { const parts = action.split(/(?=[A-Z])/) parts[0] = parts[0].charAt(0).toUpperCase() + parts[0].slice(1) return parts.join(' ') }