Steam Integration
Requires feature:
steam
The Steam integration covers achievements, stats, friends, peer-to-peer messages, rich presence, and overlays. The whole surface lives on world.resources.steam. It is desktop only, gated behind the steam feature so wasm and headless builds do not have to pull in steamworks-sys.
Feature flag
[dependencies]
nightshade = { version = "...", features = ["steam"] }
Enabling steam pulls in steamworks and steamworks-sys. The crate links against the prebuilt Steamworks libraries those crates bundle, so there is no extra system dependency to install.
Initialization
initialize connects to the running Steam client and primes the internal state. Call it once at startup, from the State::initialize hook or wherever your game does setup:
#![allow(unused)] fn main() { fn initialize(&mut self, world: &mut World) { world.resources.steam.initialize(); } }
Steam runs its own callback pump. The engine does not poll it for you. run_callbacks drains pending events such as friend list updates and incoming P2P messages, and it has to run every frame to keep those queues from backing up:
#![allow(unused)] fn main() { fn run_systems(&mut self, world: &mut World) { world.resources.steam.run_callbacks(); } }
If Steam is not running or the user is not signed in, initialize records the failure and is_initialized() returns false. Subsequent calls become no-ops rather than panicking. Check is_initialized() before showing Steam-specific UI.
Achievements
#![allow(unused)] fn main() { let steam = &mut world.resources.steam; steam.unlock_achievement("FIRST_BLOOD"); steam.clear_achievement("FIRST_BLOOD"); steam.refresh_achievements(); for achievement in &steam.achievements { let name = &achievement.api_name; let unlocked = achievement.achieved; } }
unlock_achievement and clear_achievement take the API name configured in the Steamworks partner portal. refresh_achievements fetches the current achievement list from Steam into steam.achievements. Read the field, do not call out per achievement, because each call to Steam has a fixed overhead and the engine keeps the cached list in sync.
Stats
Stats are typed. Integer counters use set_stat_int, floating-point values use set_stat_float. Writes stay client-side until store_stats flushes them to Steam:
#![allow(unused)] fn main() { let steam = &mut world.resources.steam; steam.set_stat_int("kills", 42); steam.set_stat_float("play_time", 3.5); steam.store_stats(); steam.refresh_stats(); for stat in &steam.stats { match &stat.value { StatValue::Int(value) => { /* ... */ } StatValue::Float(value) => { /* ... */ } } } }
The pair of store_stats and refresh_stats is the I/O boundary. Set the values you care about, flush once, then refresh when you need to read them back.
Friends
#![allow(unused)] fn main() { let steam = &mut world.resources.steam; steam.refresh_friends(); for friend in &steam.friends { let name = &friend.name; let state = &friend.persona_state; } }
persona_state is one of Online, Offline, Busy, Away, Snooze, LookingToTrade, LookingToPlay. The same caching rule applies. Call refresh_friends when you want to repopulate the list, then iterate the cached steam.friends vector from your UI code.
P2P networking
The networking surface mirrors Steamworks. Each message carries a sender, a payload, and a channel number, and channels are independent queues:
#![allow(unused)] fn main() { let steam = &mut world.resources.steam; steam.setup_networking_callbacks(); steam.send_message(peer_steam_id, data_bytes, channel); let messages = steam.receive_messages(channel); for message in messages { let sender = message.sender; let data = &message.data; } }
setup_networking_callbacks wires up the Steam callbacks that let other peers connect to you. Call it once at startup, after initialize. receive_messages drains the queue for a single channel and returns whatever has arrived since the last call.
Session management
#![allow(unused)] fn main() { steam.close_session(peer_steam_id); let state = steam.get_session_state(peer_steam_id); steam.refresh_session_states(); }
Session states are None, Connecting, Connected, ClosedByPeer, ProblemDetected, and Failed. close_session is the polite hang-up. The other side observes ClosedByPeer on its next refresh. ProblemDetected and Failed are terminal and indicate the connection cannot be recovered without rebuilding it.
Rich presence
#![allow(unused)] fn main() { steam.set_rich_presence("status", "In Battle - Level 5"); steam.clear_rich_presence(); }
Rich presence keys are arbitrary strings. The status key is the conventional name for the line that appears next to a friend in their friend list.
Overlays
#![allow(unused)] fn main() { steam.open_invite_dialog(); steam.open_overlay_to_user(friend_steam_id); }
These functions ask the Steam client to draw its overlay on top of the game window. The overlay is part of the Steam client process, not the engine, so it requires the user to have the overlay enabled in their Steam settings.
Platform notes
The Steam client has to be running on the user's machine. The integration is desktop only and not exposed on wasm. When Steam is unavailable, every method on the resource becomes a no-op rather than a panic, so a build that ships with the feature enabled still runs on a machine without Steam installed. Guard Steam-specific UI on is_initialized() so the game stays usable in that case.