UNPKG

@epicgames-ps/lib-pixelstreamingfrontend-ue5.4

Version:
999 lines (917 loc) 35.5 kB
// Copyright Epic Games, Inc. All Rights Reserved. import { Logger } from '../Logger/Logger'; import { SettingFlag } from './SettingFlag'; import { SettingNumber } from './SettingNumber'; import { SettingText } from './SettingText'; import { SettingOption } from './SettingOption'; import { EventEmitter, SettingsChangedEvent } from '../Util/EventEmitter'; import { SettingBase } from './SettingBase'; /** * A collection of flags that can be toggled and are core to all Pixel Streaming experiences. * These are used in the `Config.Flags` map. */ export class Flags { static AutoConnect = 'AutoConnect' as const; static AutoPlayVideo = 'AutoPlayVideo' as const; static AFKDetection = 'TimeoutIfIdle' as const; static BrowserSendOffer = 'OfferToReceive' as const; static HoveringMouseMode = 'HoveringMouse' as const; static ForceMonoAudio = 'ForceMonoAudio' as const; static ForceTURN = 'ForceTURN' as const; static FakeMouseWithTouches = 'FakeMouseWithTouches' as const; static IsQualityController = 'ControlsQuality' as const; static MatchViewportResolution = 'MatchViewportRes' as const; static StartVideoMuted = 'StartVideoMuted' as const; static SuppressBrowserKeys = 'SuppressBrowserKeys' as const; static UseMic = 'UseMic' as const; static KeyboardInput = 'KeyboardInput' as const; static MouseInput = 'MouseInput' as const; static TouchInput = 'TouchInput' as const; static GamepadInput = 'GamepadInput' as const; static XRControllerInput = 'XRControllerInput' as const; static WaitForStreamer = 'WaitForStreamer' as const; static HideUI = 'HideUI' as const; } export type FlagsKeys = Exclude<keyof typeof Flags, 'prototype'>; export type FlagsIds = typeof Flags[FlagsKeys]; const isFlagId = (id: string): id is FlagsIds => Object.getOwnPropertyNames(Flags).some( (name: FlagsKeys) => Flags[name] === id ); /** * A collection of numeric parameters that are core to all Pixel Streaming experiences. * */ export class NumericParameters { static AFKTimeoutSecs = 'AFKTimeout' as const; static AFKCountdownSecs = 'AFKCountdown' as const; static MinQP = 'MinQP' as const; static MaxQP = 'MaxQP' as const; static WebRTCFPS = 'WebRTCFPS' as const; static WebRTCMinBitrate = 'WebRTCMinBitrate' as const; static WebRTCMaxBitrate = 'WebRTCMaxBitrate' as const; static MaxReconnectAttempts = 'MaxReconnectAttempts' as const; static StreamerAutoJoinInterval = 'StreamerAutoJoinInterval' as const; } export type NumericParametersKeys = Exclude< keyof typeof NumericParameters, 'prototype' >; export type NumericParametersIds = typeof NumericParameters[NumericParametersKeys]; const isNumericId = (id: string): id is NumericParametersIds => Object.getOwnPropertyNames(NumericParameters).some( (name: NumericParametersKeys) => NumericParameters[name] === id ); /** * A collection of textual parameters that are core to all Pixel Streaming experiences. * */ export class TextParameters { static SignallingServerUrl = 'ss' as const; } export type TextParametersKeys = Exclude< keyof typeof TextParameters, 'prototype' >; export type TextParametersIds = typeof TextParameters[TextParametersKeys]; const isTextId = (id: string): id is TextParametersIds => Object.getOwnPropertyNames(TextParameters).some( (name: TextParametersKeys) => TextParameters[name] === id ); /** * A collection of enum based parameters that are core to all Pixel Streaming experiences. * */ export class OptionParameters { static PreferredCodec = 'PreferredCodec' as const; static StreamerId = 'StreamerId' as const; } export type OptionParametersKeys = Exclude< keyof typeof OptionParameters, 'prototype' >; export type OptionParametersIds = typeof OptionParameters[OptionParametersKeys]; const isOptionId = (id: string): id is OptionParametersIds => Object.getOwnPropertyNames(OptionParameters).some( (name: OptionParametersKeys) => OptionParameters[name] === id ); /** * Utility types for inferring data type based on setting ID */ export type OptionIds = | FlagsIds | NumericParametersIds | TextParametersIds | OptionParametersIds; export type OptionKeys<T> = T extends FlagsIds ? boolean : T extends NumericParametersIds ? number : T extends TextParametersIds ? string : T extends OptionParametersIds ? string : never; export type AllSettings = { [K in OptionIds]: OptionKeys<K>; }; export interface ConfigParams { /** Initial Pixel Streaming settings */ initialSettings?: Partial<AllSettings>; /** If useUrlParams is set true, will read initial values from URL parameters and persist changed settings into URL */ useUrlParams?: boolean; } export class Config { /* A map of flags that can be toggled - options that can be set in the application - e.g. Use Mic? */ private flags = new Map<FlagsIds, SettingFlag>(); /* A map of numerical settings - options that can be in the application - e.g. MinBitrate */ private numericParameters = new Map<NumericParametersIds, SettingNumber>(); /* A map of text settings - e.g. signalling server url */ private textParameters = new Map<TextParametersIds, SettingText>(); /* A map of enum based settings - e.g. preferred codec */ private optionParameters = new Map<OptionParametersIds, SettingOption>(); private _useUrlParams: boolean; // ------------ Settings ----------------- constructor(config: ConfigParams = {}) { const { initialSettings, useUrlParams } = config; this._useUrlParams = !!useUrlParams; this.populateDefaultSettings(this._useUrlParams, initialSettings); } /** * True if reading configuration initial values from URL parameters, and * persisting changes in URL when changed. */ public get useUrlParams() { return this._useUrlParams; } /** * Populate the default settings for a Pixel Streaming application */ private populateDefaultSettings(useUrlParams: boolean, settings: Partial<AllSettings>): void { /** * Text Parameters */ this.textParameters.set( TextParameters.SignallingServerUrl, new SettingText( TextParameters.SignallingServerUrl, 'Signalling url', 'Url of the signalling server', settings && settings.hasOwnProperty(TextParameters.SignallingServerUrl) ? settings[TextParameters.SignallingServerUrl] : (location.protocol === 'https:' ? 'wss://' : 'ws://') + window.location.hostname + // for readability, we omit the port if it's 80 (window.location.port === '80' || window.location.port === '' ? '' : `:${window.location.port}`), useUrlParams ) ); this.optionParameters.set( OptionParameters.StreamerId, new SettingOption( OptionParameters.StreamerId, 'Streamer ID', 'The ID of the streamer to stream.', settings && settings.hasOwnProperty(OptionParameters.StreamerId) ? settings[OptionParameters.StreamerId] : '', [], useUrlParams ) ); /** * Enum Parameters */ this.optionParameters.set( OptionParameters.PreferredCodec, new SettingOption( OptionParameters.PreferredCodec, 'Preferred Codec', 'The preferred codec to be used during codec negotiation', 'H264 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f', settings && settings.hasOwnProperty(OptionParameters.PreferredCodec) ? [settings[OptionParameters.PreferredCodec]] : (function (): Array<string> { const browserSupportedCodecs: Array<string> = []; // Try get the info needed from the RTCRtpReceiver. This is only available on chrome if (!RTCRtpReceiver.getCapabilities) { browserSupportedCodecs.push('Only available on Chrome'); return browserSupportedCodecs; } const matcher = /(VP\d|H26\d|AV1).*/; const codecs = RTCRtpReceiver.getCapabilities('video').codecs; codecs.forEach((codec) => { const str = codec.mimeType.split('/')[1] + ' ' + (codec.sdpFmtpLine || ''); const match = matcher.exec(str); if (match !== null) { browserSupportedCodecs.push(str); } }); return browserSupportedCodecs; })(), useUrlParams ) ); /** * Boolean parameters */ this.flags.set( Flags.AutoConnect, new SettingFlag( Flags.AutoConnect, 'Auto connect to stream', 'Whether we should attempt to auto connect to the signalling server or show a click to start prompt.', settings && settings.hasOwnProperty(Flags.AutoConnect) ? settings[Flags.AutoConnect] : false, useUrlParams ) ); this.flags.set( Flags.AutoPlayVideo, new SettingFlag( Flags.AutoPlayVideo, 'Auto play video', 'When video is ready automatically start playing it as opposed to showing a play button.', settings && settings.hasOwnProperty(Flags.AutoPlayVideo) ? settings[Flags.AutoPlayVideo] : true, useUrlParams ) ); this.flags.set( Flags.BrowserSendOffer, new SettingFlag( Flags.BrowserSendOffer, 'Browser send offer', 'Browser will initiate the WebRTC handshake by sending the offer to the streamer', settings && settings.hasOwnProperty(Flags.BrowserSendOffer) ? settings[Flags.BrowserSendOffer] : false, useUrlParams ) ); this.flags.set( Flags.UseMic, new SettingFlag( Flags.UseMic, 'Use microphone', 'Make browser request microphone access and open an input audio track.', settings && settings.hasOwnProperty(Flags.UseMic) ? settings[Flags.UseMic] : false, useUrlParams ) ); this.flags.set( Flags.StartVideoMuted, new SettingFlag( Flags.StartVideoMuted, 'Start video muted', 'Video will start muted if true.', settings && settings.hasOwnProperty(Flags.StartVideoMuted) ? settings[Flags.StartVideoMuted] : false, useUrlParams ) ); this.flags.set( Flags.SuppressBrowserKeys, new SettingFlag( Flags.SuppressBrowserKeys, 'Suppress browser keys', 'Suppress certain browser keys that we use in UE, for example F5 to show shader complexity instead of refresh the page.', settings && settings.hasOwnProperty(Flags.SuppressBrowserKeys) ? settings[Flags.SuppressBrowserKeys] : true, useUrlParams ) ); this.flags.set( Flags.IsQualityController, new SettingFlag( Flags.IsQualityController, 'Is quality controller?', 'True if this peer controls stream quality', settings && settings.hasOwnProperty(Flags.IsQualityController) ? settings[Flags.IsQualityController] : true, useUrlParams ) ); this.flags.set( Flags.ForceMonoAudio, new SettingFlag( Flags.ForceMonoAudio, 'Force mono audio', 'Force browser to request mono audio in the SDP', settings && settings.hasOwnProperty(Flags.ForceMonoAudio) ? settings[Flags.ForceMonoAudio] : false, useUrlParams ) ); this.flags.set( Flags.ForceTURN, new SettingFlag( Flags.ForceTURN, 'Force TURN', 'Only generate TURN/Relayed ICE candidates.', settings && settings.hasOwnProperty(Flags.ForceTURN) ? settings[Flags.ForceTURN] : false, useUrlParams ) ); this.flags.set( Flags.AFKDetection, new SettingFlag( Flags.AFKDetection, 'AFK if idle', 'Timeout the experience if user is AFK for a period.', settings && settings.hasOwnProperty(Flags.AFKDetection) ? settings[Flags.AFKDetection] : false, useUrlParams ) ); this.flags.set( Flags.MatchViewportResolution, new SettingFlag( Flags.MatchViewportResolution, 'Match viewport resolution', 'Pixel Streaming will be instructed to dynamically resize the video stream to match the size of the video element.', settings && settings.hasOwnProperty(Flags.MatchViewportResolution) ? settings[Flags.MatchViewportResolution] : false, useUrlParams ) ); this.flags.set( Flags.HoveringMouseMode, new SettingFlag( Flags.HoveringMouseMode, 'Control Scheme: Locked Mouse', 'Either locked mouse, where the pointer is consumed by the video and locked to it, or hovering mouse, where the mouse is not consumed.', settings && settings.hasOwnProperty(Flags.HoveringMouseMode) ? settings[Flags.HoveringMouseMode] : false, useUrlParams, (isHoveringMouse: boolean, setting: SettingBase) => { setting.label = `Control Scheme: ${isHoveringMouse ? 'Hovering' : 'Locked'} Mouse`; } ) ); this.flags.set( Flags.FakeMouseWithTouches, new SettingFlag( Flags.FakeMouseWithTouches, 'Fake mouse with touches', 'A single finger touch is converted into a mouse event. This allows a non-touch application to be controlled partially via a touch device.', settings && settings.hasOwnProperty(Flags.FakeMouseWithTouches) ? settings[Flags.FakeMouseWithTouches] : false, useUrlParams ) ); this.flags.set( Flags.KeyboardInput, new SettingFlag( Flags.KeyboardInput, 'Keyboard input', 'If enabled, send keyboard events to streamer', settings && settings.hasOwnProperty(Flags.KeyboardInput) ? settings[Flags.KeyboardInput] : true, useUrlParams ) ); this.flags.set( Flags.MouseInput, new SettingFlag( Flags.MouseInput, 'Mouse input', 'If enabled, send mouse events to streamer', settings && settings.hasOwnProperty(Flags.MouseInput) ? settings[Flags.MouseInput] : true, useUrlParams ) ); this.flags.set( Flags.TouchInput, new SettingFlag( Flags.TouchInput, 'Touch input', 'If enabled, send touch events to streamer', settings && settings.hasOwnProperty(Flags.TouchInput) ? settings[Flags.TouchInput] : true, useUrlParams ) ); this.flags.set( Flags.GamepadInput, new SettingFlag( Flags.GamepadInput, 'Gamepad input', 'If enabled, send gamepad events to streamer', settings && settings.hasOwnProperty(Flags.GamepadInput) ? settings[Flags.GamepadInput] : true, useUrlParams ) ); this.flags.set( Flags.XRControllerInput, new SettingFlag( Flags.XRControllerInput, 'XR controller input', 'If enabled, send XR controller events to streamer', settings && settings.hasOwnProperty(Flags.XRControllerInput) ? settings[Flags.XRControllerInput] : true, useUrlParams ) ); this.flags.set( Flags.WaitForStreamer, new SettingFlag( Flags.WaitForStreamer, 'Wait for streamer', 'Will continue trying to connect to the first streamer available.', settings && settings.hasOwnProperty(Flags.WaitForStreamer) ? settings[Flags.WaitForStreamer] : true, useUrlParams ) ); this.flags.set( Flags.HideUI, new SettingFlag( Flags.HideUI, 'Hide the UI overlay', 'Will hide all UI overlay details', settings && settings.hasOwnProperty(Flags.HideUI) ? settings[Flags.HideUI] : false, useUrlParams ) ); /** * Numeric parameters */ this.numericParameters.set( NumericParameters.AFKTimeoutSecs, new SettingNumber( NumericParameters.AFKTimeoutSecs, 'AFK timeout', 'The time (in seconds) it takes for the application to time out if AFK timeout is enabled.', 0 /*min*/, null /*max*/, settings && settings.hasOwnProperty(NumericParameters.AFKTimeoutSecs) ? settings[NumericParameters.AFKTimeoutSecs] : 120, /*value*/ useUrlParams ) ); this.numericParameters.set( NumericParameters.AFKCountdownSecs, new SettingNumber( NumericParameters.AFKCountdownSecs, 'AFK countdown', 'The time (in seconds) for a user to respond before the stream is ended after an AFK timeout.', 10 /*min*/, null /*max*/, 10 /*value*/, useUrlParams ) ) this.numericParameters.set( NumericParameters.MaxReconnectAttempts, new SettingNumber( NumericParameters.MaxReconnectAttempts, 'Max Reconnects', 'Maximum number of reconnects the application will attempt when a streamer disconnects.', 0 /*min*/, 999 /*max*/, settings && settings.hasOwnProperty(NumericParameters.MaxReconnectAttempts) ? settings[NumericParameters.MaxReconnectAttempts] : 3, /*value*/ useUrlParams ) ); this.numericParameters.set( NumericParameters.MinQP, new SettingNumber( NumericParameters.MinQP, 'Min QP', 'The lower bound for the quantization parameter (QP) of the encoder. 0 = Best quality, 51 = worst quality.', 0 /*min*/, 51 /*max*/, settings && settings.hasOwnProperty(NumericParameters.MinQP) ? settings[NumericParameters.MinQP] : 0, /*value*/ useUrlParams ) ); this.numericParameters.set( NumericParameters.MaxQP, new SettingNumber( NumericParameters.MaxQP, 'Max QP', 'The upper bound for the quantization parameter (QP) of the encoder. 0 = Best quality, 51 = worst quality.', 0 /*min*/, 51 /*max*/, settings && settings.hasOwnProperty(NumericParameters.MaxQP) ? settings[NumericParameters.MaxQP] : 51, /*value*/ useUrlParams ) ); this.numericParameters.set( NumericParameters.WebRTCFPS, new SettingNumber( NumericParameters.WebRTCFPS, 'Max FPS', 'The maximum FPS that WebRTC will try to transmit frames at.', 1 /*min*/, 999 /*max*/, settings && settings.hasOwnProperty(NumericParameters.WebRTCFPS) ? settings[NumericParameters.WebRTCFPS] : 60, /*value*/ useUrlParams ) ); this.numericParameters.set( NumericParameters.WebRTCMinBitrate, new SettingNumber( NumericParameters.WebRTCMinBitrate, 'Min Bitrate (kbps)', 'The minimum bitrate that WebRTC should use.', 0 /*min*/, 500000 /*max*/, settings && settings.hasOwnProperty(NumericParameters.WebRTCMinBitrate) ? settings[NumericParameters.WebRTCMinBitrate] : 0, /*value*/ useUrlParams ) ); this.numericParameters.set( NumericParameters.WebRTCMaxBitrate, new SettingNumber( NumericParameters.WebRTCMaxBitrate, 'Max Bitrate (kbps)', 'The maximum bitrate that WebRTC should use.', 0 /*min*/, 500000 /*max*/, settings && settings.hasOwnProperty(NumericParameters.WebRTCMaxBitrate) ? settings[NumericParameters.WebRTCMaxBitrate] : 0, /*value*/ useUrlParams ) ); this.numericParameters.set( NumericParameters.StreamerAutoJoinInterval, new SettingNumber( NumericParameters.StreamerAutoJoinInterval, 'Streamer Auto Join Interval (ms)', 'Delay between retries when waiting for an available streamer.', 500 /*min*/, 900000 /*max*/, settings && settings.hasOwnProperty(NumericParameters.StreamerAutoJoinInterval) ? settings[NumericParameters.StreamerAutoJoinInterval] : 3000, /*value*/ useUrlParams ) ); } /** * Add a callback to fire when the numeric setting is toggled. * @param id The id of the flag. * @param onChangedListener The callback to fire when the numeric value changes. */ _addOnNumericSettingChangedListener( id: NumericParametersIds, onChangedListener: (newValue: number) => void ): void { if (this.numericParameters.has(id)) { this.numericParameters .get(id) .addOnChangedListener(onChangedListener); } } _addOnOptionSettingChangedListener( id: OptionParametersIds, onChangedListener: (newValue: string) => void ): void { if (this.optionParameters.has(id)) { this.optionParameters .get(id) .addOnChangedListener(onChangedListener); } } /** * @param id The id of the numeric setting we are interested in getting a value for. * @returns The numeric value stored in the parameter with the passed id. */ getNumericSettingValue(id: NumericParametersIds): number { if (this.numericParameters.has(id)) { return this.numericParameters.get(id).number; } else { throw new Error(`There is no numeric setting with the id of ${id}`); } } /** * @param id The id of the text setting we are interested in getting a value for. * @returns The text value stored in the parameter with the passed id. */ getTextSettingValue(id: TextParametersIds): string { if (this.textParameters.has(id)) { return this.textParameters.get(id).value as string; } else { throw new Error(`There is no numeric setting with the id of ${id}`); } } /** * Set number in the setting. * @param id The id of the numeric setting we are interested in. * @param value The numeric value to set. */ setNumericSetting(id: NumericParametersIds, value: number): void { if (this.numericParameters.has(id)) { this.numericParameters.get(id).number = value; } else { throw new Error(`There is no numeric setting with the id of ${id}`); } } /** * Add a callback to fire when the flag is toggled. * @param id The id of the flag. * @param onChangeListener The callback to fire when the value changes. */ _addOnSettingChangedListener( id: FlagsIds, onChangeListener: (newFlagValue: boolean) => void ): void { if (this.flags.has(id)) { this.flags.get(id).onChange = onChangeListener; } } /** * Add a callback to fire when the text is changed. * @param id The id of the flag. * @param onChangeListener The callback to fire when the value changes. */ _addOnTextSettingChangedListener( id: TextParametersIds, onChangeListener: (newTextValue: string) => void ): void { if (this.textParameters.has(id)) { this.textParameters.get(id).onChange = onChangeListener; } } /** * Get the option which has the given id. * @param id The id of the option. * @returns The SettingOption object matching id */ getSettingOption(id: OptionParametersIds): SettingOption { return this.optionParameters.get(id); } /** * Get the value of the configuration flag which has the given id. * @param id The unique id for the flag. * @returns True if the flag is enabled. */ isFlagEnabled(id: FlagsIds): boolean { return this.flags.get(id).flag as boolean; } /** * Set flag to be enabled/disabled. * @param id The id of the flag to toggle. * @param flagEnabled True if the flag should be enabled. */ setFlagEnabled(id: FlagsIds, flagEnabled: boolean) { if (!this.flags.has(id)) { Logger.Warning( Logger.GetStackTrace(), `Cannot toggle flag called ${id} - it does not exist in the Config.flags map.` ); } else { this.flags.get(id).flag = flagEnabled; } } /** * Set the text setting. * @param id The id of the setting * @param settingValue The value to set in the setting. */ setTextSetting(id: TextParametersIds, settingValue: string) { if (!this.textParameters.has(id)) { Logger.Warning( Logger.GetStackTrace(), `Cannot set text setting called ${id} - it does not exist in the Config.textParameters map.` ); } else { this.textParameters.get(id).text = settingValue; } } /** * Set the option setting list of options. * @param id The id of the setting * @param settingOptions The values the setting could take */ setOptionSettingOptions( id: OptionParametersIds, settingOptions: Array<string> ) { if (!this.optionParameters.has(id)) { Logger.Warning( Logger.GetStackTrace(), `Cannot set text setting called ${id} - it does not exist in the Config.optionParameters map.` ); } else { this.optionParameters.get(id).options = settingOptions; } } /** * Set option enum settings selected option. * @param id The id of the setting * @param settingOptions The value to select out of all the options */ setOptionSettingValue(id: OptionParametersIds, settingValue: string) { if (!this.optionParameters.has(id)) { Logger.Warning( Logger.GetStackTrace(), `Cannot set text setting called ${id} - it does not exist in the Config.enumParameters map.` ); } else { const optionSetting = this.optionParameters.get(id); const existingOptions = optionSetting.options; if (!existingOptions.includes(settingValue)) { existingOptions.push(settingValue); optionSetting.options = existingOptions; } optionSetting.selected = settingValue; } } /** * Set the label for the flag. * @param id The id of the flag. * @param label The new label to use for the flag. */ setFlagLabel(id: FlagsIds, label: string) { if (!this.flags.has(id)) { Logger.Warning( Logger.GetStackTrace(), `Cannot set label for flag called ${id} - it does not exist in the Config.flags map.` ); } else { this.flags.get(id).label = label; } } /** * Set a subset of all settings in one function call. * * @param settings A (partial) list of settings to set */ setSettings(settings: Partial<AllSettings>) { for (const key of Object.keys(settings)) { if (isFlagId(key)) { this.setFlagEnabled(key, settings[key]); } else if (isNumericId(key)) { this.setNumericSetting(key, settings[key]); } else if (isTextId(key)) { this.setTextSetting(key, settings[key]); } else if (isOptionId(key)) { this.setOptionSettingValue(key, settings[key]); } } } /** * Get all settings * @returns All setting values as an object with setting ids as keys */ getSettings(): Partial<AllSettings> { const settings: Partial<AllSettings> = {}; for (const [key, value] of this.flags.entries()) { settings[key] = value.flag; } for (const [key, value] of this.numericParameters.entries()) { settings[key] = value.number; } for (const [key, value] of this.textParameters.entries()) { settings[key] = value.text; } for (const [key, value] of this.optionParameters.entries()) { settings[key] = value.selected; } return settings; } /** * Get all Flag settings as an array. * @returns All SettingFlag objects */ getFlags(): Array<SettingFlag> { return Array.from(this.flags.values()); } /** * Get all Text settings as an array. * @returns All SettingText objects */ getTextSettings(): Array<SettingText> { return Array.from(this.textParameters.values()); } /** * Get all Number settings as an array. * @returns All SettingNumber objects */ getNumericSettings(): Array<SettingNumber> { return Array.from(this.numericParameters.values()); } /** * Get all Option settings as an array. * @returns All SettingOption objects */ getOptionSettings(): Array<SettingOption> { return Array.from(this.optionParameters.values()); } /** * Emit events when settings change. * @param eventEmitter */ _registerOnChangeEvents(eventEmitter: EventEmitter) { for (const key of this.flags.keys()) { const flag = this.flags.get(key); if (flag) { flag.onChangeEmit = (newValue: boolean) => eventEmitter.dispatchEvent( new SettingsChangedEvent({ id: flag.id, type: 'flag', value: newValue, target: flag }) ); } } for (const key of this.numericParameters.keys()) { const number = this.numericParameters.get(key); if (number) { number.onChangeEmit = (newValue: number) => eventEmitter.dispatchEvent( new SettingsChangedEvent({ id: number.id, type: 'number', value: newValue, target: number }) ); } } for (const key of this.textParameters.keys()) { const text = this.textParameters.get(key); if (text) { text.onChangeEmit = (newValue: string) => eventEmitter.dispatchEvent( new SettingsChangedEvent({ id: text.id, type: 'text', value: newValue, target: text }) ); } } for (const key of this.optionParameters.keys()) { const option = this.optionParameters.get(key); if (option) { option.onChangeEmit = (newValue: string) => eventEmitter.dispatchEvent( new SettingsChangedEvent({ id: option.id, type: 'option', value: newValue, target: option }) ); } } } } /** * The enum associated with the mouse being locked or hovering */ export enum ControlSchemeType { LockedMouse = 0, HoveringMouse = 1 }