Add event system for SteamClient callbacks (untested; WIP)
This commit is contained in:
parent
a52309484e
commit
17c61907a9
18 changed files with 446 additions and 33 deletions
32
README.md
32
README.md
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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"] }
|
||||||
|
|
|
@ -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;
|
||||||
|
|
36
backend/src/api/on_event.rs
Normal file
36
backend/src/api/on_event.rs
Normal 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()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
67
backend/src/config/event_display.rs
Normal file
67
backend/src/config/event_display.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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];
|
||||||
|
}
|
||||||
|
|
|
@ -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
80
src/steam_events.ts
Normal 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 = [];
|
||||||
|
}
|
Reference in a new issue