Skip to content

Commit 05c0a17

Browse files
committed
desktop: Add Steamworks API as an optional ExternalInterface
1 parent f059327 commit 05c0a17

File tree

5 files changed

+138
-0
lines changed

5 files changed

+138
-0
lines changed

Cargo.lock

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

desktop/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ unicode-bidi = "0.3.18"
5454
fontconfig = { version = "0.10.0", optional = true, features = ["dlopen"]}
5555
memmap2.workspace = true
5656

57+
steamworks = { version = "0.12.0", optional = true }
58+
5759
[target.'cfg(target_os = "linux")'.dependencies]
5860
ashpd = "0.11.0"
5961

desktop/src/backends.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
mod external_interface;
22
mod fscommand;
33
mod navigator;
4+
#[cfg(feature = "steamworks")]
5+
mod steamworks_external_interface;
46
mod ui;
57

68
pub use external_interface::DesktopExternalInterfaceProvider;
79
pub use fscommand::DesktopFSCommandProvider;
810
pub use navigator::DesktopNavigatorInterface;
911
pub use navigator::PathAllowList;
12+
#[cfg(feature = "steamworks")]
13+
pub use steamworks_external_interface::SteamWorksExternalInterfaceProvider;
1014
pub use ui::DesktopUiBackend;
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
use ruffle_core::context::UpdateContext;
2+
use ruffle_core::external::{ExternalInterfaceProvider, Value as ExternalValue};
3+
use std::cell::RefCell;
4+
use steamworks::{AppId, Client};
5+
6+
#[derive(Default)]
7+
pub struct SteamWorksExternalInterfaceProvider {
8+
pub client: RefCell<Option<Client>>,
9+
}
10+
11+
impl ExternalInterfaceProvider for SteamWorksExternalInterfaceProvider {
12+
fn call_method(
13+
&self,
14+
_context: &mut UpdateContext<'_>,
15+
name: &str,
16+
args: &[ExternalValue],
17+
) -> ExternalValue {
18+
let Some(ref client) = *self.client.borrow() else {
19+
if name == "steamworks.client.init" {
20+
let client = if let [ExternalValue::Number(id)] = args {
21+
Client::init_app(AppId(*id as u32))
22+
} else {
23+
Client::init()
24+
};
25+
26+
match client {
27+
Ok(client) => {
28+
self.client.replace(Some(client));
29+
}
30+
Err(err) => tracing::warn!("Client::init failed: {err}"),
31+
}
32+
} else {
33+
tracing::warn!(
34+
"Steamworks client not initialized! Expected call to steamworks.client.init."
35+
);
36+
}
37+
38+
return ExternalValue::Undefined;
39+
};
40+
41+
// API is heavily inspired by https://github.com/ceifa/steamworks.js/
42+
43+
match name {
44+
"steamworks.utils.getAppId" => ExternalValue::Number(client.utils().app_id().0 as f64),
45+
"steamworks.utils.isSteamRunningOnSteamDeck" => {
46+
ExternalValue::Bool(client.utils().is_steam_running_on_steam_deck())
47+
}
48+
49+
"steamworks.localplayer.getSteamId" => {
50+
ExternalValue::String(client.user().steam_id().steamid32())
51+
}
52+
"steamworks.localplayer.getName" => ExternalValue::String(client.friends().name()),
53+
"steamworks.localplayer.getLevel" => {
54+
ExternalValue::Number(client.user().level() as f64)
55+
}
56+
"steamworks.localplayer.getIpCountry" => {
57+
ExternalValue::String(client.utils().ip_country())
58+
}
59+
60+
"steamworks.achievement.activate" => {
61+
let [ExternalValue::String(achievement)] = args else {
62+
tracing::warn!("steamworks.achievement.activate: Expected string argument");
63+
return ExternalValue::Undefined;
64+
};
65+
66+
ExternalValue::Bool(
67+
client
68+
.user_stats()
69+
.achievement(&achievement)
70+
.set()
71+
.and_then(|_| client.user_stats().store_stats())
72+
.is_ok(),
73+
)
74+
}
75+
"steamworks.achievement.isActivated" => {
76+
let [ExternalValue::String(achievement)] = args else {
77+
tracing::warn!("steamworks.achievement.isActivated: Expected string argument");
78+
return ExternalValue::Undefined;
79+
};
80+
81+
ExternalValue::Bool(
82+
client
83+
.user_stats()
84+
.achievement(&achievement)
85+
.get()
86+
.unwrap_or(false),
87+
)
88+
}
89+
90+
_ => {
91+
tracing::warn!(
92+
"Trying to call unknown SteamWorksExternalInterfaceProvider method: {name}"
93+
);
94+
ExternalValue::Undefined
95+
}
96+
}
97+
}
98+
99+
fn on_callback_available(&self, _name: &str) {}
100+
101+
fn get_id(&self) -> Option<String> {
102+
None
103+
}
104+
}

desktop/src/player.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#[cfg(feature = "steamworks")]
2+
use crate::backends::SteamWorksExternalInterfaceProvider;
13
use crate::backends::{
24
DesktopExternalInterfaceProvider, DesktopFSCommandProvider, DesktopNavigatorInterface,
35
DesktopUiBackend,
@@ -284,6 +286,13 @@ impl ActivePlayer {
284286
builder = builder.with_external_interface(Box::new(DesktopExternalInterfaceProvider {
285287
spoof_url: opt.player.spoof_url.clone(),
286288
}));
289+
} else {
290+
#[cfg(feature = "steamworks")]
291+
{
292+
builder = builder.with_external_interface(Box::new(
293+
SteamWorksExternalInterfaceProvider::default(),
294+
));
295+
}
287296
}
288297

289298
if !opt.gamepad_button_mapping.is_empty() {

0 commit comments

Comments
 (0)