<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: zevnda</title>
    <description>The latest articles on DEV Community by zevnda (@zevnda).</description>
    <link>https://dev.to/zevnda</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3583727%2F7e049de6-2acf-49d0-90e2-03ffa448d09a.png</url>
      <title>DEV Community: zevnda</title>
      <link>https://dev.to/zevnda</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zevnda"/>
    <language>en</language>
    <item>
      <title>Parsing Steam Achievement Sets: DLC ownership, archived achievements and a batch-write gotcha</title>
      <dc:creator>zevnda</dc:creator>
      <pubDate>Fri, 30 Oct 2026 00:10:21 +0000</pubDate>
      <link>https://dev.to/zevnda/steam-achievement-sets-broke-our-bulk-unlocks-what-changed-in-the-schema-and-how-we-fixed-it-4h8e</link>
      <guid>https://dev.to/zevnda/steam-achievement-sets-broke-our-bulk-unlocks-what-changed-in-the-schema-and-how-we-fixed-it-4h8e</guid>
      <description>&lt;p&gt;On October 15, 2026, Valve put &lt;strong&gt;Achievement Sets&lt;/strong&gt; into the Steam Client Beta. Developers can now split a game's achievements into groups (the base game, each DLC, single-player vs multiplayer, and so on), and each group gets its own progress bar and its own "set complete" state.&lt;/p&gt;

