Skip to content

fix(steam): restore achievement loading for real-world games#22

Open
MotherSphere wants to merge 1 commit into
jsnli:masterfrom
MotherSphere:fix/critical-steam-bugs
Open

fix(steam): restore achievement loading for real-world games#22
MotherSphere wants to merge 1 commit into
jsnli:masterfrom
MotherSphere:fix/critical-steam-bugs

Conversation

@MotherSphere
Copy link
Copy Markdown

Summary

Three independent bugs in src-tauri/src/steam.rs that combine to make Samira return empty achievement lists for most non-trivial games. All happen to be hidden by happy-path luck on small/simple games, which is why upstream testing didn't catch them.

I hit these while building a downstream fork (Project-Colony/SAM-Colony-Edition) — sending the bug-fix portion upstream because it benefits everyone using Samira on AAA titles.

The bugs

1. CallbackHandle leak in start_client

client.register_callback(move |_data: UserStatsReceived| { ... });
//                                                                ^ return value discarded

register_callback returns a CallbackHandle whose Drop impl synchronously unregisters the callback (steamworks 0.12.2/src/callback.rs#L143). Discarding the return value drops the handle right away, removing the callback before Steam can ever fire it. The 1-second wait loop then always times out, and load_achievements proceeds against unready stats.

Fix: bind the handle to a named local for the duration of the wait loop (let _stats_cb = ...). Also reorder request_user_stats() to fire after the callback is registered (otherwise a fast pipe can race and we miss the event even with the handle held).

2. break; after first ACHIEVEMENTS block in load_achievement_icons

Steam represents achievements as 32-bit bitfields. Games with more than 32 achievements (Elden Ring: 42, Skyrim: 75, RDR2: 50+, etc.) have multiple ACHIEVEMENTS entries in the schema. The current loop reads the first block, then break;s, silently dropping every icon past index 31.

Fix: remove the break;, iterate all ACHIEVEMENTS blocks.

3. .unwrap() panics in load_achievements

Three call sites unwrap Result<&str, ()> from get_achievement_display_attribute and Result<bool, ()> from get(). Either fails for any achievement whose status hasn't loaded yet (which, before fix #1, was every achievement on every game). The panic is caught by catch_unwind and converted to Err, which main.rs maps to Vec::new(). So a single transient hiccup gives the UI "0/0" with no error surfacing.

Also: user_stats.get_achievement_names() panics inside steamworks-rs when the game has zero achievements (it calls .expect() on get_num_achievements). This crashes Samira when the user picks a no-achievement game.

Fix: replace .unwrap() with .unwrap_or_default() / .unwrap_or(false), and short-circuit load_achievements when get_num_achievements() returns 0.

Test plan

  • Builds clean on Linux (cargo check + full tauri build)
  • Elden Ring (AppID 1245620, 42 achievements): before = 0/42 with no error, with only fix 1 = 21/42 (half missing icons), with all three = 42/42 ✅
  • Counter-Strike 2 (AppID 730, 1 achievement): unchanged ✅
  • Game with no achievements: before = SIGABRT, after = renders "0/0" cleanly ✅
  • panic::catch_unwind boundary preserved, so any unexpected residual panic still surfaces as Err(String) instead of crashing the process

Three independent bugs that combine to make the tool return empty
achievement lists for most non-trivial games. All happen to be hidden
by happy-path luck on small/simple games, which is why upstream tests
pass.

1. CallbackHandle leak in start_client.

   client.register_callback(...) returns a CallbackHandle whose Drop
   impl synchronously unregisters the callback (see
   steamworks-rs 0.12.2/src/callback.rs:143). Upstream discards the
   return value, so the UserStatsReceived handler is removed before
   Steam can ever fire it. The 1-second wait loop then always times
   out, and load_achievements proceeds against unready stats. This is
   fixed by binding the handle to a named local for the duration of
   the wait loop.

   Also reorder request_user_stats() to fire AFTER the callback is
   registered, otherwise a fast Steam pipe can race and we'd miss the
   event even with the handle held.

2. break; after first ACHIEVEMENTS block in load_achievement_icons.

   Steam represents achievements as 32-bit bitfields. Games with more
   than 32 achievements (Elden Ring: 42, Skyrim: 75, etc.) have
   multiple ACHIEVEMENTS entries in the schema. The current loop reads
   the first block, then `break;`s, silently dropping every icon past
   index 31. Removing the break iterates them all.

3. .unwrap() panics in load_achievements.

   Three call sites unwrap Result<&str, ()> from
   get_achievement_display_attribute and Result<bool, ()> from get().
   Either fails for any achievement whose status hasn't loaded yet
   (which, before fix jsnli#1, was every achievement). The panic is caught
   by catch_unwind and converted to Err, which main.rs maps to
   Vec::new(). So a single transient hiccup gives the UI "0/0" with
   no error surfacing.

   Replace with unwrap_or_default / unwrap_or(false), and short-circuit
   when get_num_achievements() returns 0 (steamworks-rs panics inside
   get_achievement_names for zero-achievement games).

Tested locally against Elden Ring (42), Counter-Strike 2 (1), and a
game with no achievements - all now load correctly. Before the fix,
Elden Ring loaded 0/42 with no error; with only fix 1, 21/42 (half
the icons missing); with all three, 42/42 correctly.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Suggestion: Allow searching by AppID

1 participant