diff --git a/src/extension/companion.ts b/src/extension/companion.ts index c99c333e..61a505cb 100644 --- a/src/extension/companion.ts +++ b/src/extension/companion.ts @@ -1,12 +1,14 @@ -import { player, startPlaylist } from './intermission-player'; -import companion from './util/companion'; -import { wait } from './util/helpers'; +import { player } from './intermission-player'; +import companion, { ActionHandler } from './util/companion'; import { get as nodecg } from './util/nodecg'; -import obs, { canChangeScene, changeScene } from './util/obs'; -import { assetsVideos, obsData, streamDeckData, videoPlayer } from './util/replicants'; +import { assetsVideos, obsData, streamDeckData } from './util/replicants'; import { sc } from './util/speedcontrol'; - -const config = nodecg().bundleConfig; +import actionVideoPlay from './companion/actionVideoPlay'; +import actionIntermissionSceneChange from './companion/actionIntermissionSceneChange'; +import actionTimerToggle from './companion/actionTimerToggle'; +import actionPlayerHudTriggerToggle from './companion/actionPlayerHudTriggerToggle'; +import actionTwitchCommercialsDisable from './companion/actionTwitchCommercialsDisable'; +import actionSceneCycle from './companion/actionSceneCycle'; // Replicants only applicable to this file from another bundle. const twitchCommercialsDisabled = nodecg().Replicant('disabled', 'esa-commercials'); @@ -36,112 +38,22 @@ companion.evt.on('open', (socket) => { companion.send({ name: 'videos', value: assetsVideos.value }); }); +const actionMap: { [key: string]: ActionHandler } = { + timer_toggle: actionTimerToggle, + player_hud_trigger_toggle: actionPlayerHudTriggerToggle, + twitch_commercials_disable: actionTwitchCommercialsDisable, + scene_cycle: actionSceneCycle, + intermission_scene_change: actionIntermissionSceneChange, + video_play: actionVideoPlay, + + // Yes, this is still allowed :) + // anything that takes up more than a single line of code should have its own file. + video_stop: () => player.endPlaylistEarly(), +}; + // Listening for any actions triggered from Companion. -let videoPlayPressedRecently = false; companion.evt.on('action', async (name, value) => { - // Controls the nodecg-speedcontrol timer. - // Currently the "Stop Timer" state works if there's only 1 team. - // TODO: Add team support. - if (name === 'timer_toggle') { - try { - // Note: the nodecg-speedcontrol bundle will check if it *can* do these actions, - // we do not need to check that here. - switch (sc.timer.value.state) { - case 'stopped': - case 'paused': - await sc.startTimer(); - break; - case 'running': - await sc.stopTimer(); - break; - case 'finished': - await sc.resetTimer(); - break; - default: - // Don't do anything - break; - } - } catch (err) { - // Drop for now - } - // Used to toggle the "Player HUD Trigger" type. - } else if (name === 'player_hud_trigger_toggle') { - const val = value as string; - if (streamDeckData.value.playerHUDTriggerType === val) { - delete streamDeckData.value.playerHUDTriggerType; - } else { - streamDeckData.value.playerHUDTriggerType = val; - } - // Used to disable the Twitch commercials for the remainder of a run. - } else if (name === 'twitch_commercials_disable') { - if (!twitchCommercialsDisabled.value - && !['stopped', 'finished'].includes(sc.timer.value.state)) { - // Sends a message to the esa-commercials bundle. - // Because we are using server-to-server messages, no confirmation yet. - nodecg().sendMessageToBundle('disable', 'esa-commercials'); - } - // Used to cycle scenes if applicable, usually used by hosts. - // Some of this is copied from obs-data.ts - } else if (name === 'scene_cycle') { - const { disableTransitioning, transitioning, connected } = obsData.value; - const { scenes } = config.obs.names; - // If transitioning is disabled, or we *are* transitioning, and OBS is connected, - // and the timer is not running or paused, we can trigger these actions. - if (!disableTransitioning && !transitioning && connected - && !['running', 'paused'].includes(sc.timer.value.state)) { - // If the current scene is any of the applicable intermission ones, the next scene - // will be the game layout, so change to it. - if (obs.isCurrentScene(scenes.commercials) - || obs.isCurrentScene(scenes.intermission) - || obs.isCurrentScene(scenes.intermissionCrowd)) { - await changeScene({ scene: config.obs.names.scenes.gameLayout }); - // If the current scene is the game layout, the next scene will be the intermission, - // so change to it. - } else if (obs.isCurrentScene(scenes.gameLayout)) { - // If the commercial intermission scene exists, use that, if not, use the regular one. - if (obs.findScene(scenes.commercials)) { - await changeScene({ scene: scenes.commercials }); - } else { - await changeScene({ scene: scenes.intermission }); - } - } - } - // Used to change between intermission scenes using a supplied scene name config key. - } else if (name === 'intermission_scene_change') { - const { scenes } = config.obs.names; - const val = value as string; - const scene = (scenes as { [k: string]: string })[val]; - await changeScene({ scene, force: true }); - // Used to play back a single video in the "Intermission Player" scene, - // intended to be used by hosts. - } else if (name === 'video_play') { - if (!videoPlayPressedRecently && !videoPlayer.value.playing - && canChangeScene({ scene: config.obs.names.scenes.intermissionPlayer, force: true })) { - videoPlayPressedRecently = true; - setTimeout(() => { videoPlayPressedRecently = false; }, 1000); - const val = value as string; - nodecg().log.debug('[Companion] Message received to play video (sum: %s)', val); - const videos = assetsVideos.value.filter((v) => v.sum === val); - if (videos.length > 1) { - // VIDEO WAS FOUND TWICE, MAKES NO SENSE! - nodecg().log.debug('[Companion] Multiple videos with the same sum found!'); - } else if (!videos.length) { - // VIDEO WAS NOT FOUND - nodecg().log.debug('[Companion] No videos found with that sum!'); - } else { - nodecg().log.debug('[Companion] Video found matching sum: %s', videos[0].name); - videoPlayer.value.playlist = [ - { - sum: videos[0].sum, - length: 0, - commercial: false, - }, - ]; - wait(500); // Safety wait - await startPlaylist(); - } - } - } else if (name === 'video_stop') { - await player.endPlaylistEarly(); + if (name in actionMap) { + await actionMap[name](name, value); } }); diff --git a/src/extension/companion/actionIntermissionSceneChange.ts b/src/extension/companion/actionIntermissionSceneChange.ts new file mode 100644 index 00000000..bc093e15 --- /dev/null +++ b/src/extension/companion/actionIntermissionSceneChange.ts @@ -0,0 +1,14 @@ +import { get as nodecg } from '@esa-layouts/util/nodecg'; +import { changeScene } from '@esa-layouts/util/obs'; + +const config = nodecg().bundleConfig; + +// Used to change between intermission scenes using a supplied scene name config key. +export default async function actionIntermissionSceneChange(name: string, value: unknown) + : Promise { + const { scenes } = config.obs.names; + const val = value as string; + const scene = (scenes as { [k: string]: string })[val]; + + await changeScene({ scene, force: true }); +} diff --git a/src/extension/companion/actionPlayerHudTriggerToggle.ts b/src/extension/companion/actionPlayerHudTriggerToggle.ts new file mode 100644 index 00000000..fdac0781 --- /dev/null +++ b/src/extension/companion/actionPlayerHudTriggerToggle.ts @@ -0,0 +1,11 @@ +import { streamDeckData } from '@esa-layouts/util/replicants'; + +// Used to toggle the "Player HUD Trigger" type. +export default function actionPlayerHudTriggerToggle(name: string, value: unknown): void { + const val = value as string; + if (streamDeckData.value.playerHUDTriggerType === val) { + delete streamDeckData.value.playerHUDTriggerType; + } else { + streamDeckData.value.playerHUDTriggerType = val; + } +} diff --git a/src/extension/companion/actionSceneCycle.ts b/src/extension/companion/actionSceneCycle.ts new file mode 100644 index 00000000..68101427 --- /dev/null +++ b/src/extension/companion/actionSceneCycle.ts @@ -0,0 +1,35 @@ +import { obsData } from '@esa-layouts/util/replicants'; +import { get as nodecg } from '@esa-layouts/util/nodecg'; +import { sc } from '@esa-layouts/util/speedcontrol'; +import obs, { changeScene } from '@esa-layouts/util/obs'; + +const config = nodecg().bundleConfig; + +// Used to cycle scenes if applicable, usually used by hosts. +// Some of this is copied from obs-data.ts +export default async function actionSceneCycle(): Promise { + const { disableTransitioning, transitioning, connected } = obsData.value; + const { scenes } = config.obs.names; + + // If transitioning is disabled, or we *are* transitioning, and OBS is connected, + // and the timer is not running or paused, we can trigger these actions. + if (!disableTransitioning && !transitioning && connected + && !['running', 'paused'].includes(sc.timer.value.state)) { + // If the current scene is any of the applicable intermission ones, the next scene + // will be the game layout, so change to it. + if (obs.isCurrentScene(scenes.commercials) + || obs.isCurrentScene(scenes.intermission) + || obs.isCurrentScene(scenes.intermissionCrowd)) { + await changeScene({ scene: config.obs.names.scenes.gameLayout }); + // If the current scene is the game layout, the next scene will be the intermission, + // so change to it. + } else if (obs.isCurrentScene(scenes.gameLayout)) { + // If the commercial intermission scene exists, use that, if not, use the regular one. + if (obs.findScene(scenes.commercials)) { + await changeScene({ scene: scenes.commercials }); + } else { + await changeScene({ scene: scenes.intermission }); + } + } + } +} diff --git a/src/extension/companion/actionTemplate.ts b/src/extension/companion/actionTemplate.ts new file mode 100644 index 00000000..1ed6ccfc --- /dev/null +++ b/src/extension/companion/actionTemplate.ts @@ -0,0 +1,5 @@ +// This is a template of witch you can base your own actions on. + +export default function actionTemplate(name: string, value: unknown): void { + // TODO: write code +} diff --git a/src/extension/companion/actionTimerToggle.ts b/src/extension/companion/actionTimerToggle.ts new file mode 100644 index 00000000..b261e7f0 --- /dev/null +++ b/src/extension/companion/actionTimerToggle.ts @@ -0,0 +1,28 @@ +import { sc } from '../util/speedcontrol'; + +// Controls the nodecg-speedcontrol timer. +// Currently the "Stop Timer" state works if there's only 1 team. +// TODO: Add team support. +export default async function actionTimerToggle(): Promise { + try { + // Note: the nodecg-speedcontrol bundle will check if it *can* do these actions, + // we do not need to check that here. + switch (sc.timer.value.state) { + case 'stopped': + case 'paused': + await sc.startTimer(); + break; + case 'running': + await sc.stopTimer(); + break; + case 'finished': + await sc.resetTimer(); + break; + default: + // Don't do anything + break; + } + } catch (err) { + // Drop for now + } +} diff --git a/src/extension/companion/actionTwitchCommercialsDisable.ts b/src/extension/companion/actionTwitchCommercialsDisable.ts new file mode 100644 index 00000000..9a26b724 --- /dev/null +++ b/src/extension/companion/actionTwitchCommercialsDisable.ts @@ -0,0 +1,15 @@ +import { sc } from '@esa-layouts/util/speedcontrol'; +import { get as nodecg } from '@esa-layouts/util/nodecg'; + +// Replicants only applicable to this file from another bundle. +const twitchCommercialsDisabled = nodecg().Replicant('disabled', 'esa-commercials'); + +// Used to disable the Twitch commercials for the remainder of a run. +export default function actionTwitchCommercialsDisable(): void { + if (!twitchCommercialsDisabled.value + && !['stopped', 'finished'].includes(sc.timer.value.state)) { + // Sends a message to the esa-commercials bundle. + // Because we are using server-to-server messages, no confirmation yet. + nodecg().sendMessageToBundle('disable', 'esa-commercials'); + } +} diff --git a/src/extension/companion/actionVideoPlay.ts b/src/extension/companion/actionVideoPlay.ts new file mode 100644 index 00000000..1a3b6eab --- /dev/null +++ b/src/extension/companion/actionVideoPlay.ts @@ -0,0 +1,40 @@ +import { assetsVideos, videoPlayer } from '@esa-layouts/util/replicants'; +import { canChangeScene } from '@esa-layouts/util/obs'; +import { get as nodecg } from '@esa-layouts/util/nodecg'; +import { wait } from '@esa-layouts/util/helpers'; +import { startPlaylist } from '@esa-layouts/intermission-player'; + +const config = nodecg().bundleConfig; +let videoPlayPressedRecently = false; + +// Used to play back a single video in the "Intermission Player" scene, +// intended to be used by hosts. +export default async function actionVideoPlay(name: string, value: unknown): Promise { + if (!videoPlayPressedRecently && !videoPlayer.value.playing + && canChangeScene({ scene: config.obs.names.scenes.intermissionPlayer, force: true })) { + videoPlayPressedRecently = true; + setTimeout(() => { videoPlayPressedRecently = false; }, 1000); + const val = value as string; + nodecg().log.debug('[Companion] Message received to play video (sum: %s)', val); + const videos = assetsVideos.value.filter((v) => v.sum === val); + if (videos.length > 1) { + // VIDEO WAS FOUND TWICE, MAKES NO SENSE! + nodecg().log.debug('[Companion] Multiple videos with the same sum found!'); + } else if (!videos.length) { + // VIDEO WAS NOT FOUND + nodecg().log.debug('[Companion] No videos found with that sum!'); + } else { + nodecg().log.debug('[Companion] Video found matching sum: %s', videos[0].name); + videoPlayer.value.playlist = [ + { + sum: videos[0].sum, + length: 0, + commercial: false, + }, + ]; + // TODO: this was never awaited in the original code, did it ever work? + await wait(500); // Safety wait + await startPlaylist(); + } + } +} diff --git a/src/extension/util/companion.ts b/src/extension/util/companion.ts index a89340d2..235fbba6 100644 --- a/src/extension/util/companion.ts +++ b/src/extension/util/companion.ts @@ -67,6 +67,8 @@ function send(data: { name: string, value: unknown }, socket?: WebSocket) { } } +export type ActionHandler = (name: string, value: unknown) => Promise | void; + export default { evt, send,