Add event system for SteamClient callbacks (untested; WIP)

This commit is contained in:
NGnius (Graham) 2022-10-12 20:39:34 -04:00
parent a52309484e
commit 17c61907a9
18 changed files with 446 additions and 33 deletions

View file

@ -1,6 +1,8 @@
# React-Frontend Plugin Template # Caylon
Reference example for using [decky-frontend-lib](https://github.com/SteamDeckHomebrew/decky-frontend-lib) in a [PluginLoader](https://github.com/SteamDeckHomebrew/PluginLoader) plugin. Custom widgets for simple tasks.
Adapted from a template for using [decky-frontend-lib](https://github.com/SteamDeckHomebrew/decky-frontend-lib) in a [Decky Loader](https://github.com/SteamDeckHomebrew/PluginLoader) plugin.
## PluginLoader Discord [![Chat](https://img.shields.io/badge/chat-on%20discord-7289da.svg)](https://discord.gg/ZU74G2NJzk) ## PluginLoader Discord [![Chat](https://img.shields.io/badge/chat-on%20discord-7289da.svg)](https://discord.gg/ZU74G2NJzk)
@ -8,30 +10,18 @@ Reference example for using [decky-frontend-lib](https://github.com/SteamDeckHom
### Dependencies ### Dependencies
This template relies on the user having `pnpm` installed on their system. This project relies on `npm` and `rustup`.
This can be downloaded from `npm` itself which is recommended. For building on another Linux PC, Rust toolchain `x86_64-unknown-linux-musl` must also be installed.
#### Linux
```bash
sudo npm i -g pnpm
```
### Getting Started ### Getting Started
1. Clone the repository to use as an example for making your plugin. 1. Clone the repository to use as an example for making your plugin.
2. In your clone of the repository run these commands: 2. In your clone of the repository run these commands to build the front-end:
1. ``pnpm i`` 1. ``npm install``
2. ``pnpm run build`` 2. ``npm run build``
3. You should do this every time you make changes to your plugin. 3. From `backend/`, Run `./build.sh` to build the back-end.
Note: If you are recieveing build errors due to an out of date library, you should run this command inside of your repository:
```bash
pnpm update decky-frontend-lib --latest
```
### Distribution ### Distribution
WIP. Check back in later. IDK, maybe a bit of custom spins as well as a barebones offering from me

View file

@ -14,7 +14,7 @@ clap = { version = "3.2", features = ["derive", "std"], default-features = false
# async # async
tokio = { version = "*", features = ["time"] } tokio = { version = "*", features = ["time"] }
async-trait = { version = "0.1.57" } async-trait = { version = "0.1" }
# json # json
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }

View file

@ -2,6 +2,7 @@ mod about;
pub(crate) mod async_utils; pub(crate) mod async_utils;
mod get_display; mod get_display;
mod get_items; mod get_items;
mod on_event;
mod on_javascript_result; mod on_javascript_result;
mod on_update; mod on_update;
mod reload; mod reload;
@ -12,6 +13,7 @@ mod types;
pub use about::get_about; pub use about::get_about;
pub use get_display::GetDisplayEndpoint; pub use get_display::GetDisplayEndpoint;
pub use get_items::get_items; pub use get_items::get_items;
pub use on_event::on_event;
pub use on_javascript_result::on_javascript_result; pub use on_javascript_result::on_javascript_result;
pub use on_update::on_update; pub use on_update::on_update;
pub use reload::reload; pub use reload::reload;

View file

@ -0,0 +1,36 @@
use std::sync::{Mutex, mpsc::Sender};
use usdpl_back::core::serdes::Primitive;
use super::ApiParameterType;
use crate::runtime::{QueueAction, QueueItem};
/// API web method to notify the back-end of a steam event (i.e. callback through SteamClient API)
pub fn on_event(sender: Sender<QueueItem>) -> impl Fn(ApiParameterType) -> ApiParameterType {
let sender = Mutex::new(sender);
move |params: ApiParameterType| {
log::debug!("API: on_event");
if let Some(Primitive::Json(event_data)) = params.get(0) {
match serde_json::from_str(event_data) {
Ok(event_obj) => {
sender.lock().unwrap().send(
QueueItem {
action: QueueAction::DoSteamEvent {
event: event_obj,
}
}
).unwrap();
log::info!("Sent steam event");
vec![true.into()]
},
Err(e) => {
log::warn!("Failed to parse event json: {}", e);
vec![false.into()]
}
}
} else {
vec![false.into()]
}
}
}

View file

@ -1,9 +1,44 @@
#![allow(non_snake_case)] #![allow(non_snake_case)]
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Clone)]
#[serde(tag = "event_type", content = "event_data")]
pub enum SteamEvent {
#[serde(rename = "download-items")]
DownloadItems(SteamDownloadInfo),
#[serde(rename = "download-overview")]
DownloadOverview(SteamDownloadOverview),
#[serde(rename = "achievement-notification")]
AchievementNotification(SteamAchievementNotification),
#[serde(rename = "bluetooth-state")]
BluetoothState(SteamBluetoothState),
#[serde(rename = "connectivity-test-change")]
ConnectivityTestChange(SteamConnectivityTestChange),
#[serde(rename = "network-diagnostic")]
NetworkDiagnostic(SteamNetworkDiagnostic),
#[serde(rename = "audio-device-added")]
AudioDeviceAdded(SteamAudioDevice),
#[serde(rename = "audio-device-removed")]
AudioDeviceRemoved(SteamAudioDevice),
#[serde(rename = "brightness")]
Brightness(SteamBrightness),
#[serde(rename = "airplane")]
Airplane(SteamAirplane),
#[serde(rename = "battery")]
Battery(SteamBattery),
#[serde(rename = "screenshot-notification")]
ScreenshotNotification(SteamScreenshotNotification),
#[serde(rename = "controller-input-message")]
ControllerInputMessage(Vec<SteamControllerInputMessage>),
#[serde(rename = "app-lifetime-notification")]
AppLifetimeNotification(SteamAppLifetimeNotification),
#[serde(rename = "game-action-start")]
GameActionStart(SteamGameAction),
}
// list of these is second callback param for SteamClient.Downloads.RegisterForDownloadItems // list of these is second callback param for SteamClient.Downloads.RegisterForDownloadItems
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub struct SteamDownloadInfo { pub struct SteamDownloadItem {
pub active: bool, pub active: bool,
pub appid: usize, pub appid: usize,
pub buildid: usize, pub buildid: usize,
@ -11,6 +46,13 @@ pub struct SteamDownloadInfo {
pub paused: bool, pub paused: bool,
} }
// both params for callback for SteamClient.Downloads.RegisterForDownloadItems
#[derive(Serialize, Deserialize, Clone)]
pub struct SteamDownloadInfo {
pub paused: bool,
pub items: Vec<SteamDownloadItem>,
}
// only callback param for SteamClient.Downloads.RegisterForDownloadOverview // only callback param for SteamClient.Downloads.RegisterForDownloadOverview
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub struct SteamDownloadOverview { pub struct SteamDownloadOverview {
@ -103,7 +145,7 @@ pub struct SteamNetworkDiagnostic {
} }
// only param of callback for SteamClient.System.Audio.RegisterForDeviceAdded // only param of callback for SteamClient.System.Audio.RegisterForDeviceAdded
// and SteamClient.System.Audio.RegisterForDeviceAdded // and SteamClient.System.Audio.RegisterForDeviceRemoved
// Also type of vecDevices of await SteamClient.System.Audio.GetDevices() // Also type of vecDevices of await SteamClient.System.Audio.GetDevices()
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub struct SteamAudioDevice { pub struct SteamAudioDevice {
@ -196,3 +238,19 @@ pub struct SteamControllerInputMessage {
pub nController: usize, pub nController: usize,
pub strActionName: String, pub strActionName: String,
} }
// only param of callback for SteamClient.GameSessions.RegisterForAppLifetimeNotifications
#[derive(Serialize, Deserialize, Clone)]
pub struct SteamAppLifetimeNotification {
pub bRunning: bool,
pub nInstanceID: usize,
pub unAppID: usize,
}
// params of callback for SteamClient.Apps.RegisterForGameActionStart
#[derive(Serialize, Deserialize, Clone)]
pub struct SteamGameAction {
pub param0: usize, // idk what this is supposed to indicate
pub gameID: usize,
pub action: String, // idk what possible values are
}

View file

@ -1,6 +1,6 @@
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use super::{ButtonConfig, ToggleConfig, SliderConfig, ReadingConfig, ResultDisplayConfig}; use super::{ButtonConfig, ToggleConfig, SliderConfig, ReadingConfig, ResultDisplayConfig, EventDisplayConfig};
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
#[serde(tag = "element")] #[serde(tag = "element")]
@ -15,4 +15,6 @@ pub enum ElementConfig {
ReadingDisplay(ReadingConfig), ReadingDisplay(ReadingConfig),
#[serde(rename = "result-display")] #[serde(rename = "result-display")]
ResultDisplay(ResultDisplayConfig), ResultDisplay(ResultDisplayConfig),
#[serde(rename = "event-display")]
EventDisplay(EventDisplayConfig)
} }

View file

@ -0,0 +1,67 @@
use serde::{Serialize, Deserialize};
use super::TopLevelActionConfig;
#[derive(Serialize, Deserialize, Clone)]
pub struct EventDisplayConfig {
pub title: String,
/// Type of event to listen for
pub event: EventType,
/// Action to perform when the event occurs
pub on_event: TopLevelActionConfig,
}
#[derive(Serialize, Deserialize, Clone)]
pub enum EventType {
#[serde(rename = "achievement")]
Achievement,
#[serde(rename = "airplane", alias = "airplane mode")]
Airplane,
#[serde(rename = "bluetooth")]
Bluetooth,
#[serde(rename = "brightness")]
Brightness,
#[serde(rename = "screenshot")]
Screenshot,
#[serde(rename = "game-start", alias = "game start")]
GameStart,
#[serde(rename = "game-lifetime", alias = "game lifetime")]
GameLifetime,
}
impl EventType {
#[inline]
pub fn is_achievement(&self) -> bool {
matches!(self, Self::Achievement)
}
#[inline]
pub fn is_airplane(&self) -> bool {
matches!(self, Self::Airplane)
}
#[inline]
pub fn is_bluetooth(&self) -> bool {
matches!(self, Self::Bluetooth)
}
#[inline]
pub fn is_brightness(&self) -> bool {
matches!(self, Self::Brightness)
}
#[inline]
pub fn is_screenshot(&self) -> bool {
matches!(self, Self::Screenshot)
}
#[inline]
pub fn is_game_start(&self) -> bool {
matches!(self, Self::GameStart)
}
#[inline]
pub fn is_game_lifetime(&self) -> bool {
matches!(self, Self::GameLifetime)
}
}

View file

@ -3,6 +3,7 @@ mod action;
mod base; mod base;
mod button; mod button;
mod element; mod element;
mod event_display;
mod reading; mod reading;
mod result_display; mod result_display;
mod slider; mod slider;
@ -14,6 +15,7 @@ pub use action::{TopLevelActionConfig, ActionConfig, CommandAction, MirrorAction
pub use base::BaseConfig; pub use base::BaseConfig;
pub use button::ButtonConfig; pub use button::ButtonConfig;
pub use element::ElementConfig; pub use element::ElementConfig;
pub use event_display::{EventDisplayConfig, EventType};
pub use reading::ReadingConfig; pub use reading::ReadingConfig;
pub use result_display::ResultDisplayConfig; pub use result_display::ResultDisplayConfig;
pub use slider::SliderConfig; pub use slider::SliderConfig;

View file

@ -7,5 +7,6 @@ pub struct ReadingConfig {
pub title: String, pub title: String,
/// Period in milliseconds, or None/null for non-repeating actions /// Period in milliseconds, or None/null for non-repeating actions
pub period_ms: Option<u64>, pub period_ms: Option<u64>,
/// Action to perform on every period
pub on_period: TopLevelActionConfig, pub on_period: TopLevelActionConfig,
} }

View file

@ -3,6 +3,6 @@ use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub struct ResultDisplayConfig { pub struct ResultDisplayConfig {
pub title: String, pub title: String,
/// index of element who's action's result will be used /// Index of element who's action's result will be used
pub result_of: usize, pub result_of: usize,
} }

View file

@ -33,6 +33,7 @@ fn main() -> Result<(), ()> {
.register_blocking("get_items", api::get_items(sender.clone())) .register_blocking("get_items", api::get_items(sender.clone()))
.register("on_javascript_result", api::on_javascript_result(sender.clone())) .register("on_javascript_result", api::on_javascript_result(sender.clone()))
.register("on_update", api::on_update(sender.clone())) .register("on_update", api::on_update(sender.clone()))
.register("on_event", api::on_event(sender.clone()))
.register_blocking("reload", api::reload(sender.clone())); .register_blocking("reload", api::reload(sender.clone()));
let _exec_handle = executor.spawn(); let _exec_handle = executor.spawn();
instance.run_blocking() instance.run_blocking()

View file

@ -31,6 +31,7 @@ impl<'a> SeqAct<'a> for Actor {
ElementConfig::Slider(s) => TopLevelActorType::build(&s.on_set, parameter.1), ElementConfig::Slider(s) => TopLevelActorType::build(&s.on_set, parameter.1),
ElementConfig::ReadingDisplay(r) => TopLevelActorType::build(&r.on_period, parameter.1), ElementConfig::ReadingDisplay(r) => TopLevelActorType::build(&r.on_period, parameter.1),
ElementConfig::ResultDisplay(_) => Err(format!("Item #{} is a ResultDisplay, which can't act", i)), ElementConfig::ResultDisplay(_) => Err(format!("Item #{} is a ResultDisplay, which can't act", i)),
ElementConfig::EventDisplay(e) => TopLevelActorType::build(&e.on_event, parameter.1),
}?; }?;
Ok(Self { Ok(Self {
actor_type: a_type, actor_type: a_type,
@ -168,6 +169,7 @@ where
outputs: std::collections::VecDeque<Expected>, outputs: std::collections::VecDeque<Expected>,
} }
#[cfg(test)]
pub enum Expected { pub enum Expected {
Output(Primitive), Output(Primitive),
BuildErr(ActError), BuildErr(ActError),

View file

@ -85,6 +85,7 @@ mod test {
#[test] #[test]
fn json_actor_test() { fn json_actor_test() {
//let (runtime_io, _result_rx, _js_rx) = crate::runtime::RuntimeIO::mock(); //let (runtime_io, _result_rx, _js_rx) = crate::runtime::RuntimeIO::mock();
// test data """borrowed""" from https://jmespath.org/
SeqActTestHarness::builder(JsonActor::build) SeqActTestHarness::builder(JsonActor::build)
// test 1 --- // test 1 ---
.with_io( .with_io(

View file

@ -2,6 +2,7 @@ use std::sync::mpsc::Sender;
use usdpl_back::core::serdes::Primitive; use usdpl_back::core::serdes::Primitive;
use crate::api::SteamEvent;
use crate::config::{AboutConfig, ElementConfig}; use crate::config::{AboutConfig, ElementConfig};
/// An API operation for the executor to perform /// An API operation for the executor to perform
@ -27,6 +28,9 @@ pub enum QueueAction {
id: usize, id: usize,
value: Primitive, value: Primitive,
}, },
DoSteamEvent {
event: SteamEvent,
},
} }
/// Wrapper for an executor command /// Wrapper for an executor command

View file

@ -2,10 +2,43 @@ use std::thread;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::mpsc::{self, Receiver, Sender}; use std::sync::mpsc::{self, Receiver, Sender};
use usdpl_back::core::serdes::Primitive;
use crate::config::{BaseConfig, ElementConfig}; use crate::config::{BaseConfig, ElementConfig};
use crate::api::SteamEvent;
use super::{QueueItem, QueueAction, Act, SeqAct}; use super::{QueueItem, QueueAction, Act, SeqAct};
use super::{ResultRouter, RouterCommand, JavascriptRouter, JavascriptCommand}; use super::{ResultRouter, RouterCommand, JavascriptRouter, JavascriptCommand};
macro_rules! wire_steam_event {
(
$func_name: ident,
$display_name: literal,
$self: ident,
$conf: ident,
$item: ident,
$index: ident,
$event: ident,
$event_json_cache: ident,
) => {
if $conf.event.$func_name() {
let value = cache_event_maybe(&$event, &mut $event_json_cache);
match super::Actor::build($item, ($index, &$self.handlers)) {
Ok(act) => {
let respond_to = $self.handlers.result.clone();
thread::spawn(move || {
let result = act.run(value);
match respond_to.send(RouterCommand::HandleResult{$index, result}) {
Ok(_) => {},
Err(_) => log::warn!("Failed to send DoSteamEvent `{}` response for item #{}", $display_name, $index),
}
});
},
Err(e) => log::error!("Failed to build DoSteamEvent `{}` actor for item #{}: {}", $display_name, $index, e)
}
}
}
}
#[derive(Clone)] #[derive(Clone)]
pub struct RuntimeIO { pub struct RuntimeIO {
pub result: Sender<RouterCommand>, pub result: Sender<RouterCommand>,
@ -83,7 +116,7 @@ impl ExecutorState {
}, },
QueueAction::DoUpdate { index, value } => { QueueAction::DoUpdate { index, value } => {
// trigger update event for element // trigger update event for element
// i.e. on_click, on_toggle, etc. action // e.g. on_click, on_toggle, etc. action
if let Some(item) = self.config_data.get_item(index) { if let Some(item) = self.config_data.get_item(index) {
match super::Actor::build(item, (index, &self.handlers)) { match super::Actor::build(item, (index, &self.handlers)) {
Ok(act) => { Ok(act) => {
@ -151,6 +184,97 @@ impl ExecutorState {
log::error!("Failed to send to JavascriptRouter again, did not SetJavascriptSubscriber"); log::error!("Failed to send to JavascriptRouter again, did not SetJavascriptSubscriber");
} }
} }
},
QueueAction::DoSteamEvent { event } => {
// handle steam event for all elements that may be listening
let mut event_json_cache: Option<String> = None;
for (index, item) in self.config_data.items().iter().enumerate() {
match item {
ElementConfig::EventDisplay(conf) => {
match &event {
SteamEvent::DownloadItems(_x) => log::error!("Unsupported event"),
SteamEvent::DownloadOverview(_x) => log::error!("Unsupported event"),
SteamEvent::AchievementNotification(x) => wire_steam_event!{
is_achievement,
"achievement",
self,
conf,
item,
index,
x,
event_json_cache,
},
SteamEvent::BluetoothState(x) => wire_steam_event!{
is_bluetooth,
"bluetooth",
self,
conf,
item,
index,
x,
event_json_cache,
},
SteamEvent::ConnectivityTestChange(_x) => log::error!("Unsupported event"),
SteamEvent::NetworkDiagnostic(_x) => log::error!("Unsupported event"),
SteamEvent::AudioDeviceAdded(_x) => log::error!("Unsupported event"),
SteamEvent::AudioDeviceRemoved(_x) => log::error!("Unsupported event"),
SteamEvent::Brightness(x) => wire_steam_event!{
is_brightness,
"brightness",
self,
conf,
item,
index,
x,
event_json_cache,
},
SteamEvent::Airplane(x) => wire_steam_event!{
is_airplane,
"airplane",
self,
conf,
item,
index,
x,
event_json_cache,
},
SteamEvent::Battery(_x) => log::error!("Unsupported event"),
SteamEvent::ScreenshotNotification(x) => wire_steam_event!{
is_screenshot,
"screenshot",
self,
conf,
item,
index,
x,
event_json_cache,
},
SteamEvent::ControllerInputMessage(_x) => log::error!("Unsupported event"),
SteamEvent::AppLifetimeNotification(x) => wire_steam_event!{
is_game_lifetime,
"game-lifetime",
self,
conf,
item,
index,
x,
event_json_cache,
},
SteamEvent::GameActionStart(x) => wire_steam_event!{
is_game_start,
"game-start",
self,
conf,
item,
index,
x,
event_json_cache,
},
}
}
_ => {}
}
}
} }
} }
} }
@ -183,3 +307,14 @@ impl ExecutorState {
} }
} }
} }
#[inline]
fn cache_event_maybe<T: serde::Serialize>(event: &T, cache: &mut Option<String>) -> Primitive {
if let Some(cached) = cache {
Primitive::Json(cached.to_owned())
} else {
let dump = serde_json::to_string(event).unwrap();
*cache = Some(dump.clone());
Primitive::Json(dump)
}
}

View file

@ -4,7 +4,7 @@ const USDPL_PORT: number = 25717;
// Utility // Utility
export function resolve(promise: Promise<any>, setter: any) { export function resolve<T>(promise: Promise<T>, setter: (x: T) => void) {
(async function () { (async function () {
let data = await promise; let data = await promise;
if (data != null) { if (data != null) {
@ -64,7 +64,18 @@ export type CResultDisplay = {
result_of: number; result_of: number;
} }
export type CElement = CButton | CToggle | CSlider | CReading | CResultDisplay; export type CEventDisplay = {
element: string; // "event-display"
title: string;
event: string;
}
export type CSteamEvent = {
event_type: string; // enum; see steam_types.rs
event_data: any;
}
export type CElement = CButton | CToggle | CSlider | CReading | CResultDisplay | CEventDisplay;
export type CErrorResult = { export type CErrorResult = {
result: string; // "error" result: string; // "error"
@ -91,7 +102,7 @@ export async function getElements(): Promise<CElement[]> {
return (await call_backend("get_items", []))[0]; return (await call_backend("get_items", []))[0];
} }
export async function onUpdate(index: number, value: any): Promise<any> { export async function onUpdate(index: number, value: any): Promise<boolean> {
return (await call_backend("on_update", [index, value]))[0]; return (await call_backend("on_update", [index, value]))[0];
} }
@ -111,6 +122,10 @@ export async function getJavascriptToRun(): Promise<CJavascriptResponse> {
return (await call_backend("get_javascript_to_run", []))[0]; return (await call_backend("get_javascript_to_run", []))[0];
} }
export async function onJavascriptResult(id: number, value: any): Promise<any> { export async function onJavascriptResult(id: number, value: any): Promise<boolean> {
return (await call_backend("on_javascript_result", [id, value]))[0]; return (await call_backend("on_javascript_result", [id, value]))[0];
} }
export async function onSteamEvent(data: CSteamEvent): Promise<boolean> {
return (await call_backend("on_javascript_result", [data]))[0];
}

View file

@ -21,6 +21,7 @@ import { GiWashingMachine } from "react-icons/gi";
import { get_value, set_value } from "usdpl-front"; import { get_value, set_value } from "usdpl-front";
import * as backend from "./backend"; import * as backend from "./backend";
import {register_for_steam_events, unregister_for_steam_events} from "./steam_events";
const FieldWithSeparator = joinClassNames(gamepadDialogClasses.Field, gamepadDialogClasses.WithBottomSeparatorStandard); const FieldWithSeparator = joinClassNames(gamepadDialogClasses.Field, gamepadDialogClasses.WithBottomSeparatorStandard);
@ -65,6 +66,7 @@ function onGetElements() {
backend.resolve(backend.getDisplay(i), displayCallback(i)); backend.resolve(backend.getDisplay(i), displayCallback(i));
} }
backend.resolve(backend.getJavascriptToRun(), jsCallback()); backend.resolve(backend.getJavascriptToRun(), jsCallback());
register_for_steam_events();
} }
const eval2 = eval; const eval2 = eval;
@ -110,6 +112,7 @@ function jsCallback() {
console.warn("CAYLON: backend connection failed"); console.warn("CAYLON: backend connection failed");
} }
backend.resolve(backend.getJavascriptToRun(), jsCallback()); backend.resolve(backend.getJavascriptToRun(), jsCallback());
register_for_steam_events();
})(); })();
const Content: VFC<{ serverAPI: ServerAPI }> = ({}) => { const Content: VFC<{ serverAPI: ServerAPI }> = ({}) => {
@ -181,6 +184,8 @@ function buildHtmlElement(element: backend.CElement, index: number, updateIdc: a
return buildReading(element as backend.CReading, index, updateIdc); return buildReading(element as backend.CReading, index, updateIdc);
case "result-display": case "result-display":
return buildResultDisplay(element as backend.CResultDisplay, index, updateIdc); return buildResultDisplay(element as backend.CResultDisplay, index, updateIdc);
case "event-display":
return buildEventDisplay(element as backend.CEventDisplay, index, updateIdc);
} }
console.error("CAYLON: Unsupported element", element); console.error("CAYLON: Unsupported element", element);
return <div>Unsupported</div>; return <div>Unsupported</div>;
@ -256,6 +261,17 @@ function buildResultDisplay(element: backend.CResultDisplay, index: number, _upd
); );
} }
function buildEventDisplay(element: backend.CEventDisplay, index: number, _updateIdc: any) {
return (
<div className={FieldWithSeparator}>
<div className={gamepadDialogClasses.FieldLabelRow}>
<div className={gamepadDialogClasses.FieldLabel}>{element.title}</div>
<div className={gamepadDialogClasses.FieldChildren}>{get_value(DISPLAY_KEY + index.toString())}</div>
</div>
</div>
);
}
function buildAbout() { function buildAbout() {
if (about == null) { if (about == null) {
return []; return [];
@ -360,12 +376,13 @@ function buildAbout() {
} }
export default definePlugin((serverApi: ServerAPI) => { export default definePlugin((serverApi: ServerAPI) => {
register_for_steam_events()
return { return {
title: <div className={staticClasses.Title}>{about == null? "Kaylon": about.name}</div>, title: <div className={staticClasses.Title}>{about == null? "Caylon": about.name}</div>,
content: <Content serverAPI={serverApi} />, content: <Content serverAPI={serverApi} />,
icon: <GiWashingMachine />, icon: <GiWashingMachine />,
onDismount() { onDismount() {
//serverApi.routerHook.removeRoute("/decky-plugin-test"); unregister_for_steam_events();
}, },
}; };
}); });

80
src/steam_events.ts Normal file
View file

@ -0,0 +1,80 @@
import * as backend from "./backend";
type Unregisterer = {
unregister: () => void;
}
let callbacks: Unregisterer[] = [];
export function register_for_steam_events() {
unregister_for_steam_events();
//@ts-ignore
SteamClient.Apps.RegisterForGameActionStart((p0, p1, p2) => {
backend.onSteamEvent( {
event_type: "game-action-start",
event_data: {
param0: p0,
gameID: p1,
action: p2,
}
});
});
//@ts-ignore
SteamClient.GameSessions.RegisterForAppLifetimeNotifications((p0) => {
backend.onSteamEvent( {
event_type: "app-lifetime-notification",
event_data: p0
});
});
//@ts-ignore
SteamClient.GameSessions.RegisterForAchievementNotification((p0) => {
backend.onSteamEvent( {
event_type: "achievement-notification",
event_data: p0
});
});
//@ts-ignore
SteamClient.System.Bluetooth.RegisterForStateChanges((p0) => {
backend.onSteamEvent( {
event_type: "bluetooth-state",
event_data: p0
});
});
//@ts-ignore
SteamClient.System.Display.RegisterForBrightnessChanges((p0) => {
backend.onSteamEvent( {
event_type: "brightness",
event_data: p0
});
});
//@ts-ignore
SteamClient.System.RegisterForAirplaneModeChanges((p0) => {
backend.onSteamEvent( {
event_type: "airplane",
event_data: p0
});
});
//@ts-ignore
SteamClient.GameSessions.RegisterForScreenshotNotification((p0) => {
backend.onSteamEvent( {
event_type: "screenshot-notification",
event_data: p0
});
});
// TODO add more events
}
export function unregister_for_steam_events() {
for (let i = 0; i < callbacks.length; i++) {
callbacks[i].unregister();
}
callbacks = [];
}