Skip to content

Steam achievements & rich presence

Roadmap item 48. This document is the Steamworks configuration companion to the implementation in apps/desktop/src-tauri/src/steam.rs and the useSteamAchievements / useSteamRichPresence React hooks.

Privacy guarantee: no conversation content, transcript text, or session details are ever sent to Steam. Only aggregate integer counts and generic activity tokens are transmitted — and only when the Steamworks SDK is active and the player is running inside Steam.


Configure these in the Steamworks App Admin → Achievements tab.

Display nameAPI nameUnlock condition
First ScenarioACH_FIRST_SCENARIOPlayer completes their first conversation scenario (session ends normally or is manually ended).
First DebriefACH_FIRST_DEBRIEFPlayer views their first generated debrief screen.
Practice StreakACH_PRACTICE_STREAKPlayer completes scenarios on three or more consecutive calendar days.
Pack ExplorerACH_PACK_EXPLORERPlayer plays a scenario from at least three different packs.
Creator First ValidateACH_CREATOR_FIRST_VALIDATEPlayer successfully validates their first custom scenario pack in the creator workbench.
  • Hidden: set to true for ACH_PRACTICE_STREAK and ACH_PACK_EXPLORER (reveal on unlock so players discover them organically).
  • Global unlock percentage: visible; Valve computes this automatically.
  • Icon: 64×64 px and 32×32 px locked/unlocked pairs required. See publishing/STEAM_ASSETS_SPEC.md for the art spec.

The front-end calls useSteamAchievements().unlock(SteamAchievement.<NAME>) at the appropriate event boundary. The Tauri command steam_unlock_achievement forwards the call to steamworks::UserStats::achievement(name).set() followed by store_stats(). The call is a no-op when Steam is absent.


Configure these in the Steamworks App Admin → Stats tab. All stats are INT type and monotonically increasing (never decremented).

Display nameAPI nameIncrement event
Scenarios CompletedSTAT_SCENARIOS_COMPLETEDSession ends (player ends a scenario or it completes naturally).
Debriefs GeneratedSTAT_DEBRIEFS_GENERATEDDebrief screen is displayed with generated content.
Packs ValidatedSTAT_PACKS_VALIDATEDCreator workbench reports a successful validate-pack run.
Text Mode SessionsSTAT_TEXT_MODE_SESSIONSSession starts in text input mode.
Voice Mode SessionsSTAT_VOICE_MODE_SESSIONSSession starts in voice input mode.

Stats store only counts — no content. A stat value of 7 means “7 scenarios completed”; it reveals nothing about which scenarios, what was said, or who the NPCs were. This matches the project’s local-first, no-telemetry commitment and the requirement stated in docs/steam-mvp-scope.md.

The front-end calls useSteamAchievements().incrementStat(SteamStat.<NAME>) at the relevant event. The Tauri command steam_increment_stat reads the current value, increments by 1, writes it back, and calls store_stats(). The call is a no-op when Steam is absent.


Configure rich presence localization in the Steamworks App Admin → Rich Presence tab under the app’s Steam client localization settings.

The integration uses a single key (steam_display) whose value is a localization token. The tokens and their suggested English display strings are:

TokenSuggested display string
#InScenarioIn a practice scenario
#ReviewingDebriefReviewing a debrief
#EditingPackEditing a scenario pack
#AtMainMenuBrowsing scenarios

Upload a localization file (richpresence.vdf) to the Steamworks portal for each supported language. Example English file:

"lang"
{
"Language" "english"
"Tokens"
{
"#InScenario" "In a practice scenario"
"#ReviewingDebrief" "Reviewing a debrief"
"#EditingPack" "Editing a scenario pack"
"#AtMainMenu" "Browsing scenarios"
}
}

Tokens reveal only the category of activity — never the scenario title, NPC name, conversation topic, turn count, or any other session detail.

The front-end calls useSteamRichPresence().setPresence(SteamActivity.<TOKEN>) when the player navigates to a new major screen. The Tauri command steam_set_rich_presence forwards the call to steamworks::Friends::set_rich_presence(key, value). The call is a no-op when Steam is absent.

Suggested call sites in the React screens:

ScreenToken to set
screens/Home / screens/ScenarioLibrarySteamActivity.AT_MAIN_MENU
screens/ConversationSteamActivity.IN_SCENARIO
screens/DebriefSteamActivity.REVIEWING_DEBRIEF
screens/CreatorWorkbenchSteamActivity.EDITING_PACK

All three features (achievements, stats, rich presence) are implemented as graceful no-ops when:

  • The steam Cargo feature is disabled (the default; builds without the Steamworks SDK).
  • The steam feature is enabled but steamworks::Client::init() fails (Steam not running, wrong AppID, or SDK not installed).
  • The front-end runs in a browser context (no window.__TAURI__).

The SteamRuntime struct always exists in managed state; its methods simply return false in all fallback cases without logging errors or throwing.

The Tauri commands (steam_unlock_achievement, steam_increment_stat, steam_set_rich_presence) can be invoked freely on any build; callers do not need to check SteamStatus.is_steam_enabled first.


Use this checklist before the Stage 4 gate (public Steam release):

  • All five achievements created in App Admin with correct API names.
  • Locked and unlocked icons uploaded for all five achievements.
  • Hidden flag set for ACH_PRACTICE_STREAK and ACH_PACK_EXPLORER.
  • All five stats created in App Admin as INT type.
  • Rich presence localization file uploaded for English.
  • Rich presence localization files uploaded for any additional launch languages.
  • End-to-end test: run with SteamAppId=480 cargo tauri dev --features steam, trigger each unlock event, confirm the Steam overlay shows the achievement notification and the stats increment.
  • Confirm no session content appears in any Steam-facing string.