Steamworks.NET
Steamworks.NET copied to clipboard
Steamworks SDK 1.6.1 had a silent destructive change and broke my project
Hello again! I updated Steamworks.NET recently, and that updated Steamworks SDK to 1.61... then my existing call to SteamUserStats.RequestCurrentStats() broke without warning - sudden compilation errors telling me that the method was missing.
Digging around the code I found in the commit https://github.com/rlabrecque/Steamworks.NET/commit/5289ecd8a3de6c0076efe096c36b95087c441eff that this line from the original Steamworks SDK header changed in a (rather rude...) manner: https://github.com/rlabrecque/Steamworks.NET/commit/5289ecd8a3de6c0076efe096c36b95087c441eff#diff-dc82e0526ee3d1bfcc0a10a2412b3353abfc1ea6112e7dab2d964faf1a2a8338R92-R95
which seems to have caused a cascade of auto-generation which ended up a function getting Thanos-snapped on the C# side: https://github.com/rlabrecque/Steamworks.NET/commit/5289ecd8a3de6c0076efe096c36b95087c441eff#diff-f8595db8ca453f1392bee55c01116232b687915838c3df68a895a157a8fa2729L3059
Totally not your fault—this came from upstream and your generator is doing what it's supposed to. But I was wondering if there could be a way to mark a function Obsolete if a function gets Thanos-snapped like this in the future. I'm currently considering a method where existing functions are saved in the repo, and the next time the auto generator runs, it compares detected symbols and adds the 'Obsolete' attribute to them.
The C# function should have looked like this rather than getting yeeted into /dev/null outright:
// delete: [DllImport(NativeLibraryName, EntryPoint = "SteamAPI_ISteamUserStats_RequestCurrentStats", CallingConvention = CallingConvention.Cdecl)]
// delete: [return: MarshalAs(UnmanagedType.I1)]
// add the following
[Obsolete("This function was deleted in the latest Steamworks SDK. Consult (header name here)")]
// mark it non-extern and return as successful
public static bool ISteamUserStats_RequestCurrentStats(IntPtr instancePtr) { return true; }
(though I'm not sure this works because I'm not used to DllImport calls...)
What do you think about this?
What's the point of a Steamworks.NET method that doesn't end up calling a Steamworks method because it no longer exists?
The core SDK is backwards compatible so if you stay on an older SDK it should continue to work. But when you update and the method is gone, then there is nothing to call. Things can be removed or changed in an upgrade (e.g. changing arguments or struct members) and dealing with those compilation errors are a part of the process.
I understand your concern @yaakov-h, but there's a good reason why APIs typically aren't removed without warning and is kept for a while, or redirected to the new one, or replaced with no-op.
But when you update and the method is gone, then there is nothing to call
Sure, it's natural to expect that deprecated APIs will eventually be removed. But outright removing a function is generally discouraged even during an upgrade unless it's been clearly communicated in advance. I found this article by Tim Perry. This article discusses about HTTP APIs, but it nicely explains why it's a bad idea, and the principles apply here too.
Removing a function is classified as a 'breaking change'. In most mature ecosystems, removing an API is paired with a "deprecation phase" or "grace period" unless the update itself is marked 'major', because maintainers (Valve in this case) can't know how many downstream users (us in this case) rely on that given function. You cannot estimate the damage it would do if you decide to delete it outright. To minimise the damage, the usual approach is:
- Mark the API as deprecated or obsolete.
- Either emit compiler warnings/errors with migration guidance, or redirect calls internally while alerting the developer.
This lets users migrate safely, instead of being backhanded by broken builds. In my case, I spent time assuming something had gone wrong during the SDK update process until I realized Valve had silently removed the function. That’s a pretty disruptive experience for anyone depending on the wrapper.
To be clear: I'm not blaming Riley or this repo. The breakage stems from Valve’s side, and this repo just faithfully reflects that via autogen. My suggestion is only that, now we know functions can disappear without warning, it might be worth adding a detection step in the generator to surface missing symbols, maybe by emitting [Obsolete] placeholders or logging what changed. This would help soften future migration pains.
The core SDK is backwards compatible so if you stay on an older SDK it should continue to work.
That's also true, but staying on an older SDK isn't always an option. If you're trying to support new features, you need the latest SDK. Or if there is a security concern, you also need to update.
The same principles for HTTP APIs don't really apply here. This is a different world - one of binary compatibility and source compatibility.
Valve kept binary compatibility The old API (ISteamUserStats012.RequestCurrentStats) still exists. If you call it, it probably does nothing, maybe it still does fire off a message to the servers.
The new build of Steamworks uses a newer API, ISteamUserStats013, which does not have a RequestCurrentStats method. If you update the SDK, that's on you. Games still using v12 will continue to function, games that want to adopt v13 have to deal with a breaking change.
This is not the first time and unlikely to be the last time that something like this happens. The model of how things work in Steamworks changes year to year, and although Valve keep the old games spinning, if you take an update that's an explicit action to adopt whatever changes are coming.
If you're trying to support new features, you need the latest SDK.
And now you also need to deal with a world that has new feature X, and new feature Y, and where game stats and achievements come pre-synchronized so RequestCurrentStats is needless.
Hey @yaakov-h, thanks for the thoughtful replies — I really appreciate you taking the time to explain your perspective in detail.
I can see where you’re coming from: if Steamworks.NET is meant to be a strict 1:1 mirror of the SDK, then removing a method as soon as Valve removes it makes sense, even if it causes breakage on upgrade. On the other hand, I brought up the [Obsolete] idea from a developer-experience angle, mostly as a safeguard for those caught off guard by transitive or indirect SDK updates.
That said, we might be operating under slightly different assumptions about what Steamworks.NET is supposed to be. Rather than go in circles, I’d like to tag @rlabrecque for clarification:
@rlabrecque, do you view Steamworks.NET as a strict SDK mirror, or is there some room for developer-experience-friendly accommodations, such as marking removed methods [Obsolete] before their full removal in a later version?
I'm 100% fine either way. I'd like to understand what direction this library is intended to take before suggesting any more changes. Thanks again, both of you!
Hey Clpsplug, Steamworks.NET is /largely/ a 1:1 mirror of Valve's Steamworks SDK indeed, every SDK upgrade is "automatically" regenerated from scratch so there isn't really any concept of what existed in a previous SDK after regenerating, nor any real sensible way to provide compatibility.
I do think it's a little silly how Valve handles deprecation with no warning; and it also puts us in an awkward spot with our version numbers on our side especially as more people move towards package managers like nuget/unity, which there's another thread about.