&lt;p&gt;For players, that's mostly good news. For anyone writing code that reads or writes Steam achievements, there are two changes that matter:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The stats schema has a new section, and achievements now point into it.&lt;/li&gt;
&lt;li&gt;Steam's servers refuse to unlock an achievement from a DLC set the account doesn't own, or from an archived set.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I maintain &lt;a href="https://steamgameidler.com" rel="noopener noreferrer"&gt;Steam Game Idler&lt;/a&gt;, a desktop app (Tauri + Rust, with a C# helper that talks to Steam) that includes an achievement manager. The second change broke "unlock all" for some games on the day the beta landed. This post covers what we found in the schema, why one rejected achievement took down a whole batch, and the fix.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you only want the player-facing side (how sets count toward completion, how to join the beta, which games support it), I wrote that up separately: &lt;strong&gt;&lt;a href="https://steamgameidler.com/blog/steam-achievement-sets" rel="noopener noreferrer"&gt;Steam Achievement Sets: what the beta changes for achievement hunters&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The feature in one paragraph
&lt;/h2&gt;

&lt;p&gt;A game can have up to 40 sets, counting the base game set. There are &lt;strong&gt;base game sets&lt;/strong&gt;, which everyone can earn, and &lt;strong&gt;DLC sets&lt;/strong&gt;, which only count toward your completion once you own that DLC. Developers can also &lt;strong&gt;archive&lt;/strong&gt; an achievement or a whole set when it can no longer be earned (a shut-down online mode, a past seasonal event). Archived achievements stay on the profiles of people who earned them and stop blocking everyone else's 100%. According to the &lt;a href="https://partner.steamgames.com/doc/features/achievements/ach_guide" rel="noopener noreferrer"&gt;Steamworks docs&lt;/a&gt;, all of it is configured in Steamworks, with no changes to the game's code.&lt;/p&gt;

&lt;p&gt;That last point is why it hit third-party tools first. Nothing about the game's binary changes. Only the schema and the server-side rules do.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed in the schema
&lt;/h2&gt;

&lt;p&gt;Steam describes a game's achievements and stats in a binary KeyValues (VDF) schema. It's the same format whether you read the local client's cached &lt;code&gt;UserGameStatsSchema_&amp;lt;appid&amp;gt;.bin&lt;/code&gt; or receive schema bytes over the wire from Steam's CM servers. Achievements live as bits inside stats of type 4/5, under &lt;code&gt;stats&lt;/code&gt; → &lt;code&gt;&amp;lt;stat id&amp;gt;&lt;/code&gt; → &lt;code&gt;bits&lt;/code&gt; → &lt;code&gt;&amp;lt;bit number&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Achievement Sets add a top-level &lt;code&gt;groups&lt;/code&gt; section next to &lt;code&gt;stats&lt;/code&gt;, and a &lt;code&gt;groupid&lt;/code&gt; on each achievement bit. Roughly (shown as text VDF for readability, based on a freshly downloaded Euro Truck Simulator 2 schema):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"227300"
{
    "stats"
    {
        "1"
        {
            "bits"
            {
                "0"
                {
                    "name"     "ACH_SOME_DLC_ACHIEVEMENT"
                    "groupid"  "3"
                    "display"  { ... }
                }
            }
        }
    }
    "groups"
    {
        "3"
        {
            "name"      { "english" "DLC: ..." }
            "dlcappid"  "&amp;lt;the DLC's app id&amp;gt;"
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few details worth knowing if you're parsing this yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The base game set is implicit.&lt;/strong&gt; Group &lt;code&gt;0&lt;/code&gt; never appears in &lt;code&gt;groups&lt;/code&gt;. Any achievement with no &lt;code&gt;groupid&lt;/code&gt;, or with a &lt;code&gt;groupid&lt;/code&gt; that isn't listed, belongs to the base game set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A DLC set is a group with a &lt;code&gt;dlcappid&lt;/code&gt;.&lt;/strong&gt; That's the only thing that distinguishes it from a base game set like "Driving Academy".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;archived&lt;/code&gt; exists per achievement&lt;/strong&gt; as an int &lt;code&gt;1&lt;/code&gt; on the bit itself. Team Fortress 2's schema already has four archived achievements (the Replay "YouTube views" ones). We also read an &lt;code&gt;archived&lt;/code&gt; key on a whole group, but as of this writing we haven't seen a real archived set in the wild, so that key name is an educated guess. A missing key reads as "not archived", so a wrong guess fails harmlessly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's purely additive.&lt;/strong&gt; A schema without sets has no &lt;code&gt;groups&lt;/code&gt; key at all. Old parsers keep working; they just don't know about sets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The parsing itself is small. Here's the group half, lightly trimmed from our C# helper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AchievementGroupDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ParseAchievementGroups&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;uint&lt;/span&gt; &lt;span class="n"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;schemaBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;language&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"english"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AchievementGroupDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;kv&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;KeyValue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LoadFromBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;schemaBytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;groups&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;?[&lt;/span&gt;&lt;span class="n"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CultureInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InvariantCulture&lt;/span&gt;&lt;span class="p"&gt;)][&lt;/span&gt;&lt;span class="s"&gt;"groups"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;groups&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="n"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Valid&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Children&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// no sets: everything is in the implicit base game set&lt;/span&gt;

    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="k"&gt;group&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Children&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="k"&gt;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Valid&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;groupId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;dlcAppId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;group&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"dlcappid"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;AsInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;AchievementGroupDto&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;groupId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ResolveLocalizedString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;group&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;DlcAppId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dlcAppId&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;dlcAppId&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Archived&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;group&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"archived"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;AsBoolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The part that broke: one bad unlock sinks the whole batch
&lt;/h2&gt;

&lt;p&gt;Valve's documentation is explicit that Steam "will not unlock an achievement in a DLC set for a player who can't play that DLC, or an achievement in an archived set." That check runs on Steam's servers, so no client-side tool can get around it.&lt;/p&gt;

&lt;p&gt;The rule itself is fine. What caught us out was the failure mode. An "unlock all" writes every achievement in a single batched stats store. On Euro Truck Simulator 2, with an account that didn't own one of the DLCs, Steam didn't skip the DLC achievements. It rejected &lt;strong&gt;the entire write&lt;/strong&gt; with &lt;code&gt;EResult.InvalidParam&lt;/code&gt;. One unowned DLC achievement meant zero achievements unlocked.&lt;/p&gt;

&lt;p&gt;We weren't the only ones. Within days, Steam Achievement Manager users &lt;a href="https://github.com/gibbed/SteamAchievementManager/issues/651" rel="noopener noreferrer"&gt;reported it "not working"&lt;/a&gt; on games in the sets beta, and the explanation in that thread is the same ownership check.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: know ownership before you write
&lt;/h2&gt;

&lt;p&gt;The fix is to filter the batch before sending it, which means knowing which DLC the account owns. Our app has two backends, and they answer that question differently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With a local Steam client (Steamworks.NET):&lt;/strong&gt; ask the client directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;groups&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SchemaWalker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ParseAchievementGroups&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;group&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DlcAppId&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kt"&gt;uint&lt;/span&gt; &lt;span class="n"&gt;dlcAppId&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;group&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Owned&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SteamApps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BIsSubscribedApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AppId_t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dlcAppId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;group&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gotcha here is picking the right call. &lt;code&gt;BIsDlcInstalled&lt;/code&gt; looks like the obvious choice, but it reports an owned DLC that isn't downloaded as unowned. &lt;code&gt;BIsSubscribedApp&lt;/code&gt; answers for the signed-in account whether or not the DLC is installed, including DLC borrowed through Family Sharing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Without a local client (SteamKit2, talking to Steam's servers directly):&lt;/strong&gt; there's no &lt;code&gt;BIsSubscribedApp&lt;/code&gt;. We already resolve the account's licenses through PICS to build the owned games list, and a package's &lt;code&gt;appids&lt;/code&gt; include its DLC. So DLC ownership is a set lookup against data we already have. The lookup is skipped entirely for games with no DLC sets.&lt;/p&gt;

&lt;p&gt;Then the bulk unlock skips anything Steam would reject:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;unownedDlcGroupIds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;unlock&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;AchievementGroupDto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UnownedDlcGroupIds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;HashSet&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;achievement&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;achievements&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;achievement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Achieved&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;unlock&lt;/span&gt;
        &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;achievement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProtectedAchievement&lt;/span&gt;
        &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unlock&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;achievement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Archived&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;achievement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GroupId&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;groupId&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;unownedDlcGroupIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;groupId&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;skipped&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;achievement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;toChange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;achievement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unlock&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The check only applies to &lt;strong&gt;unlocking&lt;/strong&gt;. Locking an achievement you've already earned, archived or not, is still allowed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fail open, not closed
&lt;/h3&gt;

&lt;p&gt;The ownership lookup is a network call, and network calls flake. We model ownership as a nullable bool:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;true&lt;/code&gt;: owned, unlock normally.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;false&lt;/code&gt;: known unowned, skip.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;null&lt;/code&gt;: not a DLC set, &lt;strong&gt;or the lookup failed&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;null&lt;/code&gt; is treated as owned. If PICS times out, we send the unlock and let Steam decide, rather than quietly refusing achievements the user might own. A missed skip costs one rejected write that the user can retry. A false skip silently blocks something that would have worked, and nobody would ever report it. The failure gets logged as a warning, so it shows up in the user's log file if it keeps happening.&lt;/p&gt;

&lt;h2&gt;
  
  
  Showing it in the UI
&lt;/h2&gt;

&lt;p&gt;Once groups are parsed, the UI follows from them. The achievement manager lists &lt;strong&gt;All achievements&lt;/strong&gt;, the &lt;strong&gt;Base game&lt;/strong&gt; set and each DLC or other set in a sidebar. Achievements from DLC you don't own stay visible but are marked, never sent in an unlock, and left out of the game's progress count, which matches how Steam now counts completion. In the automated unlocker, they sit in a separate "From DLC you don't own" section outside the unlock order, so a long run never stalls on an unlock Steam was always going to reject.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff8i4nctnvzfcnltecvn3.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff8i4nctnvzfcnltecvn3.webp" alt="Steam Game Idler's Achievement Manager for American Truck Simulator, listing achievements by base game and DLC set" width="800" height="557"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways if you touch Steam achievements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Parse &lt;code&gt;groups&lt;/code&gt; and &lt;code&gt;groupid&lt;/code&gt;&lt;/strong&gt; if you display achievements. A missing or unknown &lt;code&gt;groupid&lt;/code&gt; means the base game set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filter batched writes yourself.&lt;/strong&gt; Steam rejects the entire batch on one unowned-DLC or archived achievement, not just the offending one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;BIsSubscribedApp&lt;/code&gt;, not &lt;code&gt;BIsDlcInstalled&lt;/code&gt;&lt;/strong&gt;, for ownership.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decide what a failed ownership lookup means&lt;/strong&gt; before it happens. We chose fail open, and logged it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expect the beta to move.&lt;/strong&gt; The archived-set key is still unconfirmed, and Valve has said it'll keep changing things during the beta.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you play achievement-heavy games and want to manage achievements set by set, &lt;a href="https://steamgameidler.com/download" rel="noopener noreferrer"&gt;Steam Game Idler&lt;/a&gt; is free and already supports sets. The full player-side guide, with the list of games that launched with sets and how to join the beta, is on the &lt;a href="https://steamgameidler.com/blog/steam-achievement-sets" rel="noopener noreferrer"&gt;Steam Game Idler blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>csharp</category>
      <category>dotnet</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Steam Game Idler — an Open-Source Alternative to ArchiSteamFarm, Steam Achievement Manager, and Idle Master.</title>
      <dc:creator>zevnda</dc:creator>
      <pubDate>Sun, 26 Oct 2025 15:28:53 +0000</pubDate>
      <link>https://dev.to/zevnda/how-i-built-an-open-source-alternative-to-archisteamfarm-and-why-you-should-care-37od</link>
      <guid>https://dev.to/zevnda/how-i-built-an-open-source-alternative-to-archisteamfarm-and-why-you-should-care-37od</guid>
      <description>&lt;p&gt;If you're a PC gamer with a decent Steam library, you've probably noticed those trading cards piling up. Some people ignore them. Others manually idle games for hours to farm cards worth a few cents each. And then there are developers who thought: "There's got to be a better way."&lt;/p&gt;

&lt;p&gt;That's where &lt;strong&gt;Steam Game Idler (SGI)&lt;/strong&gt; comes in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Steam's Card Economy is Tedious
&lt;/h2&gt;

&lt;p&gt;Steam's trading card system has been around for over a decade. The concept is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Play games → earn trading card drops&lt;/li&gt;
&lt;li&gt;Collect full sets → craft badges&lt;/li&gt;
&lt;li&gt;Badges → showcase on your profile (and sweet, sweet XP)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But here's the catch: &lt;strong&gt;you need to actually "play" the game&lt;/strong&gt; for 2+ hours to get all card drops. For someone with 500+ games, that's... impractical.&lt;/p&gt;

&lt;p&gt;Enter idling tools like ArchiSteamFarm (ASF) and Idle Master. They simulate gameplay to trigger card drops. Problem solved, right?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not quite.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Existing Solutions Fall Short
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;ArchiSteamFarm (ASF):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Powerful but complex (JSON config files, command-line heavy)&lt;/li&gt;
&lt;li&gt;Designed for server environments and multiple accounts&lt;/li&gt;
&lt;li&gt;Steep learning curve for casual users&lt;/li&gt;
&lt;li&gt;No GUI by default (requires third-party plugins)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Idle Master:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simple but abandoned (last update: 2016)&lt;/li&gt;
&lt;li&gt;No active development or security patches&lt;/li&gt;
&lt;li&gt;Lacks modern features like achievement management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Steam Achievement Manager (SAM):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Great for achievements, but no card farming&lt;/li&gt;
&lt;li&gt;Separate tool = switching between apps&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Enter Steam Game Idler: A Modern, User-Friendly Alternative
&lt;/h2&gt;

&lt;p&gt;I built &lt;a href="https://github.com/zevnda/steam-game-idler" rel="noopener noreferrer"&gt;Steam Game Idler&lt;/a&gt; to solve these problems. Here's what makes it different:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. &lt;strong&gt;Built with Modern Tech&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tauri + TypeScript + Rust&lt;/strong&gt; = fast, secure, lightweight&lt;/li&gt;
&lt;li&gt;Native Windows app (not browser-based or Electron)&lt;/li&gt;
&lt;li&gt;Uses official Steamworks SDK (not reverse-engineered APIs)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. &lt;strong&gt;All-in-One Functionality&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Why juggle multiple tools? SGI combines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Card Farming:&lt;/strong&gt; Idle up to 32 games simultaneously (Steam's max)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Achievement Manager:&lt;/strong&gt; Manually lock/unlock achievements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Achievement Unlocker:&lt;/strong&gt; Auto-unlock with human-like timing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Playtime Booster:&lt;/strong&gt; Increase game hours for... reasons&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trading Card Manager:&lt;/strong&gt; View inventory, sort by rarity, bulk sell&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. &lt;strong&gt;Security First&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fully open-source&lt;/strong&gt; (inspect the code yourself)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AES-256-GCM encryption&lt;/strong&gt; for stored credentials&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No telemetry or data collection&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Uses official Steam APIs (minimizes ban risk)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. &lt;strong&gt;User Experience&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Clean, intuitive GUI (no config files!)&lt;/li&gt;
&lt;li&gt;Real-time card drop notifications&lt;/li&gt;
&lt;li&gt;One-click setup (no command-line required)&lt;/li&gt;
&lt;li&gt;Portable version available (no installation needed)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Technical Deep Dive: How It Works
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Card Farming Logic
&lt;/h3&gt;

&lt;p&gt;SGI uses Steam's official &lt;code&gt;ISteamClient&lt;/code&gt; interface to spoof game sessions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simplified example from the Rust backend&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;start_idling&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app_ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;app_id&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;app_ids&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;steam_client&lt;/span&gt;&lt;span class="nf"&gt;.run_app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tool registers game sessions with Steam's servers, triggering card drop timers without actually launching the games. It respects Steam's 32-game limit to avoid detection.&lt;/p&gt;

&lt;h3&gt;
  
  
  Achievement Management
&lt;/h3&gt;

&lt;p&gt;Unlike older tools that directly modify Steam's local achievement cache (risky), SGI uses the &lt;code&gt;ISteamUserStats&lt;/code&gt; API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// TypeScript example&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;unlockAchievement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;achievementName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;success&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;steamworks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAchievement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;achievementName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;steamworks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;storeStats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Commit to Steam&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach mimics how games legitimately unlock achievements, reducing ban risk.&lt;/p&gt;

&lt;h3&gt;
  
  
  Human-Like Behavior
&lt;/h3&gt;

&lt;p&gt;The auto-achievement unlocker includes randomized delays:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Waits 5-30 minutes between games&lt;/li&gt;
&lt;li&gt;Unlocks achievements in random order&lt;/li&gt;
&lt;li&gt;Adds 30-120 second delays between unlocks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This prevents the obvious "all 50 achievements unlocked in 2 minutes" pattern that might flag automated behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Open Source Matters
&lt;/h2&gt;

&lt;p&gt;Steam idling tools have a bad reputation. Some are malware. Others are abandoned. Many require you to trust a random .exe file with your Steam credentials.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SGI is fully open-source on GitHub.&lt;/strong&gt; You can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Audit the code before running it&lt;/li&gt;
&lt;li&gt;Build from source if you don't trust pre-compiled binaries&lt;/li&gt;
&lt;li&gt;Contribute features or report bugs&lt;/li&gt;
&lt;li&gt;Fork it and customize for your needs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The project has 120+ GitHub stars and active community discussions. Transparency builds trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Use Cases
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;For collectors:&lt;/strong&gt; Automatically farm cards to complete badge sets without babysitting your PC.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For achievement hunters:&lt;/strong&gt; Unlock time-consuming achievements (like "play 1000 hours") without actually playing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For profile showcases:&lt;/strong&gt; Boost playtime on favorite games to show off dedication.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For broke gamers:&lt;/strong&gt; Farm cards, sell on marketplace, buy more games. (Seriously, I've made $50+ doing this.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Download:&lt;/strong&gt; &lt;a href="https://github.com/zevnda/steam-game-idler/releases/latest" rel="noopener noreferrer"&gt;Latest release on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Install:&lt;/strong&gt; Run the installer (or use portable version)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Login:&lt;/strong&gt; Enter Steam credentials (stored locally, encrypted)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start farming:&lt;/strong&gt; Select games and click "Start Idling"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Full documentation at &lt;a href="https://steamgameidler.com" rel="noopener noreferrer"&gt;steamgameidler.com&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Ethics Question
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;"Isn't this cheating?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Fair question. Here's my take:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Card farming:&lt;/strong&gt; Steam allows idling. Valve hasn't banned users for it in 10+ years.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Achievement unlocking:&lt;/strong&gt; You're only "cheating" yourself. No competitive advantage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terms of Service:&lt;/strong&gt; Steam's ToS is vague on automation. Use at your own risk.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I built this tool for personal use and decided to share it. If you're uncomfortable with the ethics, don't use it. But if you value your time and want to optimize a grindy system, SGI exists for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Roadmap &amp;amp; Contributing
&lt;/h2&gt;

&lt;p&gt;Current priorities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Linux/Mac support (Tauri makes this feasible)&lt;/li&gt;
&lt;li&gt;Cloud save sync for multi-device users&lt;/li&gt;
&lt;li&gt;Automated badge crafting&lt;/li&gt;
&lt;li&gt;Integration with Steam Market API for pricing analytics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Want to contribute? Check out the &lt;a href="https://github.com/zevnda/steam-game-idler" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt;. Issues and PRs are welcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Building Steam Game Idler taught me a lot about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Working with proprietary APIs&lt;/li&gt;
&lt;li&gt;Balancing UX with power-user features&lt;/li&gt;
&lt;li&gt;Security best practices for handling credentials&lt;/li&gt;
&lt;li&gt;The importance of open-source transparency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're tired of juggling multiple Steam tools or want a modern alternative to ASF, give SGI a shot. It's free, open-source, and actively maintained.&lt;/p&gt;

&lt;p&gt;And hey, if you make a few bucks selling trading cards, consider starring the repo. 😄&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Resources:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/zevnda/steam-game-idler" rel="noopener noreferrer"&gt;Steam Game Idler GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://steamgameidler.com" rel="noopener noreferrer"&gt;Official Website &amp;amp; Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/zevnda/steam-game-idler/releases/latest" rel="noopener noreferrer"&gt;Latest Release&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;What tools do you use for Steam automation? Drop a comment below!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>gamedev</category>
      <category>typescript</category>
      <category>rust</category>
    </item>
  </channel>
</rss>
