diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index b99fbcce54a..60f101631ba 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -92,6 +92,8 @@ 1. [EFCS] Fix ground spoiler retraction after increasing TLA slightly above 0 - @lukecologne (luke) 1. [FWC] Improved LDG LT memo to take into account light position - @BravoMike99 (bruno_pt99) 1. [PRESS] Add pressurization system failures - @mjuhe (Miquel Juhe) +1. [EFB] Random Failures - @Maverickwoe (Garoomf) + ## 0.11.0 diff --git a/fbw-a32nx/src/systems/instruments/src/EFB/index.tsx b/fbw-a32nx/src/systems/instruments/src/EFB/index.tsx index f21c0791f9c..74e61b430de 100644 --- a/fbw-a32nx/src/systems/instruments/src/EFB/index.tsx +++ b/fbw-a32nx/src/systems/instruments/src/EFB/index.tsx @@ -6,6 +6,13 @@ import React from 'react'; import { render } from '@instruments/common/index'; import { EfbWrapper } from '@flybywiresim/flypad'; import { A320FailureDefinitions } from '@failures'; +import { EventBusContextProvider } from '../../../../../../fbw-common/src/systems/instruments/src/EFB/event-bus-provider'; // TODO: Move failure definition into VFS -render(, true, true); +render( + + + , + true, + true, +); diff --git a/fbw-common/src/jest/setupJestMock.js b/fbw-common/src/jest/setupJestMock.js index 2af90369553..d292fee444e 100644 --- a/fbw-common/src/jest/setupJestMock.js +++ b/fbw-common/src/jest/setupJestMock.js @@ -2,6 +2,25 @@ let values; global.beforeEach(() => { values = {}; + global.SimVar = {}; + // MSFS SDK overrides those with a custom implementation, hence need to recreate it before every test + global.SimVar.GetSimVarValue = jest.fn((name, _, __) => { + // eslint-disable-next-line no-prototype-builtins + if (values.hasOwnProperty(name)) { + return values[name]; + } else { + return 0; + } + }); + + global.SimVar.SetSimVarValue = jest.fn((name, _, value, __) => { + return new Promise((resolve, _) => { + values[name] = value; + resolve(); + }); + }); + + values = {}; }); global.SimVar = {}; @@ -18,3 +37,18 @@ global.SimVar.SetSimVarValue = jest.fn( resolve(); }), ); +global.RunwayDesignator = jest.mock(); +global.Avionics = jest.mock(); +global.Avionics = jest.mock(); +global.Simplane = jest.mock(); +global.Simplane.getIsGrounded = jest.fn(); +global.Simplane.getEngineThrottleMode = jest.fn(); +global.ThrottleMode = jest.mock(); +global.Simplane.getAltitude = jest.fn(); +global.Simplane.getGroundSpeed = jest.fn(); + +global.Avionics.Utils = jest.mock(); +global.document = jest.mock(); +global.document.getElementById = jest.fn(); +global.GetStoredData = jest.fn(() => 'hi'); +global.BaseInstrument = jest.fn(); diff --git a/fbw-common/src/systems/instruments/src/EFB/Dashboard/Widgets/Reminders/MaintenanceReminder.tsx b/fbw-common/src/systems/instruments/src/EFB/Dashboard/Widgets/Reminders/MaintenanceReminder.tsx index 6f9b2d16151..725900df411 100644 --- a/fbw-common/src/systems/instruments/src/EFB/Dashboard/Widgets/Reminders/MaintenanceReminder.tsx +++ b/fbw-common/src/systems/instruments/src/EFB/Dashboard/Widgets/Reminders/MaintenanceReminder.tsx @@ -27,16 +27,15 @@ const ActiveFailureCard: FC = ({ ata, name }) => { onClick={() => { dispatch(setSearchQuery(name.toUpperCase())); - const lastFailurePath = findLatestSeenPathname(history, '/failures'); - + const lastFailurePath = findLatestSeenPathname(history, '/failures/failure-list'); if (!ata) { - history.push('/failures/compact'); + history.push('/failures/failure-list/compact'); } if (!lastFailurePath || lastFailurePath.includes('comfort')) { - history.push(`/failures/comfort/${ata}`); + history.push(`/failures/failure-list/comfort/${ata}`); } else { - history.push('/failures/compact'); + history.push('/failures/failure-list/compact'); } }} > @@ -51,7 +50,10 @@ export const MaintenanceReminder = () => { const { allFailures, activeFailures } = useFailuresOrchestrator(); return ( - + {Array.from(activeFailures) // Sorts the failures by name length, greatest to least diff --git a/fbw-common/src/systems/instruments/src/EFB/Efb.tsx b/fbw-common/src/systems/instruments/src/EFB/Efb.tsx index 72135e02abe..7e0fa12e2e0 100644 --- a/fbw-common/src/systems/instruments/src/EFB/Efb.tsx +++ b/fbw-common/src/systems/instruments/src/EFB/Efb.tsx @@ -48,7 +48,7 @@ import { Performance } from './Performance/Performance'; import { Navigation } from './Navigation/Navigation'; import { ATC } from './ATC/ATC'; import { Settings } from './Settings/Settings'; -import { Failures } from './Failures/Failures'; +import { FailuresHome } from './Failures/Failures'; import { Presets } from './Presets/Presets'; import { clearEfbState, store, useAppDispatch, useAppSelector } from './Store/store'; import { setFlightPlanProgress } from './Store/features/flightProgress'; @@ -62,6 +62,7 @@ import './Assets/Slider.scss'; import 'react-toastify/dist/ReactToastify.css'; import './toast.css'; +import { FailureGeneratorsInit } from 'instruments/src/EFB/Failures/FailureGenerators/EFBFailureGeneratorsInit'; export interface EfbWrapperProps { failures: FailureDefinition[]; // TODO: Move failure definition into VFS @@ -404,7 +405,11 @@ export const Efb: React.FC = ({ aircraftChecklistsProp }) => { switch (powerState) { case PowerStates.SHUTOFF: case PowerStates.STANDBY: - return ; + return ( + + + + ); case PowerStates.LOADING: case PowerStates.SHUTDOWN: return ; @@ -436,7 +441,7 @@ export const Efb: React.FC = ({ aircraftChecklistsProp }) => { - + diff --git a/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBAltitudeFailureGenerator.tsx b/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBAltitudeFailureGenerator.tsx new file mode 100644 index 00000000000..f9a7359703d --- /dev/null +++ b/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBAltitudeFailureGenerator.tsx @@ -0,0 +1,143 @@ +// Copyright (c) 2021-2023 FlyByWire Simulations +// +// SPDX-License-Identifier: GPL-3.0 + +import { usePersistentProperty } from '@flybywiresim/fbw-sdk'; + +import React, { useMemo, useState } from 'react'; +import { + FailureGenContext, + FailureGenData, + FailureGenMode, + ModalContext, + setNewSetting, +} from 'instruments/src/EFB/Failures/FailureGenerators/EFBRandomFailureGen'; + +import { ArrowDownRight, ArrowUpRight } from 'react-bootstrap-icons'; +import { + ButtonIcon, + FailureGeneratorChoiceSetting, + FailureGeneratorSingleSetting, +} from 'instruments/src/EFB/Failures/FailureGenerators/EFBFailureGeneratorSettingsUI'; +import { t } from '@flybywiresim/flypad'; + +enum Direction { + Climb = 0, + Descent = 1, +} + +const settingName = 'EFB_FAILURE_GENERATOR_SETTING_ALTITUDE'; +const defaultNumberOfFailuresAtOnce = 1; +const defaultMaxNumberOfFailures = 2; +const defaultMinAltitudeHundredsFeet = 80; +const defaultMaxAltitudeHundredsFeet = 250; +const additionalSetting = [ + FailureGenMode.FailureGenTakeOff, + defaultNumberOfFailuresAtOnce, + defaultMaxNumberOfFailures, + Direction.Climb, + defaultMinAltitudeHundredsFeet, + defaultMaxAltitudeHundredsFeet, +]; +const numberOfSettingsPerGenerator = 6; +const uniqueGenPrefix = 'A'; +const genName = 'Altitude'; +const alias = () => t('Failures.Generators.GenAlt'); +const disableTakeOffRearm = false; + +const AltitudeConditionIndex = 3; +const AltitudeMinIndex = 4; +const AltitudeMaxIndex = 5; + +export const failureGenConfigAltitude: () => FailureGenData = () => { + const [setting, setSetting] = usePersistentProperty(settingName); + const [armedState, setArmedState] = useState(); + const settings = useMemo(() => { + const splitString = setting?.split(','); + if (splitString) return splitString.map((it: string) => parseFloat(it)); + return []; + }, [setting]); + + return { + setSetting, + settings, + setting, + numberOfSettingsPerGenerator, + uniqueGenPrefix, + additionalSetting, + genName, + alias, + disableTakeOffRearm, + generatorSettingComponents, + armedState, + setArmedState, + }; +}; + +const generatorSettingComponents = ( + genNumber: number, + modalContext: ModalContext, + failureGenContext: FailureGenContext, +) => { + const settings = modalContext.failureGenData.settings; + const settingTable = [ + , + , + , + ]; + return settingTable; +}; + +const climbDescentMode: ButtonIcon[] = [ + { + icon: ( + <> + + > + ), + settingVar: 0, + setting: 'Climb', + }, + { + icon: ( + <> + + > + ), + settingVar: 1, + setting: 'Descent', + }, +]; diff --git a/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBFailureGeneratorCardTemplateUI.tsx b/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBFailureGeneratorCardTemplateUI.tsx new file mode 100644 index 00000000000..ab06e49082b --- /dev/null +++ b/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBFailureGeneratorCardTemplateUI.tsx @@ -0,0 +1,191 @@ +// Copyright (c) 2021-2023 FlyByWire Simulations +// +// SPDX-License-Identifier: GPL-3.0 + +import React from 'react'; +import { t } from '@flybywiresim/flypad'; +import { + FailureGenContext, + FailureGenData, + ModalContext, + ModalGenType, + findGeneratorFailures, + updateSettings, +} from 'instruments/src/EFB/Failures/FailureGenerators/EFBRandomFailureGen'; +import { ExclamationDiamond, Sliders2Vertical, Trash } from 'react-bootstrap-icons'; +import { TooltipWrapper } from 'instruments/src/EFB/UtilComponents/TooltipWrapper'; +import { ArmingModeIndex } from 'instruments/src/EFB/Failures/FailureGenerators/EFBFailureGeneratorsUI'; +import { AtaChapterNumber, AtaChaptersTitle } from '@flybywiresim/fbw-sdk'; +import { useFailuresOrchestrator } from 'instruments/src/EFB/failures-orchestrator-provider'; +import { ArmedState } from './EFBFailureGeneratorSettingsUI'; +import { useEventBus } from '../../event-bus-provider'; + +export interface FailureGeneratorCardTemplateUIProps { + genNumber: number; + failureGenData: FailureGenData; + failureGenContext: FailureGenContext; +} + +export const FailureGeneratorCardTemplateUI: React.FC = ({ + genNumber, + failureGenData, + failureGenContext, +}) => { + const bus = useEventBus(); + + const genUniqueID = `${failureGenData.uniqueGenPrefix}${genNumber.toString()}`; + const genUniqueIDDisplayed = `${(genNumber + 1).toString()}`; + const isArmed = genNumber < failureGenData.armedState?.length ? failureGenData.armedState[genNumber] : false; + + const eraseGenerator: ( + genID: number, + generatorSettings: FailureGenData, + failureGenContext: FailureGenContext, + ) => void = (genID: number, generatorSettings: FailureGenData, _failureGenContext: FailureGenContext) => { + const generatorNumber = generatorSettings.settings.length / generatorSettings.numberOfSettingsPerGenerator; + if (genID === generatorNumber - 1) { + generatorSettings.settings.splice( + genID * generatorSettings.numberOfSettingsPerGenerator, + generatorSettings.numberOfSettingsPerGenerator, + ); + updateSettings(generatorSettings.settings, generatorSettings.setSetting, bus, generatorSettings.uniqueGenPrefix); + } else { + generatorSettings.settings[genID * generatorSettings.numberOfSettingsPerGenerator + ArmingModeIndex] = -1; + updateSettings(generatorSettings.settings, generatorSettings.setSetting, bus, generatorSettings.uniqueGenPrefix); + } + }; + + return ( + + + + {`${failureGenData.alias()} ${genUniqueIDDisplayed}`} + + + + + eraseGenerator(genNumber, failureGenData, failureGenContext)} + className="text-theme-body hover:text-utility-red bg-utility-red hover:bg-theme-body border-utility-red flex-none + rounded-md border-2 p-2 transition duration-100" + > + + + + + {ArmedState(failureGenData, genNumber)} + + + { + failureGenContext.setFailureGenModalType(ModalGenType.Settings); + const genLetter = failureGenData.uniqueGenPrefix; + const context: ModalContext = { + failureGenData, + genNumber, + genUniqueID, + genLetter, + chainToFailurePool: false, + }; + failureGenContext.setModalContext(context); + }} + > + + + + + { + failureGenContext.setFailureGenModalType(ModalGenType.Failures); + const genLetter = failureGenData.uniqueGenPrefix; + const context: ModalContext = { + failureGenData, + genNumber, + genUniqueID, + genLetter, + chainToFailurePool: false, + }; + failureGenContext.setModalContext(context); + }} + > + + + + + + + ); +}; + +interface FailureShortListProps { + failureGenContext: FailureGenContext; + uniqueID: string; + reducedAtaChapterNumbers: AtaChapterNumber[]; +} + +const FailureShortList: React.FC = ({ + failureGenContext, + uniqueID, + reducedAtaChapterNumbers, +}) => { + const { allFailures } = useFailuresOrchestrator(); + + const maxNumberOfFailureToDisplay = 4; + + let listOfSelectedFailures = findGeneratorFailures(allFailures, failureGenContext.generatorFailuresGetters, uniqueID); + + if (listOfSelectedFailures.length === allFailures.length) { + return {t('Failures.Generators.AllSystems')}; + } + if (listOfSelectedFailures.length === 0) + return {t('Failures.Generators.NoFailure')}; + + const chaptersFullySelected: AtaChapterNumber[] = []; + + for (const chapter of reducedAtaChapterNumbers) { + const failuresActiveInChapter = listOfSelectedFailures.filter((failure) => failure.ata === chapter); + if (failuresActiveInChapter.length === allFailures.filter((failure) => failure.ata === chapter).length) { + chaptersFullySelected.push(chapter); + listOfSelectedFailures = listOfSelectedFailures.filter((failure) => failure.ata !== chapter); + } + } + + const subSetOfChapters = chaptersFullySelected.slice( + 0, + Math.min(maxNumberOfFailureToDisplay, chaptersFullySelected.length), + ); + const subSetOfSelectedFailures = listOfSelectedFailures.slice( + 0, + Math.min(maxNumberOfFailureToDisplay - subSetOfChapters.length, listOfSelectedFailures.length), + ); + const chaptersToDisplay = subSetOfChapters.map((chapter) => ( + {AtaChaptersTitle[chapter]} + )); + + const singleFailuresToDisplay = subSetOfSelectedFailures.map((failure) => ( + {failure.name} + )); + + return ( + + {chaptersToDisplay} + {singleFailuresToDisplay} + {listOfSelectedFailures.length + chaptersFullySelected.length > maxNumberOfFailureToDisplay ? ( + + ...+ + {Math.max(0, listOfSelectedFailures.length + chaptersFullySelected.length - maxNumberOfFailureToDisplay)} + + ) : ( + <>> + )} + + ); +}; diff --git a/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBFailureGeneratorInfo.tsx b/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBFailureGeneratorInfo.tsx new file mode 100644 index 00000000000..344db97d8ec --- /dev/null +++ b/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBFailureGeneratorInfo.tsx @@ -0,0 +1,104 @@ +// Copyright (c) 2021-2023 FlyByWire Simulations +// +// SPDX-License-Identifier: GPL-3.0 + +import React from 'react'; +import { t } from '@flybywiresim/flypad'; +import { + ArrowBarUp, + ArrowDownRight, + ArrowUpRight, + ExclamationDiamond, + Icon1Circle, + Repeat, + Sliders2Vertical, + ToggleOff, + Trash, +} from 'react-bootstrap-icons'; +import { useModals } from '../../UtilComponents/Modals/Modals'; + +export const FailureGeneratorInfoModalUI: React.FC = () => { + const { popModal } = useModals(); + + return ( + + + + {t('Failures.Generators.Legends.InfoTitle')} + + + popModal()} + > + X + + + + {t('Failures.Generators.Legends.Info')} + + + + + {t('Failures.Generators.Legends.ArrowUpRight')} + + + + + + {t('Failures.Generators.Legends.ArrowDownRight')} + + + + + + {t('Failures.Generators.Legends.Sliders2Vertical')} + + + + + + {t('Failures.Generators.Legends.ExclamationDiamond')} + + + + + + {t('Failures.Generators.Legends.Trash')} + + + + + + {t('Failures.Generators.Legends.ToggleOff')} + + + + + + {t('Failures.Generators.Legends.Once')} + + + + + + {t('Failures.Generators.Legends.Airplane')} + + + + + + {t('Failures.Generators.Legends.Repeat')} + + + + ); +}; + +export type ButtonIcon = { + settingVar: number; + icon: JSX.Element; + setting: string; +}; diff --git a/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBFailureGeneratorSettingsUI.tsx b/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBFailureGeneratorSettingsUI.tsx new file mode 100644 index 00000000000..f2a6bacc2a8 --- /dev/null +++ b/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBFailureGeneratorSettingsUI.tsx @@ -0,0 +1,487 @@ +// Copyright (c) 2021-2023 FlyByWire Simulations +// +// SPDX-License-Identifier: GPL-3.0 + +import React from 'react'; +import { t } from '@flybywiresim/flypad'; +import { + FailureGenContext, + FailureGenData, + ModalGenType, + findGeneratorFailures, + setNewSetting, + updateSettings, +} from 'instruments/src/EFB/Failures/FailureGenerators/EFBRandomFailureGen'; +import { extractFirstNumber } from 'instruments/src/EFB/Failures/FailureGenerators/EFBFailureSelectionFunctions'; +import { ArrowBarUp, Repeat, Icon1Circle, ToggleOff } from 'react-bootstrap-icons'; +import { SelectGroup, SelectItem } from 'instruments/src/EFB/UtilComponents/Form/Select'; +import { SimpleInput } from 'instruments/src/EFB/UtilComponents/Form/SimpleInput/SimpleInput'; +import { ButtonType, SettingItem } from 'instruments/src/EFB/Settings/Settings'; +import { + FailuresAtOnceIndex, + MaxFailuresIndex, +} from 'instruments/src/EFB/Failures/FailureGenerators/EFBFailureGeneratorsUI'; +import { ScrollableContainer } from 'instruments/src/EFB/UtilComponents/ScrollableContainer'; +import { TooltipWrapper } from 'instruments/src/EFB/UtilComponents/TooltipWrapper'; +import { ArmingModeIndex } from '../../../../../../../../fbw-common/src/systems/shared/src/failures/RandomFailureGen'; +import { EventBus } from '@microsoft/msfs-sdk'; +import { useFailuresOrchestrator } from '../../failures-orchestrator-provider'; +import { useModals } from '../../UtilComponents/Modals/Modals'; +import { useEventBus } from '../../event-bus-provider'; + +export type SettingVar = { + settingVar: number; +}; + +export const failureActivationMode: (ButtonType & SettingVar)[] = [ + { name: 'One', setting: 'One', settingVar: 0 }, + { name: 'All', setting: 'All', settingVar: 1 }, +]; + +export interface RearmSettingsUIProps { + generatorSettings: FailureGenData; + genID: number; + setNewSetting: ( + bus: EventBus, + newSetting: number, + generatorSettings: FailureGenData, + genID: number, + settingIndex: number, + ) => void; + failureGenContext: FailureGenContext; +} + +export const RearmSettingsUI: React.FC = ({ + generatorSettings, + genID, + setNewSetting, + failureGenContext, +}) => { + const bus = useEventBus(); + + return ( + + + {rearmButtons.map((button) => { + if (button.setting !== 'Take-Off' || generatorSettings.disableTakeOffRearm === false) { + return ( + { + setNewSetting(bus, button.settingVar, generatorSettings, genID, 0); + failureGenContext.setFailureGenModalType(ModalGenType.Settings); + }} + selected={ + generatorSettings.settings[genID * generatorSettings.numberOfSettingsPerGenerator + 0] === + button.settingVar + } + > + {t(button.name)} + + ); + } + return <>>; + })} + + + ); +}; + +export interface FailureGeneratorSingleSettingProps { + title: string; + unit: string; + min: number; + max: number; + value: number; + mult: number; + setNewSetting: ( + bus: EventBus, + newSetting: number, + generatorSettings: FailureGenData, + genID: number, + settingIndex: number, + ) => void; + generatorSettings: FailureGenData; + genIndex: number; + settingIndex: number; + failureGenContext: FailureGenContext; +} + +export const FailureGeneratorSingleSetting: React.FC = ({ + title, + unit, + min, + max, + value, + mult, + setNewSetting, + generatorSettings, + genIndex, + settingIndex, + failureGenContext, +}) => { + const bus = useEventBus(); + + const multCheck = mult === 0 ? 1 : mult; + + return ( + + { + if (!Number.isNaN(parseFloat(x))) { + setNewSetting(bus, parseFloat(x) / multCheck, generatorSettings, genIndex, settingIndex); + } else { + setNewSetting(bus, min, generatorSettings, genIndex, settingIndex); + } + failureGenContext.setFailureGenModalType(ModalGenType.Settings); + }} + /> + + ); +}; + +interface FailureGeneratorSingleSettingShortcutProps { + title: string; + unit: string; + shortCutText: string; + shortCutValue: number; + min: number; + max: number; + value: number; + mult: number; + setNewSetting: (newSetting: number, generatorSettings: FailureGenData, genID: number, settingIndex: number) => void; + generatorSettings: FailureGenData; + genIndex: number; + settingIndex: number; + failureGenContext: FailureGenContext; +} + +interface NextButtonProps { + failureGenContext: FailureGenContext; +} + +export const NextButton: React.FC = ({ failureGenContext }) => { + if (failureGenContext.modalContext.chainToFailurePool === true) { + return ( + + + + { + failureGenContext.setFailureGenModalType(ModalGenType.Failures); + }} + > + {t('Failures.Generators.Next')} + + + ); + } + return <>>; +}; + +export const FailureGeneratorSingleSettingShortcut: React.FC = ({ + title, + unit, + shortCutText, + shortCutValue, + min, + max, + value, + mult, + setNewSetting, + generatorSettings, + genIndex, + settingIndex, + failureGenContext, +}) => { + const multCheck = mult === 0 ? 1 : mult; + + return ( + + + {' ( '} + { + setNewSetting(shortCutValue, generatorSettings, genIndex, settingIndex); + failureGenContext.setFailureGenModalType(ModalGenType.Settings); + }} + > + {shortCutText} + + {' )'} + + { + if (!Number.isNaN(parseFloat(x))) { + setNewSetting(parseFloat(x) / multCheck, generatorSettings, genIndex, settingIndex); + } else { + setNewSetting(min, generatorSettings, genIndex, settingIndex); + } + failureGenContext.setFailureGenModalType(ModalGenType.Settings); + }} + /> + + ); +}; + +export const FailureGeneratorDetailsModalUI: React.FC<{ failureGenContext: FailureGenContext }> = ({ + failureGenContext, +}) => { + const bus = useEventBus(); + const { allFailures } = useFailuresOrchestrator(); + const { popModal } = useModals(); + + const closeModal = () => { + failureGenContext.setFailureGenModalCurrentlyDisplayed(ModalGenType.None); + popModal(); + failureGenContext.setModalContext(undefined); + // console.info('Popped modal'); + }; + + // console.info('MODAL', failureGenContext.modalContext); + let displayContent = <>>; + const genNumber = extractFirstNumber(failureGenContext.modalContext.genUniqueID); + failureGenContext.setFailureGenModalType(ModalGenType.None); + failureGenContext.setFailureGenModalCurrentlyDisplayed(ModalGenType.Settings); + if (genNumber !== undefined && !Number.isNaN(genNumber)) { + const numberOfSelectedFailures = findGeneratorFailures( + allFailures, + failureGenContext.generatorFailuresGetters, + failureGenContext.modalContext.genUniqueID, + ).length; + const sample = + failureGenContext.modalContext.failureGenData.settings[ + genNumber * failureGenContext.modalContext.failureGenData.numberOfSettingsPerGenerator + ArmingModeIndex + ]; + if (sample !== undefined && !Number.isNaN(sample)) { + const setNewNumberOfFailureSetting = (newSetting: number, generatorSettings: FailureGenData, genID: number) => { + const settings = generatorSettings.settings; + settings[genID * generatorSettings.numberOfSettingsPerGenerator + FailuresAtOnceIndex] = newSetting; + settings[genID * generatorSettings.numberOfSettingsPerGenerator + MaxFailuresIndex] = Math.max( + settings[genID * generatorSettings.numberOfSettingsPerGenerator + MaxFailuresIndex], + newSetting, + ); + updateSettings( + generatorSettings.settings, + generatorSettings.setSetting, + bus, + generatorSettings.uniqueGenPrefix, + ); + }; + displayContent = ( + + + + + + + {failureGenContext.modalContext.failureGenData.generatorSettingComponents( + genNumber, + failureGenContext.modalContext, + failureGenContext, + )} + + + + + ); + } else { + closeModal(); + } + } else { + closeModal(); + } + return ( + + + + {t('Failures.Generators.SettingsTitle')} + + + { + closeModal(); + }} + > + X + + + {displayContent} + + ); +}; + +export function ArmedState(generatorSettings: FailureGenData, genNumber: number) { + const readyDisplay: boolean = + genNumber < generatorSettings.armedState?.length ? generatorSettings.armedState[genNumber] : false; + switch (generatorSettings.settings[generatorSettings.numberOfSettingsPerGenerator * genNumber + ArmingModeIndex]) { + case 0: + return ( + + + + ); + case 1: + return ( + + + + ); + case 2: + return ( + + + + ); + case 3: + return ( + + + + ); + default: + return <>>; + } +} + +export type ButtonIcon = { + settingVar: number; + icon: JSX.Element; + setting: string; +}; + +export interface FailureGeneratorChoiceSettingProps { + title: string; + failureGenContext: FailureGenContext; + generatorSettings: FailureGenData; + multiChoice: ButtonIcon[]; + setNewSetting: ( + bus: EventBus, + newSetting: number, + generatorSettings: FailureGenData, + genID: number, + settingIndex: number, + ) => void; + genIndex: number; + settingIndex: number; + value: number; +} + +export const FailureGeneratorChoiceSetting: React.FC = ({ + title, + multiChoice, + generatorSettings, + genIndex, + settingIndex, + failureGenContext, + value, +}) => { + const bus = useEventBus(); + + return ( + + + {multiChoice.map((button) => ( + { + setNewSetting(bus, button.settingVar, generatorSettings, genIndex, settingIndex); + failureGenContext.setFailureGenModalType(ModalGenType.Settings); + }} + selected={value === button.settingVar} + > + {button.icon} + + ))} + + + ); +}; + +export interface FailureGeneratorTextProps { + title: string; + unit: string; + text: string; +} + +export const FailureGeneratorText: React.FC = ({ title, unit, text }) => ( + + {text} + +); + +const rearmButtons: (ButtonType & SettingVar)[] = [ + { name: 'Failures.Generators.Off', setting: 'OFF', settingVar: 0 }, + { name: 'Failures.Generators.Once', setting: 'Once', settingVar: 1 }, + { name: 'Failures.Generators.TakeOff', setting: 'Take-Off', settingVar: 2 }, + { name: 'Failures.Generators.Repeat', setting: 'Repeat', settingVar: 3 }, +]; diff --git a/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBFailureGeneratorsInit.tsx b/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBFailureGeneratorsInit.tsx new file mode 100644 index 00000000000..ad0d752e859 --- /dev/null +++ b/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBFailureGeneratorsInit.tsx @@ -0,0 +1,33 @@ +import { getGeneratorFailurePool } from 'instruments/src/EFB/Failures/FailureGenerators/EFBFailureSelectionFunctions'; +import { + sendFailurePool, + sendSettings, + useFailureGeneratorsSettings, +} from 'instruments/src/EFB/Failures/FailureGenerators/EFBRandomFailureGen'; +import { useEventBus } from 'instruments/src/EFB/event-bus-provider'; +import { useFailuresOrchestrator } from 'instruments/src/EFB/failures-orchestrator-provider'; +import { useEffect } from 'react'; + +export const FailureGeneratorsInit = () => { + const bus = useEventBus(); + const { allFailures } = useFailuresOrchestrator(); + + const settings = useFailureGeneratorsSettings(); + + useEffect(() => { + // console.info('Broadcasting all Failure Gen data once'); + for (const gen of settings.allGenSettings.values()) { + sendSettings(gen.uniqueGenPrefix, gen.setting, bus); + const nbGenerator = Math.floor(gen.settings.length / gen.numberOfSettingsPerGenerator); + for (let i = 0; i < nbGenerator; i++) { + sendFailurePool( + gen.uniqueGenPrefix, + i, + getGeneratorFailurePool(gen.uniqueGenPrefix + i.toString(), Array.from(allFailures)), + bus, + ); + } + } + }, []); + return null; +}; diff --git a/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBFailureGeneratorsUI.tsx b/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBFailureGeneratorsUI.tsx new file mode 100644 index 00000000000..ce49ded893f --- /dev/null +++ b/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBFailureGeneratorsUI.tsx @@ -0,0 +1,278 @@ +// Copyright (c) 2021-2023 FlyByWire Simulations +// +// SPDX-License-Identifier: GPL-3.0 + +import React, { useEffect, useState } from 'react'; +import { SelectInput } from 'instruments/src/EFB/UtilComponents/Form/SelectInput/SelectInput'; +import { t } from '@flybywiresim/flypad'; +import { + FailureGenContext, + FailureGenData, + useFailureGeneratorsSettings, + ModalContext, + ModalGenType, + updateSettings, + sendRefresh, + FailureGenFeedbackEvent, + sendFailurePool, +} from 'instruments/src/EFB/Failures/FailureGenerators/EFBRandomFailureGen'; +import { InfoCircle, PlusLg } from 'react-bootstrap-icons'; +import { FailureGeneratorInfoModalUI } from 'instruments/src/EFB/Failures/FailureGenerators/EFBFailureGeneratorInfo'; +import { FailureGeneratorCardTemplateUI } from 'instruments/src/EFB/Failures/FailureGenerators/EFBFailureGeneratorCardTemplateUI'; +import { ScrollableContainer } from '../../UtilComponents/ScrollableContainer'; +import { GeneratorFailureSelection } from './EFBGeneratorFailureSelectionUI'; +import { FailureGeneratorDetailsModalUI } from './EFBFailureGeneratorSettingsUI'; +import { useFailuresOrchestrator } from '../../failures-orchestrator-provider'; +import { getGeneratorFailurePool, setSelectedFailure } from './EFBFailureSelectionFunctions'; +import { useModals } from '../../UtilComponents/Modals/Modals'; +import { useEventBus } from '../../event-bus-provider'; + +export const ArmingModeIndex = 0; +export const FailuresAtOnceIndex = 1; +export const MaxFailuresIndex = 2; + +export const NumberOfFeedbacks = 2; +export const ReadyDisplayIndex = 1; + +export const FailureGeneratorsUI = () => { + const bus = useEventBus(); + const { allFailures } = useFailuresOrchestrator(); + const { showModal } = useModals(); + + const [chosenGen, setChosenGen] = useState(); + const settings = useFailureGeneratorsSettings(); + + const [genNumberNewGen, setGenNumberNewGen] = useState(undefined); + + const [settingsUpdated, setSettingsUpdated] = useState(false); + + const [checkModalUpdate, setCheckModalUpdate] = useState(false); + + // console.log(settings.allGenSettings); + useEffect(() => { + const genUniqueID = `${settings.allGenSettings.get(chosenGen)?.uniqueGenPrefix}${genNumberNewGen}`; + const genLetter = settings.allGenSettings.get(chosenGen)?.uniqueGenPrefix; + const context: ModalContext = { + failureGenData: settings.allGenSettings.get(chosenGen), + genNumber: genNumberNewGen, + genUniqueID, + genLetter, + chainToFailurePool: true, + }; + // console.info('NEW CONTEXT', context); + settings.setModalContext(context); + }, [settingsUpdated]); + + useEffect(() => { + if (settings.failureGenModalCurrentlyDisplayed !== ModalGenType.None) { + settings.setFailureGenModalType(settings.failureGenModalCurrentlyDisplayed); + } + }, [checkModalUpdate]); + + const sample = + settings.modalContext?.failureGenData?.settings[ + settings.modalContext?.genNumber * settings.modalContext?.failureGenData?.numberOfSettingsPerGenerator + + ArmingModeIndex + ]; + if (sample !== undefined && !Number.isNaN(sample)) { + if (settings.failureGenModalType === ModalGenType.Failures) + showModal(); + if (settings.failureGenModalType === ModalGenType.Settings) + showModal(); + } + + const generatorList = Array.from(settings.allGenSettings.values()).map((genSetting: FailureGenData) => ({ + value: genSetting.genName, + displayValue: `${genSetting.alias()}`, + })); + generatorList.push({ + value: 'default', + displayValue: `<${t('Failures.Generators.SelectInList')}>`, + }); + + const failureGeneratorAdd = (generatorSettings: FailureGenData) => { + let genNumber: number; + let didFindADisabledGen = false; + for (let i = 0; i < generatorSettings.settings.length / generatorSettings.numberOfSettingsPerGenerator; i++) { + if ( + generatorSettings.settings[i * generatorSettings.numberOfSettingsPerGenerator + ArmingModeIndex] === -1 && + !didFindADisabledGen + ) { + for (let j = 0; j < generatorSettings.numberOfSettingsPerGenerator; j++) { + generatorSettings.settings[i * generatorSettings.numberOfSettingsPerGenerator + j] = + generatorSettings.additionalSetting[j]; + } + didFindADisabledGen = true; + genNumber = i; + } + } + if (didFindADisabledGen === false) { + if ( + generatorSettings.settings.length % generatorSettings.numberOfSettingsPerGenerator !== 0 || + generatorSettings.settings.length === 0 + ) { + // console.info(`Number of parameters inconsistent, reseting instances of gen ${generatorSettings.uniqueGenPrefix}`); + updateSettings( + generatorSettings.additionalSetting, + generatorSettings.setSetting, + bus, + generatorSettings.uniqueGenPrefix, + ); + genNumber = 0; + } else { + updateSettings( + generatorSettings.settings.concat(generatorSettings.additionalSetting), + generatorSettings.setSetting, + bus, + generatorSettings.uniqueGenPrefix, + ); + genNumber = Math.floor(generatorSettings.settings.length / generatorSettings.numberOfSettingsPerGenerator); + } + } else { + updateSettings(generatorSettings.settings, generatorSettings.setSetting, bus, generatorSettings.uniqueGenPrefix); + } + sendRefresh(bus); + const genUniqueID = `${generatorSettings.uniqueGenPrefix}${genNumber}`; + + for (const failure of allFailures) { + setSelectedFailure(failure, genUniqueID, settings, true); + } + sendFailurePool( + generatorSettings.uniqueGenPrefix, + genNumber, + getGeneratorFailurePool(generatorSettings.uniqueGenPrefix + genNumber.toString(), Array.from(allFailures)), + bus, + ); + + settings.setFailureGenModalType(ModalGenType.Settings); + setGenNumberNewGen(genNumber); + // console.info('ADDING', generatorSettings); + + // hack to force update of modal context + setSettingsUpdated(!settingsUpdated); + }; + + const treatArmingDisplayStatusEvent = (generatorType: string, status: boolean[]) => { + for (const generator of settings.allGenSettings.values()) { + if (generatorType === generator.uniqueGenPrefix) { + // console.info(`gen ${generator.uniqueGenPrefix} ArmedDisplay received: ${`${generatorType} - ${status.toString()}`}`); + generator.setArmedState(status); + // console.info('received arming states'); + } + } + }; + + const treatExpectedModeEvent = (generatorType: string, mode: number[]) => { + for (const generator of settings.allGenSettings.values()) { + if (generatorType === generator.uniqueGenPrefix) { + // console.info(`gen ${generator.uniqueGenPrefix} expectedMode received: ${generatorType} - ${mode.toString()}`); + const nbGenerator = Math.floor(generator.settings.length / generator.numberOfSettingsPerGenerator); + let changeNeeded = false; + for (let i = 0; i < nbGenerator && i < mode?.length; i++) { + if (generator.settings[i * generator.numberOfSettingsPerGenerator + ArmingModeIndex] !== -1) { + if ( + i < mode?.length && + mode[i] === 0 && + generator.settings[i * generator.numberOfSettingsPerGenerator + ArmingModeIndex] !== 0 + ) { + // console.info(`gen ${generator.uniqueGenPrefix} ${i.toString()} switched off`); + // console.info(`reminder of previous memory state: ${generator.settings[i * generator.numberOfSettingsPerGenerator + ArmingModeIndex]}`); + generator.settings[i * generator.numberOfSettingsPerGenerator + ArmingModeIndex] = 0; + changeNeeded = true; + setCheckModalUpdate(!checkModalUpdate); + } + } + } + if (changeNeeded) { + updateSettings(generator.settings, generator.setSetting, bus, generator.uniqueGenPrefix); + } + } + } + // console.info('received expectedMode'); + }; + + useEffect(() => { + // console.info('subscribing to events'); + const sub1 = bus + .getSubscriber() + .on('expectedMode') + .handle(({ generatorType, mode }) => { + treatExpectedModeEvent(generatorType, mode); + }); + const sub2 = bus + .getSubscriber() + .on('armingDisplayStatus') + .handle(({ generatorType, status }) => { + treatArmingDisplayStatusEvent(generatorType, status); + }); + return () => { + sub1.destroy(); + sub2.destroy(); + }; + }, [settings]); + + return ( + <> + + + + {`${t('Failures.Generators.GeneratorToAdd')}:`} + setChosenGen(value as string)} + options={generatorList} + maxHeight={32} + /> + { + console.info(chosenGen); + if (chosenGen !== 'default') { + failureGeneratorAdd(settings.allGenSettings.get(chosenGen)); + // console.info(settings.modalContext); + } + }} + className="hover:text-theme-body bg-theme-accent hover:bg-theme-highlight flex-none rounded-md p-2 text-center" + > + + + + + showModal()} + className="hover:text-theme-body bg-theme-accent hover:bg-theme-highlight flex-none rounded-md p-2 text-center" + > + + + + + + {generatorsCardList(settings)} + + + > + ); +}; + +export const generatorsCardList = (settings: FailureGenContext) => { + const temp: JSX.Element[] = []; + + for (const [, generatorSetting] of settings.allGenSettings) { + const nbGenerator = Math.floor(generatorSetting.settings.length / generatorSetting.numberOfSettingsPerGenerator); + + for (let i = 0; i < nbGenerator; i++) { + if (generatorSetting.settings[i * generatorSetting.numberOfSettingsPerGenerator + ArmingModeIndex] !== -1) { + temp.push( + , + ); + } + } + } + + return temp; +}; diff --git a/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBFailureSelectionFunctions.tsx b/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBFailureSelectionFunctions.tsx new file mode 100644 index 00000000000..09c69060991 --- /dev/null +++ b/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBFailureSelectionFunctions.tsx @@ -0,0 +1,107 @@ +// Copyright (c) 2021-2023 FlyByWire Simulations +// +// SPDX-License-Identifier: GPL-3.0 + +import { Failure, NXDataStore } from '@flybywiresim/fbw-sdk'; +import { FailureGenContext } from 'instruments/src/EFB/Failures/FailureGenerators/EFBRandomFailureGen'; + +export const getGeneratorFailurePool = (genUniqueID: string, allFailures: Readonly[]): string => { + let failureIDs: string = ''; + let first = true; + const setOfGeneratorFailuresSettings = getSetOfGeneratorFailuresSettings(allFailures); + + if (allFailures.length > 0) { + for (const failure of allFailures) { + const generatorSetting = setOfGeneratorFailuresSettings.get(failure.identifier); + if (generatorSetting) { + const failureGeneratorsTable = generatorSetting.split(','); + if (failureGeneratorsTable.length > 0) { + for (const generator of failureGeneratorsTable) { + if (generator === genUniqueID) { + if (first) { + failureIDs += failure.identifier.toString(); + first = false; + } else failureIDs += `,${failure.identifier.toString()}`; + } + } + } + } + } + } + return failureIDs; +}; + +const getSetOfGeneratorFailuresSettings: (allFailures: readonly Readonly[]) => Map = ( + allFailures: readonly Readonly[], +) => { + const generatorFailuresGetters: Map = new Map(); + if (allFailures.length > 0) { + for (const failure of allFailures) { + // TODO + // Another way of storing settings on the EFB tablet will need to be used when tablet will not be part of the sim + const generatorSetting = NXDataStore.get(`EFB_FAILURE_${failure.identifier.toString()}_GENERATORS`, ''); + generatorFailuresGetters.set(failure.identifier, generatorSetting); + } + } + return generatorFailuresGetters; +}; + +export const setSelectedFailure = ( + failure: Failure, + genIDToChange: string, + failureGenContext: FailureGenContext, + value: boolean, +) => { + const initialString = failureGenContext.generatorFailuresGetters.get(failure.identifier); + const generatorsForFailure = initialString.split(','); + let newSetting: string = ''; + const genIncludedInSetting = generatorsForFailure.includes(genIDToChange); + if (genIncludedInSetting !== value) { + if (value === true) { + if (generatorsForFailure.length > 0) { + newSetting = initialString.concat(`,${genIDToChange}`); + } else { + newSetting = genIDToChange; + } + } else if (generatorsForFailure.length > 0) { + let first = true; + for (const generatorID of generatorsForFailure) { + const letterTable = generatorID.match(regexLetter); + const numberTable = generatorID.match(regexNumber); + if ( + letterTable && + letterTable.length > 0 && + numberTable && + numberTable.length > 0 && + generatorID === `${letterTable[0]}${numberTable[0]}` + ) { + // only keeps the well formated settings in case older formats are present + if (genIDToChange !== generatorID) { + if (first) { + newSetting = newSetting.concat(generatorID); + first = false; + } else { + newSetting = newSetting.concat(`,${generatorID}`); + } + } + } + } + } + failureGenContext.generatorFailuresSetters.get(failure.identifier)(newSetting); + } +}; + +const regexLetter = /\D{1,2}/; +const regexNumber = /\d{1,2}/; + +export function extractFirstLetter(generatorUniqueID: string) { + const letterTable = generatorUniqueID.match(regexLetter); + if (letterTable && letterTable.length > 0) return letterTable[0]; + return ''; +} + +export function extractFirstNumber(generatorUniqueID: string) { + const numberTable = generatorUniqueID.match(regexNumber); + if (numberTable && numberTable.length > 0) return parseInt(numberTable[0]); + return undefined; +} diff --git a/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBGeneratorFailureSelectionUI.tsx b/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBGeneratorFailureSelectionUI.tsx new file mode 100644 index 00000000000..c0a58b8201a --- /dev/null +++ b/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBGeneratorFailureSelectionUI.tsx @@ -0,0 +1,209 @@ +// Copyright (c) 2021-2023 FlyByWire Simulations +// +// SPDX-License-Identifier: GPL-3.0 + +import React from 'react'; +import { + FailureGenContext, + ModalGenType, + findGeneratorFailures, + sendFailurePool, +} from 'instruments/src/EFB/Failures/FailureGenerators/EFBRandomFailureGen'; +import { + getGeneratorFailurePool, + setSelectedFailure, +} from 'instruments/src/EFB/Failures/FailureGenerators/EFBFailureSelectionFunctions'; +import { AtaChapterNumber, AtaChaptersTitle, Failure } from '@flybywiresim/fbw-sdk'; +import { useEventBus } from '../../event-bus-provider'; +import { CheckSquareFill, DashSquare, Square } from 'react-bootstrap-icons'; +import { Toggle } from '../../UtilComponents/Form/Toggle'; +import { ScrollableContainer } from '../../UtilComponents/ScrollableContainer'; +import { useFailuresOrchestrator } from '../../failures-orchestrator-provider'; +import { useModals } from '../../UtilComponents/Modals/Modals'; +import { t } from '@flybywiresim/flypad'; + +interface FailureAtaListProps { + failureGenContext: FailureGenContext; + chapter: AtaChapterNumber; + generatorFailureTable: Failure[]; +} + +const FailureAtaList: React.FC = ({ failureGenContext, chapter, generatorFailureTable }) => { + const { allFailures } = useFailuresOrchestrator(); + const bus = useEventBus(); + + const selectOneFailure = (failure: Failure, failureGenContext: FailureGenContext, active: boolean): void => { + setSelectedFailure(failure, failureGenContext.modalContext.genUniqueID, failureGenContext, active); + sendFailurePool( + failureGenContext.modalContext.genLetter, + failureGenContext.modalContext.genNumber, + getGeneratorFailurePool(failureGenContext.modalContext.genUniqueID, Array.from(allFailures)), + bus, + ); + }; + + const ATAList: JSX.Element[] = allFailures.map((failure) => { + if (failure.ata === chapter) { + const active = + generatorFailureTable.find((genFailure) => failure.identifier === genFailure.identifier) !== undefined; + + return ( + + { + selectOneFailure(failure, failureGenContext, !active); + failureGenContext.setFailureGenModalType(ModalGenType.Failures); + }} + /> + {failure.name} + + ); + } + return <>>; + }); + + return <>{...ATAList}>; +}; + +export interface GeneratorFailureSelectionProps { + failureGenContext: FailureGenContext; +} + +export const GeneratorFailureSelection: React.FC = ({ + failureGenContext, +}): JSX.Element => { + const { allFailures } = useFailuresOrchestrator(); + const { popModal } = useModals(); + const bus = useEventBus(); + + const closeModal = () => { + failureGenContext.setFailureGenModalCurrentlyDisplayed(ModalGenType.None); + popModal(); + failureGenContext.setModalContext(undefined); + // console.info('Popped modal'); + }; + + const generatorFailureTable: Failure[] = findGeneratorFailures( + allFailures, + failureGenContext.generatorFailuresGetters, + failureGenContext.modalContext.genUniqueID, + ); + + failureGenContext.setFailureGenModalType(ModalGenType.None); + failureGenContext.setFailureGenModalCurrentlyDisplayed(ModalGenType.Failures); + + const selectAllFailures = (failureGenContext: FailureGenContext, value: boolean): void => { + for (const failure of allFailures) { + setSelectedFailure(failure, failureGenContext.modalContext.genUniqueID, failureGenContext, value); + } + sendFailurePool( + failureGenContext.modalContext.genLetter, + failureGenContext.modalContext.genNumber, + getGeneratorFailurePool(failureGenContext.modalContext.genUniqueID, Array.from(allFailures)), + bus, + ); + }; + + const selectAllFailureChapter = (chapter: number, failureGenContext: FailureGenContext, value: boolean): void => { + for (const failure of allFailures) { + if (failure.ata === chapter) { + setSelectedFailure(failure, failureGenContext.modalContext.genUniqueID, failureGenContext, value); + } + } + sendFailurePool( + failureGenContext.modalContext.genLetter, + failureGenContext.modalContext.genNumber, + getGeneratorFailurePool(failureGenContext.modalContext.genUniqueID, Array.from(allFailures)), + bus, + ); + }; + + let selectIcon; + if (generatorFailureTable.length === allFailures.length) { + selectIcon = ; + } else if (generatorFailureTable.length === 0) { + selectIcon = ; + } else selectIcon = ; + + return ( + + + + {t('Failures.Generators.FailureSelection')} + + + { + closeModal(); + }} + > + X + + + + {t('Failures.Generators.FailureSelectionText')} + + + + + { + if (generatorFailureTable.length === allFailures.length) selectAllFailures(failureGenContext, false); + else selectAllFailures(failureGenContext, true); + failureGenContext.setFailureGenModalType(ModalGenType.Failures); + }} + > + {selectIcon} + + + {t('Failures.Generators.AllSystems')} + + + + + {failureGenContext.reducedAtaChapterNumbers.map((chapter) => { + let chaptersSelectionIcon; + const failuresActiveInChapter = generatorFailureTable.filter((failure) => failure.ata === chapter); + if (failuresActiveInChapter.length === allFailures.filter((failure) => failure.ata === chapter).length) + chaptersSelectionIcon = ; + else if (failuresActiveInChapter.length === 0) chaptersSelectionIcon = ; + else chaptersSelectionIcon = ; + return ( + + + { + if ( + failuresActiveInChapter.length === + allFailures.filter((failure) => failure.ata === chapter).length + ) { + selectAllFailureChapter(chapter, failureGenContext, false); + } else selectAllFailureChapter(chapter, failureGenContext, true); + failureGenContext.setFailureGenModalType(ModalGenType.Failures); + }} + > + {chaptersSelectionIcon} + + + {AtaChaptersTitle[chapter]} + + + + + ); + })} + + + + ); +}; diff --git a/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBPerHourFailureGenerator.tsx b/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBPerHourFailureGenerator.tsx new file mode 100644 index 00000000000..2b4a92ca9e4 --- /dev/null +++ b/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBPerHourFailureGenerator.tsx @@ -0,0 +1,104 @@ +// Copyright (c) 2021-2023 FlyByWire Simulations +// +// SPDX-License-Identifier: GPL-3.0 + +import { usePersistentProperty } from '@flybywiresim/fbw-sdk'; + +import React, { useMemo, useState } from 'react'; +import { + FailureGenContext, + FailureGenData, + FailureGenMode, + ModalContext, + setNewSetting, +} from 'instruments/src/EFB/Failures/FailureGenerators/EFBRandomFailureGen'; +import { + FailureGeneratorSingleSetting, + FailureGeneratorText, +} from 'instruments/src/EFB/Failures/FailureGenerators/EFBFailureGeneratorSettingsUI'; +import { t } from '@flybywiresim/flypad'; + +const settingName = 'EFB_FAILURE_GENERATOR_SETTING_PERHOUR'; +const defaultNumberOfFailuresAtOnce = 1; +const defaultMaxNumberOfFailures = 2; +const defaultProbabilityPerHour = 0.1; +const additionalSetting = [ + FailureGenMode.FailureGenRepeat, + defaultNumberOfFailuresAtOnce, + defaultMaxNumberOfFailures, + defaultProbabilityPerHour, +]; +const numberOfSettingsPerGenerator = 4; +const uniqueGenPrefix = 'C'; +const genName = 'PerHour'; +const alias = () => t('Failures.Generators.GenPerHour'); +const disableTakeOffRearm = false; +const FailurePerHourIndex = 3; + +export const failureGenConfigPerHour: () => FailureGenData = () => { + const [setting, setSetting] = usePersistentProperty(settingName); + const [armedState, setArmedState] = useState(); + const settings = useMemo(() => { + const splitString = setting?.split(','); + if (splitString) return splitString.map((it: string) => parseFloat(it)); + return []; + }, [setting]); + + return { + setSetting, + settings, + setting, + numberOfSettingsPerGenerator, + uniqueGenPrefix, + additionalSetting, + genName, + alias, + disableTakeOffRearm, + generatorSettingComponents, + armedState, + setArmedState, + }; +}; + +const daysPerMonth = 30.4368 * 24; +const daysPerYear = 365.24219 * 24; + +const generatorSettingComponents = ( + genNumber: number, + modalContext: ModalContext, + failureGenContext: FailureGenContext, +) => { + const settings = modalContext.failureGenData.settings; + const MttfDisplay = () => { + if (settings[genNumber * numberOfSettingsPerGenerator + FailurePerHourIndex] <= 0) + return t('Failures.Generators.Disabled'); + const meanTimeToFailure = 1 / settings[genNumber * numberOfSettingsPerGenerator + FailurePerHourIndex]; + if (meanTimeToFailure >= daysPerYear * 2) + return `${Math.round(meanTimeToFailure / daysPerYear)} ${t('Failures.Generators.years')}`; + if (meanTimeToFailure >= daysPerMonth * 2) + return `${Math.round(meanTimeToFailure / daysPerMonth)} ${t('Failures.Generators.months')}`; + if (meanTimeToFailure >= 24 * 3) return `${Math.round(meanTimeToFailure / 24)} ${t('Failures.Generators.days')}`; + if (meanTimeToFailure >= 5) return `${Math.round(meanTimeToFailure)} ${t('Failures.Generators.hours')}`; + if (meanTimeToFailure > 5 / 60) return `${Math.round(meanTimeToFailure * 60)} ${t('Failures.Generators.minutes')}`; + return `${Math.round(meanTimeToFailure * 60 * 60)} ${t('Failures.Generators.seconds')}`; + }; + + const settingTable = [ + , + , + ]; + + return settingTable; +}; diff --git a/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBRandomFailureGen.tsx b/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBRandomFailureGen.tsx new file mode 100644 index 00000000000..60ec92463bc --- /dev/null +++ b/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBRandomFailureGen.tsx @@ -0,0 +1,291 @@ +// Copyright (c) 2021-2023 FlyByWire Simulations +// +// SPDX-License-Identifier: GPL-3.0 + +import { useEffect, useMemo, useState } from 'react'; +import { AtaChapterNumber, AtaChapterNumbers, Failure, usePersistentProperty, useSimVar } from '@flybywiresim/fbw-sdk'; +import { failureGenConfigAltitude } from 'instruments/src/EFB/Failures/FailureGenerators/EFBAltitudeFailureGenerator'; +import { failureGenConfigPerHour } from 'instruments/src/EFB/Failures/FailureGenerators/EFBPerHourFailureGenerator'; +import { failureGenConfigSpeed } from 'instruments/src/EFB/Failures/FailureGenerators/EFBSpeedFailureGenerator'; +import { failureGenConfigTakeOff } from 'instruments/src/EFB/Failures/FailureGenerators/EFBTakeOffFailureGenerator'; +import { failureGenConfigTimer } from 'instruments/src/EFB/Failures/FailureGenerators/EFBTimerFailureGenerator'; +import { EventBus } from '@microsoft/msfs-sdk'; +import { useEventBus } from 'instruments/src/EFB/event-bus-provider'; +import { useFailuresOrchestrator } from '../../failures-orchestrator-provider'; + +export enum FailureGenMode { + FailureGenOff = 0, + FailureGenOnce = 1, + FailureGenTakeOff = 2, + FailureGenRepeat = 3, +} + +export interface FailureGenFailureList { + failurePool: { generatorType: string; generatorNumber: number; failureString: string }; +} + +export interface FailureGenFeedbackEvent { + expectedMode: { generatorType: string; mode: number[] }; + armingDisplayStatus: { generatorType: string; status: boolean[] }; +} + +export interface FailureGenEvent { + refreshData: boolean; + settings: { generatorType: string; settingsString: string }; +} + +/** + * Data for a generator + * + * TODO confirm + */ +export type FailureGenData = { + /** + * TODO replace with redux action + */ + setSetting: (value: string) => void; + /** + * TODO replace with redux action + */ + setArmedState: (value: boolean[]) => void; + /** + * TODO put in redux + */ + settings: number[]; + /** + * TODO put in redux + */ + setting: string; + /** + * TODO confirm - does this vary per generator? + * Yes, this varies a lot. As soon as you change a setting of the generator (min altitude, speed, mode...) + */ + numberOfSettingsPerGenerator: number; + /** + * TODO put in redux + */ + uniqueGenPrefix: string; + /** + * TODO put in redux, confirm - what even is this for? + * --> This is to identify the type of generator in the events, on the UI and in the stored memory. + * Each generator has its own letter A --> E. It is common to all generator instances within the same type. It never changes + */ + additionalSetting: number[]; + /** + * TODO put in redux + */ + genName: string; + /** + * TODO move this to a react component + */ + generatorSettingComponents: ( + genNumber: number, + modalContext: ModalContext, + failureGenContext: FailureGenContext, + ) => JSX.Element[]; + /** + * TODO confirm - what is this for? + * this is the tailored list of react components specific to this kind of generator that will be displayed in the setting page. + */ + alias: () => string; + /** + * TODO put in redux + */ + disableTakeOffRearm: boolean; + /** + * TODO put in redux + */ + armedState: boolean[]; +}; + +export type FailureGenContext = { + allGenSettings: Map; + generatorFailuresGetters: Map; + generatorFailuresSetters: Map void>; + modalContext: ModalContext; + setModalContext: (modalContext: ModalContext) => void; + failureGenModalType: ModalGenType; + setFailureGenModalType: (type: ModalGenType) => void; + reducedAtaChapterNumbers: AtaChapterNumber[]; + failureGenModalCurrentlyDisplayed: ModalGenType; + setFailureGenModalCurrentlyDisplayed: (type: ModalGenType) => void; +}; + +export type ModalContext = { + failureGenData: FailureGenData; + genNumber: number; + genUniqueID: string; + genLetter: string; + chainToFailurePool: boolean; +}; + +export enum ModalGenType { + None, + Settings, + Failures, +} + +export const flatten = (settings: number[]) => { + let settingString = ''; + for (let i = 0; i < settings.length; i++) { + settingString += settings[i].toString(); + if (i < settings.length - 1) settingString += ','; + } + return settingString; +}; + +export enum FailurePhases { + Dormant, + TakeOff, + InitialClimb, + Flight, +} + +export const basicData = () => { + const [isOnGround] = useSimVar('SIM ON GROUND', 'Bool'); + const maxThrottleMode = Math.max(Simplane.getEngineThrottleMode(0), Simplane.getEngineThrottleMode(1)); + const throttleTakeOff = useMemo( + () => maxThrottleMode === ThrottleMode.FLEX_MCT || maxThrottleMode === ThrottleMode.TOGA, + [maxThrottleMode], + ); + const failureFlightPhase = useMemo(() => { + if (isOnGround) { + if (throttleTakeOff) return FailurePhases.TakeOff; + return FailurePhases.Dormant; + } + if (throttleTakeOff) return FailurePhases.InitialClimb; + return FailurePhases.Flight; + }, [throttleTakeOff, isOnGround]); + return { isOnGround, maxThrottleMode, throttleTakeOff, failureFlightPhase }; +}; + +export const updateSettings: ( + settings: number[], + setSetting: (value: string) => void, + bus: EventBus, + uniqueGenPrefix: string, +) => void = (settings: number[], setSetting: (value: string) => void, bus: EventBus, uniqueGenPrefix: string) => { + const flattenedData = flatten(settings); + sendSettings(uniqueGenPrefix, flattenedData, bus); + console.info(`new permanent setting:${flattenedData}`); + setSetting(flattenedData); +}; + +export const useFailureGeneratorsSettings: () => FailureGenContext = () => { + const bus = useEventBus(); + const { allFailures } = useFailuresOrchestrator(); + const { generatorFailuresGetters, generatorFailuresSetters } = allGeneratorFailures(allFailures); + const allGenSettings: Map = new Map(); + const [failureGenModalType, setFailureGenModalType] = useState(ModalGenType.None); + const [modalContext, setModalContext] = useState(undefined); + const [failureGenModalCurrentlyDisplayed, setFailureGenModalCurrentlyDisplayed] = useState( + ModalGenType.None, + ); + + allGenSettings.set(failureGenConfigAltitude().genName, failureGenConfigAltitude()); + allGenSettings.set(failureGenConfigSpeed().genName, failureGenConfigSpeed()); + allGenSettings.set(failureGenConfigPerHour().genName, failureGenConfigPerHour()); + allGenSettings.set(failureGenConfigTimer().genName, failureGenConfigTimer()); + allGenSettings.set(failureGenConfigTakeOff().genName, failureGenConfigTakeOff()); + + const reducedAtaChapterNumbers: AtaChapterNumber[] = useMemo(() => { + const tempChapters: AtaChapterNumber[] = []; + for (const failure of allFailures) { + const foundChapter = tempChapters.find((value) => value === failure.ata); + if (foundChapter === undefined) { + tempChapters.push(failure.ata); + // console.info(`Adding chapter ${AtaChaptersTitle[failure.ata]}`); + } + } + return tempChapters; + }, [AtaChapterNumbers]); + + useEffect(() => { + sendRefresh(bus); + }, []); + + return { + allGenSettings, + generatorFailuresGetters, + generatorFailuresSetters, + failureGenModalType, + setFailureGenModalType, + modalContext, + setModalContext, + reducedAtaChapterNumbers, + failureGenModalCurrentlyDisplayed, + setFailureGenModalCurrentlyDisplayed, + }; +}; + +export function setNewSetting( + bus: EventBus, + newSetting: number, + generatorSettings: FailureGenData, + genID: number, + settingIndex: number, +) { + const settings = generatorSettings.settings; + settings[genID * generatorSettings.numberOfSettingsPerGenerator + settingIndex] = newSetting; + updateSettings(generatorSettings.settings, generatorSettings.setSetting, bus, generatorSettings.uniqueGenPrefix); +} + +export function sendRefresh(bus: EventBus) { + bus.getPublisher().pub('refreshData', true); + // console.info('requesting refresh'); +} + +export function sendFailurePool(generatorType: string, generatorNumber: number, failureString: string, bus: EventBus) { + // console.info(`failure pool sent ${generatorType}${generatorNumber} : ${failureString}`); + bus.getPublisher().pub('failurePool', { generatorType, generatorNumber, failureString }); +} + +export function sendSettings(generatorType: string, stringTosend: string, bus: EventBus) { + let settingsString: string; + if (stringTosend === undefined) settingsString = ''; + else settingsString = stringTosend; + bus.getPublisher().pub('settings', { generatorType, settingsString }); + //console.info(`settings sent: ${generatorType} - ${settingsString}`); +} + +export const allGeneratorFailures = (allFailures: readonly Readonly[]) => { + const generatorFailuresGetters: Map = new Map(); + const generatorFailuresSetters: Map void> = new Map(); + if (allFailures.length > 0) { + for (const failure of allFailures) { + const [generatorSetting, setGeneratorSetting] = usePersistentProperty( + `EFB_FAILURE_${failure.identifier.toString()}_GENERATORS`, + '', + ); + generatorFailuresGetters.set(failure.identifier, generatorSetting); + generatorFailuresSetters.set(failure.identifier, (s) => { + setGeneratorSetting(s); + }); + } + } + return { generatorFailuresGetters, generatorFailuresSetters }; +}; + +export const findGeneratorFailures = ( + allFailures: readonly Readonly[], + generatorFailuresGetters: Map, + generatorUniqueID: string, +) => { + const failureIDs: Failure[] = []; + + if (allFailures.length > 0) { + for (const failure of allFailures) { + const generatorSetting = generatorFailuresGetters.get(failure.identifier); + if (generatorSetting) { + const failureGeneratorsTable = generatorSetting.split(','); + if (failureGeneratorsTable.length > 0) { + for (const generator of failureGeneratorsTable) { + if (generator === generatorUniqueID) failureIDs.push(failure); + } + } + } + } + } + + return failureIDs; +}; diff --git a/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBSpeedFailureGenerator.tsx b/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBSpeedFailureGenerator.tsx new file mode 100644 index 00000000000..fd5498d2300 --- /dev/null +++ b/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBSpeedFailureGenerator.tsx @@ -0,0 +1,144 @@ +// Copyright (c) 2021-2023 FlyByWire Simulations +// +// SPDX-License-Identifier: GPL-3.0 + +import { usePersistentProperty } from '@flybywiresim/fbw-sdk'; + +import React, { useMemo, useState } from 'react'; +import { + FailureGenContext, + FailureGenData, + FailureGenMode, + ModalContext, + setNewSetting, +} from 'instruments/src/EFB/Failures/FailureGenerators/EFBRandomFailureGen'; +import { ArrowDownRight, ArrowUpRight } from 'react-bootstrap-icons'; +import { + ButtonIcon, + FailureGeneratorChoiceSetting, + FailureGeneratorSingleSetting, +} from 'instruments/src/EFB/Failures/FailureGenerators/EFBFailureGeneratorSettingsUI'; +import { t } from '@flybywiresim/flypad'; + +enum Direction { + Acceleration = 0, + Deceleration = 1, +} + +const settingName = 'EFB_FAILURE_GENERATOR_SETTING_SPEED'; +const defaultNumberOfFailuresAtOnce = 1; +const defaultMaxNumberOfFailures = 2; +const defaultMinSpeed = 200; +const defaultMaxSpeed = 300; +const additionalSetting = [ + FailureGenMode.FailureGenTakeOff, + defaultNumberOfFailuresAtOnce, + defaultMaxNumberOfFailures, + Direction.Acceleration, + defaultMinSpeed, + defaultMaxSpeed, +]; +const numberOfSettingsPerGenerator = 6; +const uniqueGenPrefix = 'B'; +const genName = 'Speed'; +const alias = () => t('Failures.Generators.GenSpeed'); +const disableTakeOffRearm = false; + +const SpeedConditionIndex = 3; +const SpeedMinIndex = 4; +const SpeedMaxIndex = 5; + +export const failureGenConfigSpeed: () => FailureGenData = () => { + const [setting, setSetting] = usePersistentProperty(settingName); + const [armedState, setArmedState] = useState(); + const settings = useMemo(() => { + const splitString = setting?.split(','); + if (splitString) return splitString.map((it: string) => parseFloat(it)); + return []; + }, [setting]); + + return { + setSetting, + settings, + setting, + numberOfSettingsPerGenerator, + uniqueGenPrefix, + additionalSetting, + genName, + generatorSettingComponents, + alias, + disableTakeOffRearm, + armedState, + setArmedState, + }; +}; + +const generatorSettingComponents = ( + genNumber: number, + modalContext: ModalContext, + failureGenContext: FailureGenContext, +) => { + const settings = modalContext.failureGenData.settings; + + const settingTable = [ + , + , + , + ]; + + return settingTable; +}; + +const accelDecelMode: ButtonIcon[] = [ + { + icon: ( + <> + + > + ), + settingVar: 0, + setting: 'Accel', + }, + { + icon: ( + <> + + > + ), + settingVar: 1, + setting: 'Decel', + }, +]; diff --git a/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBTakeOffFailureGenerator.tsx b/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBTakeOffFailureGenerator.tsx new file mode 100644 index 00000000000..9e5451fac61 --- /dev/null +++ b/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBTakeOffFailureGenerator.tsx @@ -0,0 +1,184 @@ +// Copyright (c) 2021-2023 FlyByWire Simulations +// +// SPDX-License-Identifier: GPL-3.0 + +import { usePersistentProperty } from '@flybywiresim/fbw-sdk'; + +import React, { useMemo, useState } from 'react'; +import { + FailureGenContext, + FailureGenData, + FailureGenMode, + ModalContext, + setNewSetting, +} from 'instruments/src/EFB/Failures/FailureGenerators/EFBRandomFailureGen'; +import { + FailureGeneratorSingleSetting, + FailureGeneratorText, +} from 'instruments/src/EFB/Failures/FailureGenerators/EFBFailureGeneratorSettingsUI'; +import { t } from '@flybywiresim/flypad'; + +const settingName = 'EFB_FAILURE_GENERATOR_SETTING_TAKEOFF'; +const numberOfSettingsPerGenerator = 10; +const uniqueGenPrefix = 'E'; +const defaultNumberOfFailuresAtOnce = 1; +const defaultMaxNumberOfFailures = 2; +const defaultChancePerTakeOff = 1; +const defaultProbabilityLowSpeed = 0.33; +const defaultProbabilityMedSpeed = 0.33; +const defaultMinSpeed = 30; +const defaultMinMedSpeed = 95; +const defaultMedHighSpeed = 140; +const defaultHGLMaxHundredsFeet = 40; +const additionalSetting = [ + FailureGenMode.FailureGenRepeat, + defaultNumberOfFailuresAtOnce, + defaultMaxNumberOfFailures, + defaultChancePerTakeOff, + defaultProbabilityLowSpeed, + defaultProbabilityMedSpeed, + defaultMinSpeed, + defaultMinMedSpeed, + defaultMedHighSpeed, + defaultHGLMaxHundredsFeet, +]; +const genName = 'TakeOff'; +const alias = () => t('Failures.Generators.GenTakeOff'); +const disableTakeOffRearm = true; + +const ChancePerTakeOffIndex = 3; +const ChanceLowIndex = 4; +const ChanceMediumIndex = 5; +const MinSpeedIndex = 6; +const MediumSpeedIndex = 7; +const MaxSpeedIndex = 8; +const AltitudeIndex = 9; + +export const failureGenConfigTakeOff: () => FailureGenData = () => { + const [setting, setSetting] = usePersistentProperty(settingName); + const [armedState, setArmedState] = useState(); + const settings = useMemo(() => { + const splitString = setting?.split(','); + if (splitString) return splitString.map((it: string) => parseFloat(it)); + return []; + }, [setting]); + + return { + setSetting, + settings, + setting, + numberOfSettingsPerGenerator, + uniqueGenPrefix, + additionalSetting, + genName, + generatorSettingComponents, + alias, + disableTakeOffRearm, + armedState, + setArmedState, + }; +}; + +const generatorSettingComponents = ( + genNumber: number, + modalContext: ModalContext, + failureGenContext: FailureGenContext, +) => { + const settings = modalContext.failureGenData.settings; + // console.info('SETTINGS IN TAKEOFF', settings); + // console.info(genNumber, numberOfSettingsPerGenerator); + const chanceClimbing = + Math.round( + 10000 * + (1 - + settings[genNumber * numberOfSettingsPerGenerator + ChanceLowIndex] - + settings[genNumber * numberOfSettingsPerGenerator + ChanceMediumIndex]), + ) / 100; + + const settingTable = [ + , + + + + + + , + , + , + , + ]; + + return settingTable; +}; diff --git a/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBTimerFailureGenerator.tsx b/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBTimerFailureGenerator.tsx new file mode 100644 index 00000000000..f3daacbedad --- /dev/null +++ b/fbw-common/src/systems/instruments/src/EFB/Failures/FailureGenerators/EFBTimerFailureGenerator.tsx @@ -0,0 +1,106 @@ +// Copyright (c) 2021-2023 FlyByWire Simulations +// +// SPDX-License-Identifier: GPL-3.0 + +import { usePersistentProperty } from '@flybywiresim/fbw-sdk'; + +import React, { useMemo, useState } from 'react'; +import { + FailureGenContext, + FailureGenData, + FailureGenMode, + ModalContext, + setNewSetting, +} from 'instruments/src/EFB/Failures/FailureGenerators/EFBRandomFailureGen'; +import { FailureGeneratorSingleSetting } from 'instruments/src/EFB/Failures/FailureGenerators/EFBFailureGeneratorSettingsUI'; +import { t } from '@flybywiresim/flypad'; + +const settingName = 'EFB_FAILURE_GENERATOR_SETTING_TIMER'; +const defaultNumberOfFailuresAtOnce = 1; +const defaultMaxNumberOfFailures = 2; +const defaultMinDelay = 300; +const defaultMaxDelay = 600; +const additionalSetting = [ + FailureGenMode.FailureGenTakeOff, + defaultNumberOfFailuresAtOnce, + defaultMaxNumberOfFailures, + defaultMinDelay, + defaultMaxDelay, +]; +const numberOfSettingsPerGenerator = 5; +const uniqueGenPrefix = 'D'; +const genName = 'Timer'; +const alias = () => t('Failures.Generators.GenTimer'); +const disableTakeOffRearm = false; + +const DelayMinIndex = 3; +const DelayMaxIndex = 4; + +export const failureGenConfigTimer: () => FailureGenData = () => { + const [setting, setSetting] = usePersistentProperty(settingName); + const [armedState, setArmedState] = useState(); + const settings = useMemo(() => { + const splitString = setting?.split(','); + if (splitString) { + const newSettings = splitString.map((it: string) => parseFloat(it)); + // console.info(`TIM update of setting array:${newSettings.toString()}`); + return newSettings; + } + return []; + }, [setting]); + + return { + setSetting, + settings, + setting, + numberOfSettingsPerGenerator, + uniqueGenPrefix, + additionalSetting, + genName, + generatorSettingComponents, + alias, + disableTakeOffRearm, + armedState, + setArmedState, + }; +}; + +const generatorSettingComponents = ( + genNumber: number, + modalContext: ModalContext, + failureGenContext: FailureGenContext, +) => { + const settings = modalContext.failureGenData.settings; + + console.log('RERENDER', modalContext); + const settingTable = [ + , + , + ]; + + return settingTable; +}; diff --git a/fbw-common/src/systems/instruments/src/EFB/Failures/Failures.tsx b/fbw-common/src/systems/instruments/src/EFB/Failures/Failures.tsx index f014ccfaf1f..c0c945e69fe 100644 --- a/fbw-common/src/systems/instruments/src/EFB/Failures/Failures.tsx +++ b/fbw-common/src/systems/instruments/src/EFB/Failures/Failures.tsx @@ -2,10 +2,9 @@ // SPDX-License-Identifier: GPL-3.0 import React from 'react'; -import { AtaChaptersTitle } from '@flybywiresim/fbw-sdk'; +import { AtaChapterNumber, AtaChaptersTitle, Failure } from '@flybywiresim/fbw-sdk'; import { Route } from 'react-router-dom'; import { InfoCircleFill } from 'react-bootstrap-icons'; -import { t } from '../Localization/translation'; import { CompactUI } from './Pages/Compact'; import { ComfortUI } from './Pages/Comfort'; import { Navbar } from '../UtilComponents/Navbar'; @@ -15,15 +14,47 @@ import { PageLink, PageRedirect } from '../Utils/routing'; import { useFailuresOrchestrator } from '../failures-orchestrator-provider'; import { setSearchQuery } from '../Store/features/failuresPage'; import { ScrollableContainer } from '../UtilComponents/ScrollableContainer'; +import { FailureGeneratorsUI } from './FailureGenerators/EFBFailureGeneratorsUI'; +import { t } from '@flybywiresim/flypad'; + +export const FailuresHome = () => { + const tabs: PageLink[] = [ + { name: 'Failure-list', alias: t('Failures.Title'), component: }, + { name: 'FailureGenerators', alias: t('Failures.Generators.Title'), component: }, + ]; + + return ( + <> + + {t('Failures.Title')} + + + {t('Failures.FullSimulationOfTheFailuresBelowIsntYetGuaranteed')} + + + + + + + + + + + + > + ); +}; export const Failures = () => { const { allFailures } = useFailuresOrchestrator(); - const chapters = Array.from(new Set(allFailures.map((it) => it.ata))).sort((a, b) => a - b); + const chapters = Array.from(new Set(allFailures.map((it: Failure) => it.ata))).sort( + (a: AtaChapterNumber, b: AtaChapterNumber) => a - b, + ); const dispatch = useAppDispatch(); const { searchQuery } = useAppSelector((state) => state.failuresPage); - const filteredFailures = allFailures.filter((failure) => { + const filteredFailures = allFailures.filter((failure: Failure) => { if (searchQuery === '') { return true; } @@ -38,7 +69,7 @@ export const Failures = () => { }); const filteredChapters = chapters.filter((chapter) => - filteredFailures.map((failure) => failure.ata).includes(chapter), + filteredFailures.map((failure: Failure) => failure.ata).includes(chapter), ); const tabs: PageLink[] = [ @@ -56,15 +87,6 @@ export const Failures = () => { return ( <> - - {t('Failures.Title')} - - - - {t('Failures.FullSimulationOfTheFailuresBelowIsntYetGuaranteed')} - - - { value={searchQuery} onChange={(value) => dispatch(setSearchQuery(value.toUpperCase()))} /> - + - + - + - + > ); }; diff --git a/fbw-common/src/systems/instruments/src/EFB/Failures/Pages/Comfort/AtaChapterPage.tsx b/fbw-common/src/systems/instruments/src/EFB/Failures/Pages/Comfort/AtaChapterPage.tsx index 11207d2aab7..5b48dfebe53 100644 --- a/fbw-common/src/systems/instruments/src/EFB/Failures/Pages/Comfort/AtaChapterPage.tsx +++ b/fbw-common/src/systems/instruments/src/EFB/Failures/Pages/Comfort/AtaChapterPage.tsx @@ -5,11 +5,12 @@ import React from 'react'; import { Link } from 'react-router-dom'; import { ArrowLeft } from 'react-bootstrap-icons'; import { AtaChapterNumber, AtaChaptersTitle, Failure } from '@flybywiresim/fbw-sdk'; -import { t } from '../../../Localization/translation'; -import { FailureButton } from '../../FailureButton'; + import { useFailuresOrchestrator } from '../../../failures-orchestrator-provider'; import { ScrollableContainer } from '../../../UtilComponents/ScrollableContainer'; import { useAppSelector } from '../../../Store/store'; +import { t } from '@flybywiresim/flypad'; +import { FailureButton } from '../../FailureButton'; interface AtaChapterPageProps { chapter: AtaChapterNumber; @@ -39,7 +40,7 @@ export const AtaChapterPage = ({ chapter, failures }: AtaChapterPageProps) => { return ( - + diff --git a/fbw-common/src/systems/instruments/src/EFB/Failures/Pages/Comfort/index.tsx b/fbw-common/src/systems/instruments/src/EFB/Failures/Pages/Comfort/index.tsx index 2677f57b877..8e7d5543bdb 100644 --- a/fbw-common/src/systems/instruments/src/EFB/Failures/Pages/Comfort/index.tsx +++ b/fbw-common/src/systems/instruments/src/EFB/Failures/Pages/Comfort/index.tsx @@ -6,7 +6,6 @@ import React from 'react'; import { Route } from 'react-router'; import { Link } from 'react-router-dom'; import { ScrollableContainer } from '../../../UtilComponents/ScrollableContainer'; -import { t } from '../../../Localization/translation'; import { pathify } from '../../../Utils/routing'; import { AtaChapterPage } from './AtaChapterPage'; import { useFailuresOrchestrator } from '../../../failures-orchestrator-provider'; @@ -21,15 +20,15 @@ const ATAChapterCard = ({ ataNumber, description, title }: ATAChapterCardProps) const { activeFailures, allFailures } = useFailuresOrchestrator(); const hasActiveFailure = allFailures - .filter((it) => it.ata === ataNumber) - .some((it) => activeFailures.has(it.identifier)); + .filter((it: Failure) => it.ata === ataNumber) + .some((it: Failure) => activeFailures.has(it.identifier)); return ( - + {`ATA ${ataNumber}`} @@ -57,11 +56,10 @@ interface ComfortUIProps { export const ComfortUI = ({ filteredChapters, allChapters, failures }: ComfortUIProps) => ( <> - + {filteredChapters.map((chapter) => ( {allChapters.map((chapter) => ( - + ))} diff --git a/fbw-common/src/systems/instruments/src/EFB/Localization/data/en.json b/fbw-common/src/systems/instruments/src/EFB/Localization/data/en.json index 83c9a58ab37..dd804c90422 100644 --- a/fbw-common/src/systems/instruments/src/EFB/Localization/data/en.json +++ b/fbw-common/src/systems/instruments/src/EFB/Localization/data/en.json @@ -124,7 +124,81 @@ }, "FullSimulationOfTheFailuresBelowIsntYetGuaranteed": "Full simulation of the failures below isn't yet guaranteed.", "Search": "Search", - "Title": "Failures" + "Title": "Failures", + "Generators": { + "Acceleration": "Acceleration", + "All": "All", + "AllSystems": "All Systems", + "AltitudeCondition": "Altitude condition", + "AltitudeMax": "Maximum altitude", + "AltitudeMin": "Minimum altitude", + "Arming": "Arming", + "ClimbingChance": "Probability at high speed and climb", + "days": "days", + "DelayAfterArmingMax": "Maximum delay after arming", + "DelayAfterArmingMin": "Minimum delay after arming", + "Disabled": "Disabled", + "FailureChancePerTakeOff": "Failure probability per take-off", + "FailurePerHour": "Average number of failure per hour", + "FailureSelection": "Failure pool selection", + "FailureSelectionText": "Select the failures that may be triggered during this scenario", + "feet": "feet", + "GenTimer": "Timed Failure", + "GenTakeOff": "Take-Off", + "GenAlt": "Altitude", + "GenPerHour": "Probability over time", + "GenSpeed": "Speed", + "hours": "hours", + "knots": "knots", + "LowSpeedChance": "Probability at low Speed", + "MaxHeightAboveRunway": "Maximum height above runway", + "MaximumGroundSpeed": "Maximum ground speed", + "MaxSimultaneous": "Maximum number of simultaneous failures", + "MeanTimeToFailure": "Mean Time To Failure", + "MedSpeedChance": "Probability at medium Speed", + "MinimumGroundSpeed": "Minimum ground speed", + "minutes": "minutes", + "months": "months", + "Next": "Next", + "None": "None", + "NumberOfFailures": "Number of failures", + "NoFailure": "No failure", + "Off": "Off", + "Once": "Once", + "Repeat": "Repeat", + "seconds": "seconds", + "GeneratorToAdd": "Failure Generator to add", + "SelectInList": "Select a Generator", + "SettingsTitle": "Triggering conditions", + "SpeedCondition": "Speed condition", + "SpeedTransLowMed": "Speed transition low-med", + "SpeedTransMedHigh": "Speed transition med-high", + "SplitOverPhases": "Split over these phases", + "TakeOff": "Take-Off", + "ToolTipFailureList": "Opens the page to select failures that can be triggered by this failure generator", + "ToolTipGeneratorSettings": "Opens the page for the settings of this failure generator", + "ToolTipOff": "No failure will occur", + "ToolTipOnce": "Failures will occur only once, when the conditions are met", + "ToolTipReady": "READY", + "ToolTipStandby": "STANDBY", + "ToolTipRepeat": "Failures will occur every time the conditions are met", + "ToolTipTakeOff": "Failures will occur only once per take-off, when the conditions are met", + "Title": "Failure Generator", + "years": "years", + "Legends": { + "Airplane": "The failure generator will trigger the failures only once after each take off.", + "ArrowUpRight": "The failure will happen when speed or altitude is increasing.", + "ArrowDownRight": "The failure will happen when speed or altitude is decreasing.", + "ExclamationDiamond": "The page to select which failures can happen when the generator triggers.", + "Info": "For More information see: docs.flybywiresim.com/a32nx-failures", + "InfoTitle": "Information and legend", + "Once": "The failure generator is armed and will trigger the failures only once and then disarm.", + "Repeat": "The failure generator is armed and will repeatingly trigger the failures when conditions are met.", + "Sliders2Vertical": "The page for the settings of each failure generator.", + "ToggleOff": "The failure generator is not armed and will not trigger any failure.", + "Trash": "Removes the failure generator and its settings." + } + } }, "Ground": { "Fuel": { diff --git a/fbw-common/src/systems/instruments/src/EFB/Localization/data/fr.json b/fbw-common/src/systems/instruments/src/EFB/Localization/data/fr.json index 9416d1a74ce..5622ea73f2c 100644 --- a/fbw-common/src/systems/instruments/src/EFB/Localization/data/fr.json +++ b/fbw-common/src/systems/instruments/src/EFB/Localization/data/fr.json @@ -123,7 +123,79 @@ }, "FullSimulationOfTheFailuresBelowIsntYetGuaranteed": "La simulation complète de toutes les pannes n'est pas garantie.", "Search": "Rechercher", - "Title": "Pannes" + "Title": "Pannes", + "Generators": { + "Acceleration": "Accélération", + "All": "Tout", + "AllSystems": "Tous les systèmes", + "AltitudeCondition": "Condition d'altitude", + "AltitudeMax": "Altitude maximale", + "AltitudeMin": "Altitude minimale", + "Arming": "Armement", + "ClimbingChance": "Probabilité à haute vitesse et montée", + "days": "jours", + "DelayAfterArmingMax": "Delai maximal après armement", + "DelayAfterArmingMin": "Delai minimal après armement", + "Disabled": "Désactivé", + "FailureChancePerTakeOff": "Probabilité par décollage", + "FailurePerHour": "Nombre moyen de panne par heure", + "FailureSelection": "Choix des pannes possibles", + "FailureSelectionText": "Selectionnez les pannes possibles lors de ce scénario", + "feet": "pieds", + "GenTimer": "Panne temporisée", + "GenTakeOff": "Décollage", + "GenAlt": "Altitude", + "GenPerHour": "Probabilité temporelle", + "GenSpeed": "Vitesse", + "hours": "heures", + "knots": "noeuds", + "LowSpeedChance": "Probabilité basse vitesse", + "MaxHeightAboveRunway": "Hauteur maximum au dessus de la piste", + "MaximumGroundSpeed": "Vitesse au sol maximale", + "MaxSimultaneous": "Nombre maximum de pannes simultanées", + "MeanTimeToFailure": "Temps moyen avant panne", + "MedSpeedChance": "Probabilité à vitesse moyenne", + "MinimumGroundSpeed": "Vitesse au sol minimale", + "minutes": "minutes", + "months": "mois", + "Next": "Suivant", + "None": "Aucun", + "NumberOfFailures": "Nombre de pannes", + "NoFailure": "Aucune panne", + "Off": "Eteint", + "Once": "Unique", + "Repeat": "Répété", + "seconds": "secondes", + "GeneratorToAdd": "Générateur de pannes à ajouter", + "SelectInList": "Selectionnez un générateur", + "SettingsTitle": "Conditions d'activation", + "SpeedCondition": "Condition de vitesse", + "SpeedTransLowMed": "Vitesse de transition lent-moyen", + "SpeedTransMedHigh": "Vitesse de transition moyen-rapide", + "SplitOverPhases": "Répartis dans ces phases", + "TakeOff": "Décollage", + "ToolTipFailureList": "Ouvre la page de choix des pannes déclenchables par ce générateur", + "ToolTipGeneratorSettings": "Ouvre la page des régales du générateur de pannes", + "ToolTipOff": "Aucune panne ne va survenir", + "ToolTipOnce": "Des pannes se déclencheront un seule fois dès que les conditions sont réunies", + "ToolTipRepeat": "Des pannes se déclencheront à chaque fois que les conditions sont réunies", + "ToolTipTakeOff": "Des pannes se déclencheront un seule fois par décollage dès que les conditions sont réunies", + "Title": "Générateur de panne", + "years": "années", + "Legends": { + "Airplane": "Le générateur va déclencher des pannes une seule fois après chaque décollage.", + "ArrowUpRight": "La panne va se décelncher lorsque la vitesse ou l'altitude augmente.", + "ArrowDownRight": "a panne va se décelncher lorsque la vitesse ou l'altitude diminue.", + "ExclamationDiamond": "La page où selectionner les pannes qui peuvent survenir lorsque le générateur se déclenche.", + "Info": "Pour plus d'informations, consultez: docs.flybywiresim.com/a32nx-failures", + "InfoTitle": "Informations et legende", + "Once": "Le générateur de panne est armé et va déclencher des pannes une seule fois puis se désarmer.", + "Repeat": "Le générateur de panne est armé et va déclencher de manière répétée des pannes quand ses conditions sont remplies.", + "Sliders2Vertical": "La page pour les réglages de chaque générateur de panne.", + "ToggleOff": "Le générateur de panne n'est pas armé et ne déclenchera aucune panne.", + "Trash": "Supprime le générateur de panne et ses réglages." + } + } }, "Ground": { "Fuel": { @@ -700,4 +772,4 @@ }, "Title": "Nouvelle version disponible" } -} \ No newline at end of file +} diff --git a/fbw-common/src/systems/instruments/src/EFB/UtilComponents/Modals/Modals.tsx b/fbw-common/src/systems/instruments/src/EFB/UtilComponents/Modals/Modals.tsx index 0f306448235..20538c87d86 100644 --- a/fbw-common/src/systems/instruments/src/EFB/UtilComponents/Modals/Modals.tsx +++ b/fbw-common/src/systems/instruments/src/EFB/UtilComponents/Modals/Modals.tsx @@ -5,7 +5,7 @@ import React, { createContext, FC, useContext, useState } from 'react'; import { t } from '../../Localization/translation'; -interface ModalContextInterface { +export interface ModalContextInterface { showModal: (modal: JSX.Element) => void; modal?: JSX.Element; popModal: () => void; @@ -67,19 +67,19 @@ export const PromptModal: FC = ({ }; return ( - + {title} {bodyText} {cancelText ?? t('Modals.Cancel')} {confirmText ?? t('Modals.Confirm')} @@ -98,11 +98,11 @@ export const AlertModal: FC = ({ title, bodyText, onAcknowledge }; return ( - + {title} {bodyText} {acknowledgeText ?? t('Modals.Okay')} diff --git a/fbw-common/src/systems/instruments/src/EFB/event-bus-provider.tsx b/fbw-common/src/systems/instruments/src/EFB/event-bus-provider.tsx new file mode 100644 index 00000000000..550388b3192 --- /dev/null +++ b/fbw-common/src/systems/instruments/src/EFB/event-bus-provider.tsx @@ -0,0 +1,16 @@ +// Copyright (c) 2021-2023 FlyByWire Simulations +// +// SPDX-License-Identifier: GPL-3.0 + +import { EventBus } from '@microsoft/msfs-sdk'; + +import React, { useContext } from 'react'; + +const Context = React.createContext(undefined as any); + +export const EventBusContextProvider: React.FC = ({ children }) => { + const bus = new EventBus(); + return {children}; +}; + +export const useEventBus = () => useContext(Context); diff --git a/fbw-common/src/systems/instruments/src/EFB/failures-orchestrator-provider.tsx b/fbw-common/src/systems/instruments/src/EFB/failures-orchestrator-provider.tsx index 78bccc037b8..5669c5da605 100644 --- a/fbw-common/src/systems/instruments/src/EFB/failures-orchestrator-provider.tsx +++ b/fbw-common/src/systems/instruments/src/EFB/failures-orchestrator-provider.tsx @@ -3,6 +3,8 @@ import React, { PropsWithChildren, useState } from 'react'; import { Failure, FailuresOrchestrator, useUpdate, FailureDefinition } from '@flybywiresim/fbw-sdk'; +import { EventBus } from '@microsoft/msfs-sdk'; +import { useEventBus } from './event-bus-provider'; interface FailuresOrchestratorContext { allFailures: Readonly[]>; @@ -12,7 +14,8 @@ interface FailuresOrchestratorContext { deactivate(identifier: number): Promise; } -const createOrchestrator = (failures: FailureDefinition[]) => new FailuresOrchestrator('A32NX', failures); +const createOrchestrator = (failures: FailureDefinition[], bus: EventBus) => + new FailuresOrchestrator(bus, 'A32NX', failures); const Context = React.createContext({ allFailures: [], @@ -25,12 +28,12 @@ const Context = React.createContext({ export interface FailuresOrchestratorProviderProps { failures: FailureDefinition[]; } - export const FailuresOrchestratorProvider: React.FC> = ({ failures, children, }) => { - const [orchestrator] = useState(() => createOrchestrator(failures)); + const bus = useEventBus(); + const [orchestrator] = useState(() => createOrchestrator(failures, bus)); const [allFailures] = useState(() => orchestrator.getAllFailures()); const [activeFailures, setActiveFailures] = useState>(() => new Set()); diff --git a/fbw-common/src/systems/instruments/src/react/bitFlags.tsx b/fbw-common/src/systems/instruments/src/react/bitFlags.tsx index 6b226885ace..4fd61cac29c 100644 --- a/fbw-common/src/systems/instruments/src/react/bitFlags.tsx +++ b/fbw-common/src/systems/instruments/src/react/bitFlags.tsx @@ -1,5 +1,7 @@ +/* eslint-disable function-paren-newline */ +import { PaxStationInfo, SeatFlags } from '@flybywiresim/fbw-sdk'; + import { useCallback, useMemo, useRef, useState } from 'react'; -import { PaxStationInfo, SeatFlags } from '../../../shared/src'; import { useSimVarList } from './simVars'; import { useUpdate } from './hooks'; diff --git a/fbw-common/src/systems/shared/src/ata.ts b/fbw-common/src/systems/shared/src/ata.ts index 9730380b2fe..cd3ec502c19 100644 --- a/fbw-common/src/systems/shared/src/ata.ts +++ b/fbw-common/src/systems/shared/src/ata.ts @@ -104,3 +104,7 @@ export const AtaChaptersDescription = Object.freeze({ }); export type AtaChapterNumber = keyof typeof AtaChaptersTitle; + +export const AtaChapterNumbers: AtaChapterNumber[] = Object.keys(AtaChaptersTitle).map((it) => + parseInt(it), +) as AtaChapterNumber[]; diff --git a/fbw-common/src/systems/shared/src/failures/AltitudeFailureGenerator.ts b/fbw-common/src/systems/shared/src/failures/AltitudeFailureGenerator.ts new file mode 100644 index 00000000000..55e381bad47 --- /dev/null +++ b/fbw-common/src/systems/shared/src/failures/AltitudeFailureGenerator.ts @@ -0,0 +1,81 @@ +// Copyright (c) 2021-2023 FlyByWire Simulations +// +// SPDX-License-Identifier: GPL-3.0 + +import { GenericGenerator } from './GenericGenerator'; + +enum Direction { + Climb = 0, + Descent = 1, +} + +export class FailureGeneratorAltitude extends GenericGenerator { + numberOfSettingsPerGenerator = 6; + + uniqueGenPrefix: string = 'A'; + + private rolledDice: number[] = []; + + private previousAltitudeCondition: number[] = []; + + private altitudeConditionIndex = 3; + + private altitudeMinIndex = 4; + + private altitudeMaxIndex = 5; + + private resetMargin = 100; + + private altitude: number; + + loopStartAction(): void { + this.altitude = SimVar.GetSimVarValue('INDICATED ALTITUDE', 'Feet') || '0'; + } + + additionalInitActions(genNumber: number): void { + this.previousAltitudeCondition[genNumber] = + this.settings[genNumber * this.numberOfSettingsPerGenerator + this.altitudeConditionIndex]; + } + + generatorSpecificActions(genNumber: number): void { + const altitudeCondition = + this.settings[genNumber * this.numberOfSettingsPerGenerator + this.altitudeConditionIndex]; + + if (this.previousAltitudeCondition[genNumber] !== altitudeCondition) { + this.disarm(genNumber); + } + } + + conditionToTriggerFailure(genNumber: number): boolean { + const altitudeMax = this.settings[genNumber * this.numberOfSettingsPerGenerator + this.altitudeMaxIndex] * 100; + const altitudeMin = this.settings[genNumber * this.numberOfSettingsPerGenerator + this.altitudeMinIndex] * 100; + const altitudeCondition = + this.settings[genNumber * this.numberOfSettingsPerGenerator + this.altitudeConditionIndex]; + const failureAltitude = altitudeMin + this.rolledDice[genNumber] * (altitudeMax - altitudeMin); + + return ( + (this.altitude > failureAltitude && altitudeCondition === Direction.Climb) || + (this.altitude < failureAltitude && altitudeCondition === Direction.Descent) + ); + } + + conditionToArm(genNumber: number): boolean { + const altitudeMax = this.settings[genNumber * this.numberOfSettingsPerGenerator + this.altitudeMaxIndex] * 100; + const altitudeMin = this.settings[genNumber * this.numberOfSettingsPerGenerator + this.altitudeMinIndex] * 100; + const altitudeCondition = + this.settings[genNumber * this.numberOfSettingsPerGenerator + this.altitudeConditionIndex]; + return ( + (this.altitude < altitudeMin - this.resetMargin && altitudeCondition === Direction.Climb) || + (this.altitude > altitudeMax + this.resetMargin && altitudeCondition === Direction.Descent) + ); + } + + additionalArmingActions(genNumber: number): void { + this.rolledDice[genNumber] = Math.random(); + } + + additionalGenEndActions(genNumber: number): void { + this.previousAltitudeCondition[genNumber] = + this.settings[genNumber * this.numberOfSettingsPerGenerator + this.altitudeConditionIndex]; + } +} diff --git a/fbw-common/src/systems/shared/src/failures/GenericGenerator.ts b/fbw-common/src/systems/shared/src/failures/GenericGenerator.ts new file mode 100644 index 00000000000..4a1457d2231 --- /dev/null +++ b/fbw-common/src/systems/shared/src/failures/GenericGenerator.ts @@ -0,0 +1,312 @@ +// Copyright (c) 2021-2023 FlyByWire Simulations +// +// SPDX-License-Identifier: GPL-3.0 + +import { EventBus } from '@microsoft/msfs-sdk'; + +import { + ArmingModeIndex, + FailurePhases, + FailuresAtOnceIndex, + MaxFailuresIndex, + RandomFailureGen, +} from './RandomFailureGen'; +import { Failure, FailuresOrchestrator } from './failures-orchestrator'; + +export enum FailureGenMode { + FailureGenAny = -1, + FailureGenOff = 0, + FailureGenOnce = 1, + FailureGenTakeOff = 2, + FailureGenRepeat = 3, +} + +export interface FailureGenFailureList { + failurePool: { generatorType: string; generatorNumber: number; failureString: string }; +} + +export interface FailureGenFeedbackEvent { + expectedMode: { generatorType: string; mode: number[] }; + armingDisplayStatus: { generatorType: string; status: boolean[] }; +} + +export interface FailureGenEvent { + refreshData: boolean; + settings: { generatorType: string; settingsString: string }; +} +export abstract class GenericGenerator { + numberOfSettingsPerGenerator: number = 3; + + uniqueGenPrefix: string = 'UNIQUE LETTER HERE'; + + failureGeneratorArmed: boolean[] = []; + + waitForTakeOff: boolean[] = []; + + waitForStopped: boolean[] = []; + + previousArmingMode: number[] = []; + + previousNbGenerator: number = 0; + + previousArmedState: boolean[] = []; + + previousRequestedMode: number[] = []; + + gs: number = 0; + + settings: number[] = []; + + requestedMode: number[] = []; + + armingMode: number[] = []; + + failuresAtOnce: number[] = []; + + maxFailures: number[] = []; + + refreshRequest: boolean = false; + + failurePool: string[] = []; + + getGeneratorFailurePool(failureOrchestrator: FailuresOrchestrator, genNumber: number): Failure[] { + const failureIDs: Failure[] = []; + const allFailures = failureOrchestrator.getAllFailures(); + + if (allFailures.length > 0) { + const failureGeneratorsTable = this.failurePool[genNumber].split(','); + for (const failureID of failureGeneratorsTable) { + if (failureGeneratorsTable.length > 0) { + const temp = allFailures.find((value) => value.identifier.toString() === failureID); + if (temp !== undefined) failureIDs.push(temp); + } + } + } + return failureIDs; + } + constructor( + private readonly randomFailuresGen: RandomFailureGen, + protected readonly bus: EventBus, + ) { + if (this.bus != null) { + this.bus + .getSubscriber() + .on('refreshData') + .handle((_value) => { + this.refreshRequest = true; + // console.info(`refresh request received: ${this.uniqueGenPrefix}`); + }); + this.bus + .getSubscriber() + .on('settings') + .handle(({ generatorType, settingsString }) => { + // console.info('DISARMED'); + if (generatorType === this.uniqueGenPrefix) { + // console.info(`settings received: ${generatorType} - ${settingsString}`); + this.settings = settingsString.split(',').map((it) => parseFloat(it)); + } + }); + this.bus + .getSubscriber() + .on('failurePool') + .handle(({ generatorType, generatorNumber, failureString }) => { + if (generatorType === this.uniqueGenPrefix) { + console.info(`failure pool received ${generatorType}${generatorNumber}: ${failureString}`); + + this.failurePool[generatorNumber] = failureString; + } + }); + } + } + + arm(genNumber: number): void { + this.failureGeneratorArmed[genNumber] = true; + // console.info('ARMED'); + } + + disarm(genNumber: number): void { + this.failureGeneratorArmed[genNumber] = false; + // console.info('DISARMED'); + } + + reset(genNumber: number): void { + this.disarm(genNumber); + this.waitForTakeOff[genNumber] = true; + this.waitForStopped[genNumber] = true; + } + + loopStartAction(): void { + // + } + + additionalGenInitActions(_genNumber: number): void { + // + } + + generatorSpecificActions(_genNumber: number): void { + // + } + + conditionToTriggerFailure(_genNumber: number): boolean { + return false; + } + + additionalFailureTriggeredActions(_genNumber: number): void { + // + } + + conditionToArm(_genNumber: number): boolean { + return false; + } + + additionalArmingActions(_genNumber: number): void { + // + } + + additionalGenEndActions(_genNumber: number): void { + // + } + + loopEndAction(): void { + // + } + + sendFeedbackModeRequest(): void { + const generatorType = this.uniqueGenPrefix; + const mode = this.requestedMode; + // console.info(`expectedMode sent: ${`${generatorType} - ${mode.toString()}`}`); + this.bus.getPublisher().pub('expectedMode', { generatorType, mode }, true); + } + + sendFeedbackArmedDisplay(): void { + const generatorType = this.uniqueGenPrefix; + const status = this.failureGeneratorArmed; + // console.info(`ArmedDisplay sent: ${`${generatorType} - ${status.toString()}`}`); + this.bus.getPublisher().pub('armingDisplayStatus', { generatorType, status }, true); + } + + updateFailure(failureOrchestrator: FailuresOrchestrator): void { + const nbGenerator = Math.floor(this.settings.length / this.numberOfSettingsPerGenerator); + this.gs = SimVar.GetSimVarValue('GPS GROUND SPEED', 'Knots') || '0'; + this.loopStartAction(); + + if (this.requestedMode === undefined) { + this.requestedMode = []; + // console.info('DECLARE'); + } + + for (let i = this.previousNbGenerator; i < nbGenerator; i++) { + this.reset(i); + this.additionalGenInitActions(i); + this.requestedMode[i] = FailureGenMode.FailureGenAny; + // console.info('INIT'); + } + for (let i = 0; i < nbGenerator; i++) { + if ( + this.requestedMode[i] === FailureGenMode.FailureGenOff && + this.settings[i * this.numberOfSettingsPerGenerator + ArmingModeIndex] <= 0 + ) { + this.requestedMode[i] = FailureGenMode.FailureGenAny; + // console.info('REQUEST RESET'); + } + if (this.settings[i * this.numberOfSettingsPerGenerator + ArmingModeIndex] >= 0) { + if (this.previousArmingMode[i] !== this.settings[i * this.numberOfSettingsPerGenerator + ArmingModeIndex]) { + // console.info('RESETTING - ArmingModeChanged'); + this.reset(i); + } + if (this.waitForStopped[i] && this.gs < 1) { + this.waitForStopped[i] = false; + } + if ( + this.waitForTakeOff[i] && + !this.waitForStopped[i] && + this.randomFailuresGen.getFailureFlightPhase() === FailurePhases.TakeOff && + this.gs > 1 + ) { + this.waitForTakeOff[i] = false; + } + this.generatorSpecificActions(i); + if (this.failureGeneratorArmed[i]) { + if ( + this.settings[i * this.numberOfSettingsPerGenerator + ArmingModeIndex] === + FailureGenMode.FailureGenTakeOff && + this.gs < 1 + ) { + this.reset(i); + } else if (this.conditionToTriggerFailure(i)) { + const activeFailures = failureOrchestrator.getActiveFailures(); + const numberOfFailureToActivate = Math.min( + this.settings[i * this.numberOfSettingsPerGenerator + FailuresAtOnceIndex], + this.settings[i * this.numberOfSettingsPerGenerator + MaxFailuresIndex] - activeFailures.size, + ); + if (numberOfFailureToActivate > 0) { + // console.info('FAILURE'); + this.randomFailuresGen.activateRandomFailure( + this.getGeneratorFailurePool(failureOrchestrator, i), + failureOrchestrator, + activeFailures, + numberOfFailureToActivate, + ); + this.reset(i); + if ( + this.settings[i * this.numberOfSettingsPerGenerator + ArmingModeIndex] === FailureGenMode.FailureGenOnce + ) { + this.requestedMode[i] = FailureGenMode.FailureGenOff; + } + this.additionalFailureTriggeredActions(i); + } + } + } + if (!this.failureGeneratorArmed[i] && this.requestedMode[i] !== FailureGenMode.FailureGenOff) { + if ( + (this.settings[i * this.numberOfSettingsPerGenerator + ArmingModeIndex] === FailureGenMode.FailureGenOnce || + (this.settings[i * this.numberOfSettingsPerGenerator + ArmingModeIndex] === + FailureGenMode.FailureGenTakeOff && + !this.waitForTakeOff[i]) || + this.settings[i * this.numberOfSettingsPerGenerator + ArmingModeIndex] === + FailureGenMode.FailureGenRepeat) && + this.conditionToArm(i) + ) { + // console.info('ARMING'); + this.arm(i); + this.additionalArmingActions(i); + } + } else if ( + this.settings[i * this.numberOfSettingsPerGenerator + ArmingModeIndex] === FailureGenMode.FailureGenOff + ) { + // console.info('RESETTING - Generator is OFF'); + this.reset(i); + } + } else if (this.failureGeneratorArmed[i] || this.requestedMode[i] === FailureGenMode.FailureGenOff) { + // console.info('RESETTING - Generator removed'); + this.reset(i); + } + this.previousArmingMode[i] = this.settings[i * this.numberOfSettingsPerGenerator + ArmingModeIndex]; + this.additionalGenEndActions(i); + } + this.previousNbGenerator = nbGenerator; + let feedbackChange: boolean = false; + for (let i = 0; i < nbGenerator; i++) { + if (this.previousArmedState[i] !== this.failureGeneratorArmed[i]) { + feedbackChange = true; + } + } + if (feedbackChange || this.refreshRequest) { + this.sendFeedbackArmedDisplay(); + this.refreshRequest = false; + } + feedbackChange = false; + for (let i = 0; i < nbGenerator; i++) { + if (this.previousRequestedMode[i] !== this.requestedMode[i]) { + feedbackChange = true; + } + } + if (feedbackChange) { + this.sendFeedbackModeRequest(); + } + this.previousArmedState = Array.from(this.failureGeneratorArmed); + this.previousRequestedMode = Array.from(this.requestedMode); + this.previousNbGenerator = nbGenerator; + this.loopEndAction(); + } +} diff --git a/fbw-common/src/systems/shared/src/failures/PerHourFailureGenerator.ts b/fbw-common/src/systems/shared/src/failures/PerHourFailureGenerator.ts new file mode 100644 index 00000000000..12d3f37d7c2 --- /dev/null +++ b/fbw-common/src/systems/shared/src/failures/PerHourFailureGenerator.ts @@ -0,0 +1,36 @@ +// Copyright (c) 2021-2023 FlyByWire Simulations +// +// SPDX-License-Identifier: GPL-3.0 + +import { GenericGenerator } from './GenericGenerator'; + +export class FailureGeneratorPerHour extends GenericGenerator { + numberOfSettingsPerGenerator = 4; + + uniqueGenPrefix = 'C'; + + private timePrev: number = Date.now(); + + private currentTime: number = 0; + + private failurePerHourIndex = 3; + + loopStartAction(): void { + this.currentTime = Date.now(); + } + + conditionToTriggerFailure(genNumber: number): boolean { + const chanceSetting = this.settings[genNumber * this.numberOfSettingsPerGenerator + this.failurePerHourIndex]; + const chancePerSecond = chanceSetting / 3600; + const rollDice = Math.random(); + return rollDice < (chancePerSecond * (this.currentTime - this.timePrev)) / 1000; + } + + conditionToArm(_genNumber: number): boolean { + return true; + } + + loopEndAction(): void { + this.timePrev = this.currentTime; + } +} diff --git a/fbw-common/src/systems/shared/src/failures/RandomFailureGen.ts b/fbw-common/src/systems/shared/src/failures/RandomFailureGen.ts new file mode 100644 index 00000000000..e96ccdbc37d --- /dev/null +++ b/fbw-common/src/systems/shared/src/failures/RandomFailureGen.ts @@ -0,0 +1,99 @@ +// Copyright (c) 2021-2023 FlyByWire Simulations +// +// SPDX-License-Identifier: GPL-3.0 + +import { EventBus } from '@microsoft/msfs-sdk'; + +import { Failure, FailuresOrchestrator } from '@flybywiresim/fbw-sdk'; +import { GenericGenerator } from './GenericGenerator'; +import { FailureGeneratorAltitude } from './AltitudeFailureGenerator'; +import { FailureGeneratorPerHour } from './PerHourFailureGenerator'; +import { FailureGeneratorSpeed } from './SpeedFailureGenerator'; +import { FailureGeneratorTakeOff } from './TakeOffFailureGenerator'; +import { FailureGeneratorTimer } from './TimerFailureGenerator'; + +export const ArmingModeIndex = 0; +export const FailuresAtOnceIndex = 1; +export const MaxFailuresIndex = 2; + +export enum FailurePhases { + Dormant, + TakeOff, + InitialClimb, + Flight, +} +export class RandomFailureGen { + failureGenerators: GenericGenerator[]; + + constructor(private readonly bus: EventBus) { + this.failureGenerators = [ + new FailureGeneratorAltitude(this, this.bus), + new FailureGeneratorSpeed(this, this.bus), + new FailureGeneratorTimer(this, this.bus), + new FailureGeneratorPerHour(this, this.bus), + new FailureGeneratorTakeOff(this, this.bus), + ]; + } + + absoluteTimePrev: number = Date.now(); + + flatten(settings: number[]): string { + let settingString = ''; + for (let i = 0; i < settings.length; i++) { + settingString += settings[i].toString(); + if (i < settings.length - 1) settingString += ','; + } + return settingString; + } + + activateRandomFailure( + failureList: readonly Readonly[], + failureOrchestrator: FailuresOrchestrator, + activeFailures: Set, + failuresAtOnce: number, + ) { + let failuresOffMap = failureList + .filter((failure) => !activeFailures.has(failure.identifier)) + .map((failure) => failure.identifier); + const maxNumber = Math.min(failuresAtOnce, failuresOffMap.length); + for (let i = 0; i < maxNumber; i++) { + if (failuresOffMap.length > 0) { + const pick = Math.floor(Math.random() * failuresOffMap.length); + const failureIdentifierPicked = failuresOffMap[pick]; + const pickedFailure = failureList.find((failure) => failure.identifier === failureIdentifierPicked); + if (pickedFailure) { + failureOrchestrator.activate(failureIdentifierPicked); + failuresOffMap = failuresOffMap.filter((identifier) => identifier !== failureIdentifierPicked); + } + } + } + } + + private failureFlightPhase: FailurePhases; + + getFailureFlightPhase(): FailurePhases { + return this.failureFlightPhase; + } + + basicDataUpdate(): void { + const isOnGround = SimVar.GetSimVarValue('SIM ON GROUND', 'Bool'); + const maxThrottleMode = Math.max(Simplane.getEngineThrottleMode(0), Simplane.getEngineThrottleMode(1)); + const throttleTakeOff = maxThrottleMode === ThrottleMode.FLEX_MCT || maxThrottleMode === ThrottleMode.TOGA; + if (isOnGround) { + if (throttleTakeOff) this.failureFlightPhase = FailurePhases.TakeOff; + else this.failureFlightPhase = FailurePhases.Dormant; + } else if (throttleTakeOff) this.failureFlightPhase = FailurePhases.InitialClimb; + else this.failureFlightPhase = FailurePhases.TakeOff; + } + + update(failureOrchestrator: FailuresOrchestrator) { + const absoluteTime = Date.now(); + if (absoluteTime - this.absoluteTimePrev >= 100.0) { + this.basicDataUpdate(); + for (let i = 0; i < this.failureGenerators.length; i++) { + this.failureGenerators[i].updateFailure(failureOrchestrator); + } + this.absoluteTimePrev = absoluteTime; + } + } +} diff --git a/fbw-common/src/systems/shared/src/failures/SpeedFailureGenerator.ts b/fbw-common/src/systems/shared/src/failures/SpeedFailureGenerator.ts new file mode 100644 index 00000000000..fd11ba0177b --- /dev/null +++ b/fbw-common/src/systems/shared/src/failures/SpeedFailureGenerator.ts @@ -0,0 +1,72 @@ +// Copyright (c) 2021-2023 FlyByWire Simulations +// +// SPDX-License-Identifier: GPL-3.0 + +import { GenericGenerator } from './GenericGenerator'; + +enum Direction { + Acceleration = 0, + Deceleration = 1, +} + +export class FailureGeneratorSpeed extends GenericGenerator { + numberOfSettingsPerGenerator = 6; + + uniqueGenPrefix = 'B'; + + private rolledDice: number[] = []; + + private previousSpeedCondition: number[] = []; + + private speedConditionIndex = 3; + + private speedMinIndex = 4; + + private speedMaxIndex = 5; + + private resetMargin = 5; + + additionalInitActions(genNumber: number): void { + this.previousSpeedCondition[genNumber] = + this.settings[genNumber * this.numberOfSettingsPerGenerator + this.speedConditionIndex]; + } + + generatorSpecificActions(genNumber: number): void { + const speedCondition = this.settings[genNumber * this.numberOfSettingsPerGenerator + this.speedConditionIndex]; + + if (this.previousSpeedCondition[genNumber] !== speedCondition) { + this.disarm(genNumber); + } + } + + conditionToTriggerFailure(genNumber: number): boolean { + const speedMax = this.settings[genNumber * this.numberOfSettingsPerGenerator + this.speedMaxIndex]; + const speedMin = this.settings[genNumber * this.numberOfSettingsPerGenerator + this.speedMinIndex]; + const speedCondition = this.settings[genNumber * this.numberOfSettingsPerGenerator + this.speedConditionIndex]; + const failureSpeed = speedMin + this.rolledDice[genNumber] * (speedMax - speedMin); + // console.info(`${this.gs}/${failureSpeed.toString()}`); + return ( + (this.gs > failureSpeed && speedCondition === Direction.Acceleration) || + (this.gs < failureSpeed && speedCondition === Direction.Deceleration) + ); + } + + conditionToArm(genNumber: number): boolean { + const speedMax = this.settings[genNumber * this.numberOfSettingsPerGenerator + this.speedMaxIndex]; + const speedMin = this.settings[genNumber * this.numberOfSettingsPerGenerator + this.speedMinIndex]; + const speedCondition = this.settings[genNumber * this.numberOfSettingsPerGenerator + this.speedConditionIndex]; + return ( + (this.gs < speedMin - this.resetMargin && speedCondition === Direction.Acceleration) || + (this.gs > speedMax + this.resetMargin && speedCondition === Direction.Deceleration) + ); + } + + additionalArmingActions(genNumber: number): void { + this.rolledDice[genNumber] = Math.random(); + } + + additionalGenEndActions(genNumber: number): void { + this.previousSpeedCondition[genNumber] = + this.settings[genNumber * this.numberOfSettingsPerGenerator + this.speedConditionIndex]; + } +} diff --git a/fbw-common/src/systems/shared/src/failures/TakeOffFailureGenerator.ts b/fbw-common/src/systems/shared/src/failures/TakeOffFailureGenerator.ts new file mode 100644 index 00000000000..e4f5f26761b --- /dev/null +++ b/fbw-common/src/systems/shared/src/failures/TakeOffFailureGenerator.ts @@ -0,0 +1,99 @@ +// Copyright (c) 2021-2023 FlyByWire Simulations +// +// SPDX-License-Identifier: GPL-3.0 + +import { GenericGenerator } from './GenericGenerator'; + +export class FailureGeneratorTakeOff extends GenericGenerator { + numberOfSettingsPerGenerator = 10; + + uniqueGenPrefix = 'E'; + + private failureTakeOffSpeedThreshold: number[] = []; + + private failureTakeOffAltitudeThreshold: number[] = []; + + private failureTakeOffAltitudeEnd: number[] = []; + + private rolledDiceTakeOff = 0; + + private chancePerTakeOffIndex = 3; + + private chanceLowIndex = 4; + + private chanceMediumIndex = 5; + + private minSpeedIndex = 6; + + private mediumSpeedIndex = 7; + + private maxSpeedIndex = 8; + + private altitudeIndex = 9; + + private altitude: number = 0; + + loopStartAction(): void { + this.altitude = SimVar.GetSimVarValue('INDICATED ALTITUDE', 'Feet') || '0'; + } + + generatorSpecificActions(genNumber: number): void { + const medHighTakeOffSpeed: number = + this.settings[genNumber * this.numberOfSettingsPerGenerator + this.maxSpeedIndex]; + if ( + this.failureGeneratorArmed[genNumber] && + ((this.altitude >= this.failureTakeOffAltitudeEnd[genNumber] && this.gs >= medHighTakeOffSpeed) || this.gs < 1) + ) { + this.reset(genNumber); + } + } + + conditionToTriggerFailure(genNumber: number): boolean { + return ( + this.rolledDiceTakeOff < + this.settings[genNumber * this.numberOfSettingsPerGenerator + this.chancePerTakeOffIndex] && + ((this.altitude >= this.failureTakeOffAltitudeThreshold[genNumber] && + this.failureTakeOffAltitudeThreshold[genNumber] !== -1) || + (this.gs >= this.failureTakeOffSpeedThreshold[genNumber] && + this.failureTakeOffSpeedThreshold[genNumber] !== -1)) + ); + } + + conditionToArm(genNumber: number): boolean { + return !this.waitForTakeOff[genNumber]; + } + + additionalArmingActions(genNumber: number): void { + const minFailureTakeOffSpeed: number = + this.settings[genNumber * this.numberOfSettingsPerGenerator + this.minSpeedIndex]; + const medHighTakeOffSpeed: number = + this.settings[genNumber * this.numberOfSettingsPerGenerator + this.maxSpeedIndex]; + const chanceFailureLowTakeOffRegime: number = + this.settings[genNumber * this.numberOfSettingsPerGenerator + this.chanceLowIndex]; + const chanceFailureMediumTakeOffRegime: number = + this.settings[genNumber * this.numberOfSettingsPerGenerator + this.chanceMediumIndex]; + const takeOffDeltaAltitudeEnd: number = + 100 * this.settings[genNumber * this.numberOfSettingsPerGenerator + this.altitudeIndex]; + const lowMedTakeOffSpeed: number = + this.settings[genNumber * this.numberOfSettingsPerGenerator + this.mediumSpeedIndex]; + this.rolledDiceTakeOff = Math.random(); + const rolledDicePhase = Math.random(); + if (rolledDicePhase < chanceFailureLowTakeOffRegime) { + // Low Speed Take Off + const temp = Math.random() * (lowMedTakeOffSpeed - minFailureTakeOffSpeed) + minFailureTakeOffSpeed; + this.failureTakeOffAltitudeThreshold[genNumber] = -1; + this.failureTakeOffSpeedThreshold[genNumber] = temp; + } else if (rolledDicePhase < chanceFailureMediumTakeOffRegime + chanceFailureLowTakeOffRegime) { + // Medium Speed Take Off + const temp = Math.random() * (medHighTakeOffSpeed - lowMedTakeOffSpeed) + lowMedTakeOffSpeed; + this.failureTakeOffAltitudeThreshold[genNumber] = -1; + this.failureTakeOffSpeedThreshold[genNumber] = temp; + } else { + // High Speed Take Off + const temp = this.altitude + 10 + Math.random() * takeOffDeltaAltitudeEnd; + this.failureTakeOffAltitudeThreshold[genNumber] = temp; + this.failureTakeOffSpeedThreshold[genNumber] = -1; + } + this.failureTakeOffAltitudeEnd[genNumber] = this.altitude + takeOffDeltaAltitudeEnd; + } +} diff --git a/fbw-common/src/systems/shared/src/failures/TimerFailureGenerator.ts b/fbw-common/src/systems/shared/src/failures/TimerFailureGenerator.ts new file mode 100644 index 00000000000..ab17747f5d0 --- /dev/null +++ b/fbw-common/src/systems/shared/src/failures/TimerFailureGenerator.ts @@ -0,0 +1,41 @@ +// Copyright (c) 2021-2023 FlyByWire Simulations +// +// SPDX-License-Identifier: GPL-3.0 + +import { GenericGenerator } from './GenericGenerator'; + +export class FailureGeneratorTimer extends GenericGenerator { + numberOfSettingsPerGenerator = 5; + + uniqueGenPrefix = 'D'; + + private failureStartTime: number[] = []; + + private rolledDice: number[] = []; + + private delayMinIndex = 3; + + private delayMaxIndex = 4; + + private currentTime: number = 0; + + loopStartAction(): void { + this.currentTime = Date.now(); + } + + conditionToTriggerFailure(genNumber: number): boolean { + const timerMax = this.settings[genNumber * this.numberOfSettingsPerGenerator + this.delayMaxIndex] * 1000; + const timerMin = this.settings[genNumber * this.numberOfSettingsPerGenerator + this.delayMinIndex] * 1000; + const failureDelay = timerMin + this.rolledDice[genNumber] * (timerMax - timerMin); + return this.currentTime > this.failureStartTime[genNumber] + failureDelay; + } + + conditionToArm(_genNumber: number): boolean { + return true; + } + + additionalArmingActions(genNumber: number): void { + this.rolledDice[genNumber] = Math.random(); + this.failureStartTime[genNumber] = this.currentTime; + } +} diff --git a/fbw-common/src/systems/shared/src/failures/failures-orchestrator.spec.ts b/fbw-common/src/systems/shared/src/failures/failures-orchestrator.spec.ts index d4b6b894611..bb57798fefa 100644 --- a/fbw-common/src/systems/shared/src/failures/failures-orchestrator.spec.ts +++ b/fbw-common/src/systems/shared/src/failures/failures-orchestrator.spec.ts @@ -1,4 +1,8 @@ -import { FailuresOrchestrator } from '.'; +// Copyright (c) 2021-2023 FlyByWire Simulations +// +// SPDX-License-Identifier: GPL-3.0 + +import { FailuresOrchestrator } from './failures-orchestrator'; import { getActivateFailureSimVarName, getDeactivateFailureSimVarName } from './sim-vars'; import { flushPromises } from './test-functions'; @@ -86,7 +90,7 @@ const identifier = 123; const name = 'test'; function orchestrator() { - return new FailuresOrchestrator(prefix, [[0, identifier, name]]); + return new FailuresOrchestrator(null, prefix, [[0, identifier, name]]); } function activateFailure(o: FailuresOrchestrator) { diff --git a/fbw-common/src/systems/shared/src/failures/failures-orchestrator.ts b/fbw-common/src/systems/shared/src/failures/failures-orchestrator.ts index b00463432d2..47bd3d36b55 100644 --- a/fbw-common/src/systems/shared/src/failures/failures-orchestrator.ts +++ b/fbw-common/src/systems/shared/src/failures/failures-orchestrator.ts @@ -2,7 +2,11 @@ // // SPDX-License-Identifier: GPL-3.0 -import { AtaChapterNumber } from '../ata'; +import { EventBus } from '@microsoft/msfs-sdk'; + +import { AtaChapterNumber } from '@flybywiresim/fbw-sdk'; + +import { RandomFailureGen } from './RandomFailureGen'; import { QueuedSimVarWriter, SimVarReaderWriter } from './communication'; import { getActivateFailureSimVarName, getDeactivateFailureSimVarName } from './sim-vars'; @@ -12,8 +16,6 @@ export interface Failure { name: string; } -export type FailureDefinition = [AtaChapterNumber, number, string]; - /** * Orchestrates the activation and deactivation of failures. * @@ -22,6 +24,8 @@ export type FailureDefinition = [AtaChapterNumber, number, string]; export class FailuresOrchestrator { private failures: Failure[] = []; + private randomFailureGe: RandomFailureGen; + private activeFailures = new Set(); private changingFailures = new Set(); @@ -30,7 +34,8 @@ export class FailuresOrchestrator { private deactivateFailureQueue: QueuedSimVarWriter; - constructor(simVarPrefix: string, failures: FailureDefinition[]) { + constructor(bus: EventBus, simVarPrefix: string, failures: [AtaChapterNumber, number, string][]) { + this.randomFailureGe = new RandomFailureGen(bus); this.activateFailureQueue = new QueuedSimVarWriter( new SimVarReaderWriter(getActivateFailureSimVarName(simVarPrefix)), ); @@ -47,6 +52,7 @@ export class FailuresOrchestrator { } update() { + this.randomFailureGe.update(this); this.activateFailureQueue.update(); this.deactivateFailureQueue.update(); } diff --git a/igniter.config.mjs b/igniter.config.mjs index 20687bfd309..04a7825cf3a 100644 --- a/igniter.config.mjs +++ b/igniter.config.mjs @@ -57,12 +57,6 @@ export default new TaskOfTasks("all", [ 'fbw-a32nx/out/flybywire-aircraft-a320-neo/html_ui/Pages/VCockpit/Instruments/A32NX/ExtrasHost' ] ), - new ExecTask("failures", - "npm run build-a32nx:failures", - [ - "fbw-a32nx/src/systems/failures", - "fbw-a32nx/out/flybywire-aircraft-a320-neo/html_ui/JS/fbw-a32nx/failures/failures.js" - ]), new ExecTask("fmgc", "npm run build-a32nx:fmgc", [ diff --git a/jest.config.js b/jest.config.js index 56db75ae449..1ab3c3a7fc2 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,17 +1,45 @@ /** @type {import('@ts-jest/dist/types').InitialOptionsTsJest} */ + +const esModules = ['@microsoft/msfs-sdk'].join('|'); module.exports = { preset: 'ts-jest', + testEnvironment: 'node', + transform: { + '\\.[j]s?$': 'ts-jest', + }, + + transformIgnorePatterns: [`/node_modules/(?!${esModules})`], + setupFilesAfterEnv: ['./fbw-common/src/jest/setupJestMock.js'], globals: { 'ts-jest': { // Babel assumes isolated modules, therefore enable it here as well. // This also speeds up the unit testing performance. isolatedModules: true, - diagnostics: { ignoreCodes: ['TS151001'] }, + diagnostics: { + ignoreCodes: ['TS151001'], + }, + tsconfig: { + jsx: 'react', + typeRoots: ['/fbw-common/src/typings', '/node_modules/@types'], + moduleResolution: 'node', + allowSyntheticDefaultImports: true, + allowJs: true, + esModuleInterop: true, + }, }, }, + // disable fmsv2 tests until they are fixed modulePathIgnorePatterns: ['fbw-a380x/src/systems/fmgc/src/flightplanning'], - moduleNameMapper: { '@flybywiresim/fbw-sdk': '/fbw-common/src/systems/index.ts' }, + + moduleNameMapper: { + '@flybywiresim/fbw-sdk': '/fbw-common/src/systems/index.ts', + + '@fmgc/types/fstypes/FSEnums': '/fbw-a32nx/src/systems/fmgc/src/types/fstypes/FSEnums.ts', + '@shared/autopilot': ['/fbw-a32nx/src/systems/shared/src/autopilot.ts'], + '@shared/logic': '/fbw-a32nx/src/systems/shared/src/logic.ts', + '@shared/flightphase': '/fbw-a32nx/src/systems/shared/src/flightphase.ts', + }, }; diff --git a/package.json b/package.json index b0c7a27be95..62f594b0642 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,6 @@ "build-a32nx:atsu-common": "node fbw-a32nx/src/systems/atsu/common/build.js", "build-a32nx:atsu-fms-client": "node fbw-a32nx/src/systems/atsu/fmsclient/build.js", "build-a32nx:extras-host": "node fbw-a32nx/src/systems/extras-host/build.js", - "build-a32nx:failures": "rollup -c fbw-a32nx/src/systems/failures/rollup.config.js", "build-a32nx:fmgc": "node fbw-a32nx/src/systems/fmgc/build.js", "build-a32nx:instruments": "mach build --config fbw-a32nx/mach.config.js --work-in-config-dir", "build-a32nx:sentry-client": "node fbw-a32nx/src/systems/sentry-client/build.js", @@ -52,7 +51,7 @@ "serve:efb": "cd fbw-a32nx/src/systems/instruments/src/EFB/ && vite --port 9696", "build:instruments": "rollup --max-old-space-size=8192 -c src/systems/instruments/buildSrc/simulatorBuild.mjs", "watch:instruments": "rollup --max-old-space-size=8192 -wc src/systems/instruments/buildSrc/simulatorBuild.mjs", - + "====== A380 =================": "==========================================", "build-a380x:copy-base-files": "mkdir -p fbw-a380x/out/flybywire-aircraft-a380-842 && (rsync -a fbw-a380x/src/base/flybywire-aircraft-a380-842 fbw-a380x/out/ || cp -a -u fbw-a380x/src/base/flybywire-aircraft-a380-842 fbw-a380x/out/)", @@ -75,7 +74,7 @@ "build-ingamepanels-checklist-fix:copy-base-package": "mkdir -p fbw-ingamepanels-checklist-fix/out/flybywire-ingamepanels-checklist-fix && (rsync -a fbw-ingamepanels-checklist-fix/src/base/flybywire-ingamepanels-checklist-fix fbw-ingamepanels-checklist-fix/out/ || cp -a -u fbw-ingamepanels-checklist-fix/src/base/flybywire-ingamepanels-checklist-fix fbw-ingamepanels-checklist-fix/out/)", "build-ingamepanels-checklist-fix:copy-base-files": "npm run build-ingamepanels-checklist-fix:copy-base-package", "build-ingamepanels-checklist-fix:manifest": "node scripts/build_ingamepanels_checklist_fix.js", - + "====== COMMON ================": "==========================================", "lint": "eslint --cache **/*.{js,mjs,jsx,ts,tsx}",
{t('Failures.FullSimulationOfTheFailuresBelowIsntYetGuaranteed')}
{bodyText}