Steam and Steam Deck: sources and measurements
The research behind the steam and console-cert (Steam Deck) skills, the textsize runner, the steam-removed-api and
steam-env-dotnet architecture rules, doctor's GDExtension, Steam package and Steamworks file checks, the SteamAppId=480 default,
and the C# example's Platform/ (IPlatformServices, LocalPlatform, BreakoutProgress) and SaveStore.SaveResolved,
gathered on 2026-10-21. Tests ran on Godot 4.7.2 mono and standard, .NET 8 (SDK 8.0.131), Linux x86_64, with no Steam
client and no Steamworks partner account. Marks: tested means run here, doc means stated by Valve or Godot,
source means read in the code, and unconfirmed means memory, community knowledge or inference not checked here.
Sources
| Source | What it gave |
|---|---|
GodotSteam, codeberg.org/godotsteam/godotsteam (releases, readme, changelog, license.md) |
4.23.2-gde of 2026-10-20 for "Godot 4.4+ - Steamworks 1.65"; MIT; removed functions; the 4.23.2 macOS file names; a "No LLM Policy" for issues and pull requests |
| GodotSteam documentation, godotsteam.com (tutorial, Common Issues, C-Sharp Tools) | GDScript examples; app ID 480 for testing; the C# options; still lists isSteamRunningOnSteamDeck() |
| GodotSteam C# Bindings, github.com/LauraWebdev/GodotSteam_CSharpBindings (MIT) | A generated typed wrapper over Call; supports "the GodotSteam 4.22 plugin" |
| Steamworks.NET (rlabrecque, MIT) and the fork Steamworks.NET.AnyCPU, github.com/Akarinnnnn/Steamworks.NET.AnyCPU (MIT) | SDK 1.65 headers and enums; the AnyCPU package's natives per runtime; the native resolver fix of 2026-10-21 |
NuGet packages Steamworks.NET 2024.8.0, Steamworks.NET.AnyCPU 2026.164.4 to 2026.165.2, Facepunch.Steamworks 2.3.3 |
What each package contains (listings); which load in Godot 4.7.2 mono |
| Facepunch.Steamworks repository (commit of 2026-10-13) | Per-OS assemblies; SteamClient.Init sets SteamAppId through .NET |
| Steamworks SDK access agreement, partner.steamgames.com/documentation/sdk_access_agreement | What may be redistributed; no software that replaces the SDK |
Steamworks documentation: api/steam_api, features/auth, ISteamUser, ISteamUserStats, ISteamFriends, ISteamApps, ISteamInput, ISteamUtils, features/cloud, features/enhancedrichpresence, store/application/dlc, store/application/depots, branches, demos, sdk/uploading/distributing_gs |
Init and relaunch rules, authentication, stats, leaderboards, rich presence, Cloud, DLC, depots, branches, demos and servers |
| Steam Web API, AuthenticateUserTicket, SetLeaderboardScore | Server-side ticket checks; trusted leaderboard writes |
Steam hardware compatibility, doc/steamhardware/compat (doc/steamdeck/compat serves the same text), the recommendations page and steamdeck/faq |
One review for Deck, Machine and other SteamOS devices; the 9 px text rule; mixed input; suspend advice |
| Steam Support FAQ 68D2-35AB-09A9-7678 | The Cloud conflict and "Unable to sync" dialogs |
| steamcommunity.com/dev/testrichpresence | The rich presence test page |
| Steamworks.NET-Example | Spacewar's (480) achievement and stat names |
game-ci/steam-deploy action.yml (commit of 2026-09-09) |
Depot IDs, built-in exclusions, the CLI it runs |
Godot 4.7.2 class reference and source (DisplayServer, data paths, os_linuxbsd.cpp, SDL's SDL_hidapi_steamdeck.c) |
No virtual keyboard on Linux; user:// locations; no built-in Deck detection |
Bindings in Godot 4.7.2 mono (tested)
| Binding | Loads in 4.7.2 mono | Natives in the export | Notes |
|---|---|---|---|
GodotSteam 4.23.2 GDExtension, raw Call or the C# wrapper |
Yes | Next to the executable, from [dependencies] in godotsteam.gdextension |
MIT, active; string calls; macOS file names wrong; the wrapper lags one release |
Steamworks.NET.AnyCPU 2026.164.4 |
Yes, project run and exported Linux build | data_<name>_<platform>/ |
MIT fork, one maintainer; typed one-to-one Steamworks API |
Steamworks.NET.AnyCPU 2026.165.0 and 2026.165.2 |
No on Linux: every call throws DllNotFoundException |
Same | Fixed in commit 374c3aa on 2026-10-21, not released |
Facepunch.Steamworks built from source |
Exported builds yes; a project run needs a native resolver | Per-OS ProjectReference |
NuGet 2.3.3 is "For Windows x64" only |
| GodotSteam module (pre-compiled editor) | No: no mono builds; 4.23.2's templates and macOS assets are listed with size 0 | Custom templates | GDScript only in practice |
- GodotSteam from C#:
Engine.HasSingleton("Steam")is true andget_godotsteam_version()returns4.23.2;steamInitEx(480, false)without a client returns{"status":1,"verbal":"Failed to load module '/root/.steam/sdk64/steamclient.so'"}. The community wrapper (Steam.GetSingleton(),SteamInitEx(480, false),IsRunningOnSteamHardware()) works against 4.23.2 and compiles with hundreds of CS8618 warnings under<Nullable>. - Calls after a failed GodotSteam init do not crash; each logs a Godot error and returns a default, for example
ERROR: Friends class not found, Steam may not be initialized: getPersonaName. - GodotSteam 4.23.2's
godotsteam.gdextensionnamesosx/libgodotsteam.macos.template_debug.dylib, but the zip shipslibgodotsteam.macos.template_debug.universal.dylib(release the same), although its changelog says "Fixed: incorrect name of MacOS file in .gdextension file". 4.21 removedisSteamRunningOnSteamDeck(); 4.23.2 hasisRunningOnSteamHardware,getSteamHardwareDefaultConfig,isRunningUnderProtonand norequestCurrentStats. - NuGet
Steamworks.NETis not rlabrecque's: newest 2024.8.0, "building against Steamworks SDK 1.60", and onlySteamworks.NET.dllper runtime, no native libraries. - The AnyCPU failure: the resolver stripped a synthetic
AnyCPUprefix from the library name on Windows only, so Linux looks forlibAnyCPU`steam_api.so(source). The fix's commit is "Properly strip AnyCPU prefix for every platform". - Godot loads game assemblies from memory in a project run (
Assembly.Locationis empty,AppContext.BaseDirectoryis.godot/mono/temp/bin/Debug/), so Facepunch'sDllImportfinds nothing there; aNativeLibrary.SetDllImportResolverthat loads fromAppContext.BaseDirectoryfixes it. Wiring Facepunch's Win64 project and copyingsteam_api64.dllagain fails the publish withNETSDK1152. - Setting
SteamAppIdwith .NET'sEnvironment.SetEnvironmentVariablenever reached the native Steam API on Linux:RestartAppIfNecessary(480)still returned true andOS.GetEnvironmentsaw nothing. After Godot'sOS.SetEnvironmentit returned false. Facepunch'sSteamClient.Inituses the .NET call (source). - The SDK agreement licenses the
redistributable_binfiles to ship with the game; a licensee "will not ... develop software to replace the SDK's functionality" (doc), which rules out Steam emulators.
Start-up and stats (doc, with measurements)
SteamAPI_InitExreturnsESteamAPIInitResult: OK 0, FailedGeneric 1 ("Some other failure"), NoSteamClient 2 ("We cannot connect to Steam, steam probably isn't running"), VersionMismatch 3 (source). The partner pageapi/steam_apidocuments onlySteamAPI_Init.- Without a Steam client all three bindings returned FailedGeneric (1) with
Failed to load module '/root/.steam/sdk64/steamclient.so', not NoSteamClient (tested). RestartAppIfNecessary: "you should quit your process as soon as possible" when it returns true; "if a steam_appid.txt file is present then this will return false regardless"; "Make sure to remove the steam_appid.txt file when uploading" (doc). Measured without a client:
| Environment | RestartAppIfNecessary(480) |
|---|---|
No steam_appid.txt, no SteamAppId |
True, and GodotSteam runs sh: 1: /root/.steam/root/steam.sh: not found; the game quits with exit code 0 |
steam_appid.txt in the working directory |
False |
SteamAppId=480 in the native environment (shell or OS.SetEnvironment) |
False |
SteamAppId=480 set with .NET's Environment.SetEnvironmentVariable |
True |
RunCallbacks: "Most games call this once per render-frame"; listeners run "in the callers thread context" (doc).RequestCurrentStatsis deprecated: "the Steam client will have loaded stats and achievements for your game prior to launch" (doc); Steamworks.NET 1.65 and GodotSteam 4.23 no longer have it. Valve'sfeatures/achievements/stats_guidepage still teaches calling it first, andResetAllStatsstill says to resync with it: those pages are stale against the SDK, so thesteam-removed-apifinding is right, and after aResetAllStatsin a test the stats are read again instead.SetAchievementandSetStatare "very cheap";StoreStats"can be rate limited. Call frequency should be on the order of minutes" and "is required to display the achievement unlock notification dialog" (doc).IndicateAchievementProgress: "Calling this function will NOT set the progress or unlock the achievement" (doc).- Leaderboards:
KeepBestkeeps the best for the board's sort order,ForceUpdatealways replaces; uploads are "rate limited to 10 uploads per 10 minutes" with one outstanding call. A "Trusted" board takes scores only through theSetLeaderboardScoreWeb API, which "MUST be called from a secure server" (doc). - Rich presence: Valve's two sources disagree on the key count. The
ISteamFriendspage says up to 20 keys, in the text and in its constants table (k_cchMaxRichPresenceKeys); the SDK 1.65 headeristeamfriends.h(line 123) defines the same constant as 30. Which one the Steam client enforces was not measured, so design for 20 (the C# example caps at 20). Keys up to 64 characters, values up to 256, in both. Ifsteam_display"is not set to a valid localization tag, then rich presence will not be displayed"; a token missing in a language falls back to English, and a%key%that is not set shows nothing (doc). - Authentication:
GetAuthTicketForWebApion the client,AuthenticateUserTicketon the server,CheckAppOwnershipfor ownership;GetAuthSessionTicketandBeginAuthSessionfor peers and game servers.features/authstill namesGetAuthSessionTicketwithAuthenticateUserTicketin two FAQ lines, against its own main flow (doc). - Spacewar (480), for manual tests: achievements
ACH_WIN_ONE_GAME,ACH_WIN_100_GAMES,ACH_TRAVEL_FAR_ACCUM,ACH_TRAVEL_FAR_SINGLE; statsNumGames,NumWins,NumLosses,FeetTraveled,MaxFeetTraveled,AverageSpeed(source). Anyone can run it, so its leaderboards and stats prove nothing.
Cloud, DLC, depots and branches (doc)
- Auto-Cloud: "Steam will automatically sync the groups of files when the application launches and exits". Valve
lists twelve roots:
App Install Directory,SteamCloudDocuments(a path Steam picks per OS),WinMyDocuments,WinAppDataLocal,WinAppDataLocalLow,WinAppDataRoaming,WinSavedGames,MacHome,MacAppSupport,MacDocuments,LinuxHomeandLinuxXdgDataHome. Leave a root's OS Restriction at its default, Cross-Platform; where the folder differs per OS, add Root Overrides (with an Add/Replace Path) instead. The byte quota and the file-count quota per user are two settings in App Admin. Godot'suser://lives under those roots (%APPDATA%,~/.local/share,~/Library/Application Support), but the mapping was not tested end to end. - "The Steam Cloud conflict window will be shown when a game is launched" and shows dates. Offline Mode "is very likely to cause a conflict"; after "Unable to sync", "play game" "could potentially cause a conflict". No API event tells the game which side the player chose (no such event is documented).
- With Dynamic Cloud Sync,
RemoteStorageLocalFileChange_tarrives after Steam has already replaced local files; Steam "will suspend your game process and upload any changed files" (doc). - DLC: "the DLC uses the app ID as the depot ID".
BIsDlcInstalled"Should only be used for simple client side checks"; ownership is checked on a server. Optional on-demand DLC: at most 10. - A demo is "a separate App ID that is associated with your full game's App ID", with its own depots; "Shared cloud APP ID" shares saves. Demos, DLC and tools are not eligible for the Deck review.
- A dedicated server is a separate Tool app whose depot includes
steam_appid.txtwith the base game's App ID; "Include this tool in Dedicated Server package" makes it downloadable anonymously. - Branches: "you can not set the build live on the default branch automatically";
SetLivenames a beta branch;Preview "1""only outputs logs and a file manifest". Names such asqaare conventions. - game-ci/steam-deploy numbers depots app ID + 1, + 2, ...; excludes
*.pdb,*.logand*.vdfby default (a Steam Input manifest the game reads is dropped); now runs agame-ci/clibinary whose version defaults tolatest(source). - Steam Input:
GetGlyphPNGForActionOriginreturns a local file path;SetInputActionManifestFilePathneeds an absolute path to a real file, so not a file inside the.pck(doc). - The overlay "may not work when running your game from Godot" (GodotSteam doc);
GameOverlayActivated_tis for the game to "pause or resume single player games" (doc).
Steam Deck (doc and tested)
- One review rates the game for Steam Deck, Steam Machine and other SteamOS devices: "Any game that is Verified on Steam
Deck is guaranteed to be Verified on Steam Machine as well". Display tests (resolution, text size) are Deck only. Steam Frame has a review of its own
(
steamhardware/steamframe/compat: Verified, Playable, Unsupported or Unknown, standalone Frame only), so the sentence above does not reach it. Do not mix up two sets of numbers:IsRunningOnSteamHardware()returns 0 (none), 1 (Deck), 2 (Machine) or 3 (Frame) (isteamutils.h, SDK 1.65), while the hardware default configuration uses 5, 6 and 7 below. - The text rule: "the smallest on-screen font character should never fall below 9 pixels in height at 1280x800", 12 recommended. Valve does not say whether that is the font size or the letters' drawn height.
- Recommendations: gyro and trackpads arrive as mouse input alongside the sticks, so switch prompts to keyboard and mouse only on a mouse click; "we recommend against restricting access to settings based on specific hardware"; cloud saves but not graphics settings; no launcher.
IsSteamRunningOnSteamDeckis "Deprecated as of Steamworks SDK 1.65" (doc) and gone from Steamworks.NET 1.65 and GodotSteam 4.21 and later.GetSteamHardwareDefaultConfig()returns Low 1, Medium 2, High 3, Max 4, SteamDeck 5, SteamMachine 6, SteamFrame 7, configurable per device on the partner site;IsRunningOnSteamHardware()is "intended to be used for usage analytics, support, diagnostic and other non-functional decisions" (doc).- Godot's virtual keyboard is for "Android, iOS, Web" only (doc). Steam's
ShowFloatingGamepadTextInput"sends OS keyboard keys directly to the game"; its rectangle is "in pixels relative the origin of the game window" (doc). - Steam's
AppResumingFromSuspend_tis "Sent after the device returns from sleep/suspend mode"; there is no callback before suspend (doc). The FAQ: "we recommend that you back up your save files before suspending".
Frame-rate data on the store page (Valve's announcement, November 2026)
From Valve's Steam news post "Steam Deck Framerate" (the text was pasted by the owner on 2026-11-10; the post's URL and date were not read here):
- A game tested for Deck compatibility (Verified, Playable or Unsupported) shows the average frame rate players get on a Deck on its store page (Steam Deck Compatibility > Learn more) and in the Library of the Steam Client Beta, with a histogram of the share of samples at each frame rate. Hitches show "as small columns in the lower framerate values".
- The data is anonymous and opt-in ("a majority of Steam Deck players have opted in"), and filtered to Decks at the default frame limit: 90 FPS on the OLED model, 60 FPS on the LCD model. In-game settings are not accounted for. A game needs a minimum number of data points before anything is shown. The desktop client's beta asks for the same opt-in, "a stepping stone toward providing framerate data for more devices".
- Why: the Verified rating "may not match every customer's bar for acceptable gameplay"; a Verified game "may not reach a framerate that some customers prefer". So 30 fps still passes the review, and the number a buyer reads is now the measured average.
- VSync and the gap between 90 and 45: "on a 90Hz display, if a game has a framerate of 75fps, VSync will force the game to present as many frames as it can at 90fps, and when it can't make that target it will drop to 45fps", a pattern that averages 75 ("90 90 45 90 90 45"). "Some games set an internal frame limit to 60, so on Steam Deck OLED set to default (90) it will bounce between 90 and 45 to hit 60 FPS."
What the kit does with it: deck reports deck.framePacing and warns when the project's frame cap (application/run/max_fps) is below a
Deck display's rate and does not divide it (60 on the OLED's 90 Hz; 45 on the LCD's 60 Hz). The rule (a cap at or above the rate, or a
divisor of it, paces evenly) is arithmetic from Valve's two examples, not a measurement on a Deck. Not done: a cap set in code
(Engine.max_fps) is not read, and nothing here measures a Deck's speed.
Text size at 1280x800 (tested)
With stretch mode canvas_items, text in window pixels is the theme font size times the stretch scale. A 1920x1080 base
at 1280x800 gives min(1280/1920, 800/1080) = 0.6667. Screenshots (Xvfb, Compatibility renderer, Godot's default font)
matched the font's glyph outlines within 1 px:
| Font size (1080p base) | Size at 1280x800 | Capitals ("H") | x-height |
|---|---|---|---|
| 12 | 8.00 px | 5 px | 4 px |
| 14 | 9.33 px | 6 px | 5 px |
| 16 (Godot's default) | 10.67 px | 8 px | 6 px |
| 18 | 12.00 px | 9 px | 7 px |
| 20 | 13.33 px | 9 px | 7 px |
| 24 | 16.00 px | 12 px | 9 px |
| 28 | 18.67 px | 14 px | 11 px |
- Read as the font size, 9 px needs 14 at a 1080p base and 12 px needs 18. Read as capitals, 9 px needs 18 and 12 px needs 24. At 1280x720 the numbers are the same (the width decides).
- The stretch formula (min of the two ratios for every aspect but
ignore, times the stretch scale, floored to at least 1 withscale_mode = "integer") matched Godot in 80 of 80 combinations of window size, aspect, scale mode and factor. - With integer scaling and a 1920x1080 base, the final transform at 1280x800 is 1.0: the game is drawn 1:1 and cut off.
- Headless Godot has no window (its size is 0x0), so the scale must be computed from project settings; glyph metrics work headless.
Suspend (tested)
A Godot 4.7.2 process stopped for 3 s (SIGSTOP, then SIGCONT, on X11) received no notification at all: no focus change,
no APPLICATION_PAUSED. Its next frame had a delta of 0.134 s against 3.013 s of wall clock, with 8 physics steps
(max_physics_steps_per_frame). Time.GetTicksMsec() advanced through the stop.
SDK release notes 1.59 to 1.65 (the SDK's own Readme.txt)
From the Readme.txt that ships in the Steamworks SDK (newest entry v1.65, 23 July 2026), paraphrased; the owner supplied
the file on 2026-10-29. It lists changes only, with no licence or redistribution terms. "Pinned package" means
Steamworks.NET.AnyCPU 2026.164.4, which doctor pins and which carries SDK 1.64, so a call added in 1.65 is not in it.
| SDK | What changed | What it means for a Godot game (suggestions are marked) |
|---|---|---|
| 1.65 (23 Jul 2026) | ISteamUtils::IsRunningUnderProton() added: tells the game it runs under Proton on Linux. |
For support and diagnostics, like IsRunningOnSteamHardware(); decide behaviour by feature. Not in the pinned package. |
| 1.65 | ISteamApps::SetGamePerformanceSetting() and SetGameRenderResolution() added: the game reports the relative performance setting and render resolution the player chose; Steam adds them to anonymous frame-rate data when the player opted into sharing it. |
Suggestion: call them when the player changes the quality preset or the render scale; skip them if the game has neither. Not in the pinned package. |
| 1.65 | ISteamMatchmakingServers::ServerFriends() (friends who recently played on a server) and the popularamongfriends server filter. |
Only for a game with a server browser, such as the multiplayer arena. |
| 1.65 | Steam Input action origin values for Switch 2, Steam Controller and Steam Frame Controller. | Do not hard-code origin tables: pass the origin to the glyph and string functions, as the 1.39 notes advise, so a device added later still gets a prompt. |
| 1.65 | Two new UGC query sort orders: by the number of items that depend on an item, and by the number of collections that contain it. | Workshop browsers only. |
| 1.64 (11 Mar 2026) | androidarm64 libraries link libc++ statically and use a 16 KB maximum page size (was 4 KB); linuxarm64 libraries use 4 KB (was 64 KB). |
The 16 KB size matches what Android exports need (mobile-web). |
| 1.64 | ISteamApps::GetBetaInfo() returns the last update time of a branch. UGC: MarkDownloadedItemAsUnused(), GetDownloadedItems(), GetNumDownloadedItems(). Remote Play: BSessionRemotePlayTogether(), GetSessionGuestID(), small, medium and large guest avatars and a callback when one has loaded. |
Workshop and Remote Play Together games. |
| 1.63 (18 Nov 2025) | First linuxarm64 and androidarm64 libraries. ISteamMusicRemote removed. Action origins for the Lenovo Legion Go and a generic controller. GetMarketEligibility can report a trade-cooldown reason. |
Remove any use of ISteamMusicRemote. |
| 1.62 (14 Mar 2025) | ISteamFriends::SetPersonaName() and GetUserRestrictions() removed. EMouseCursor renamed EHTMLMouseCursor. Remote Play Together: BStartRemotePlayTogether() removed (it is always available when a supported game launches), ShowRemotePlayTogetherUI() added, and functions to read a guest's keyboard and mouse directly instead of simulating local input. UGC: SetSubscriptionsLoadOrder(), SetItemsDisabledLocally(), and an optional flag on the subscribed-items calls to include locally disabled items. |
A game cannot rename the player. Mod load order and locally disabled mods are now Steam's to store (modding). |
| 1.61 (8 Nov 2024) | ISteamTimeline calls renamed and extended: instantaneous and range events, removing an event, opening the overlay at an event, game phases. RequestCurrentStats() removed (above). Glyphs for the Wireless HORIPAD for Steam. |
Suggestion: Steam Timeline is optional; mark moments a player might clip (a boss kill, a death) and the phases of a run. |
| 1.60 (19 Jun 2024) | ISteamTimeline added (the Game Recording feature). CSteamAPIContext removed: use the per-interface accessors. UGC: a 64-bit total file size, supported game versions per item, SetAdminQuery(). ISteamApps can manage the active beta branches. |
C++ bindings only for the accessors; C# bindings already use the static classes. |
| 1.59 (9 Feb 2024) | SteamAPI_InitFlat added as an initialization method for the flat API. ISteamUtils::DismissGamepadTextInput(). ISteamAppList removed. |
InitEx stays the call for Steamworks.NET and GodotSteam (section above). |
Kit checks
textsizerunner (ci/godot/lib/TextSize.ps1,ci/godot/tools/textsize.gd): runs Godot headless, computes the stretch scale from project settings, and measures every visible text control (Label,Button,LineEdit,RichTextLabeland others) after one frame, including the node's scale, with capitals and x-height from the font's "H" and "x". A size underminPx(9) fails; capitals under 9 or a size underwantPx(12) is atext-below-recommendedwarning. Integer scale mode with a base bigger than the window fails as cut off. Optionaltextsize.jsonsetsscenes,resolutions,minPxandwantPx(default: the main scene at 1280x800); when the file exists the runner is detected. One JUnit case per resolution, plusrunwhen Godot fails or finds no text.- Self-test
tests/Test-TextSize.ps1ontests/textsize-fixture(1920x1080 base,canvas_items,expand; needs Godot), measured on Godot 4.7.2:
| Case | Scale | Result |
|---|---|---|
Hint, size 12, at 1280x800 |
0.6667 | 8.00 px (capitals 5.92): fails |
Play, Godot's default 16, at 1280x800 |
0.6667 | 10.67 px (capitals 7.89): warning |
Title, size 40, at 1280x800 |
0.6667 | 26.67 px: ok |
Hint, size 12, at 1920x1080 |
1.0 | 12.00 px (capitals 8.88): warning |
| A hidden label, size 6 | Not measured | |
The same menu with scale_mode="integer" (integer/), at 1280x800 |
1.0 | Fails as cut off |
- Architecture rules (
ci/godot/lib/Architecture.ps1, warnings unlessarchitecture.jsonraises them):steam-removed-apiflagsIsSteamRunningOnSteamDeck,isSteamRunningOnSteamDeck,RequestCurrentStatsandrequestCurrentStatsin C# or GDScript;steam-env-dotnetflagsEnvironment.SetEnvironmentVariable("SteamAppId"or"SteamGameId"in C#. - Doctor (
ci/godot/lib/Doctor.ps1):Get-GdextensionProblemsreads the[libraries]and[dependencies]of every.gdextensionoutside.godot/and fails on ares://path missing for a platform the project exports to (GodotSteam 4.23.2's macOS names).Get-SteamPackageProblemsreads the game's.csproj: fails onSteamworks.NET.AnyCPU2026.165.0 to 2026.165.2 (fix: pin 2026.164.4 or add a resolver), warns on NuGetSteamworks.NET, and warns onFacepunch.Steamworkswhen a preset exports to anything but Windows. - Doctor's Steamworks files (
ci/godot/lib/Steam.ps1): a game keeps copies of what it uploads to Steamworks insteam/(with an empty.gdignore), and doctor reads them against the game's.csand.gdfiles (outsideaddons/and folders with a.gdignore) and the C# of its solution outside the Godot folder, less test projects (a folder namedtestortests, or ending.Tests), whose wrong names are on purpose. A name is a string literal or a string constant: C#const stringorstatic readonly string(several to a line), GDScriptconst, matched by the name's last part (BreakoutProgress.FirstFloorisFirstFloor) when only one value goes by it; a name built at run time is not followed. The C# example keeps itssteam/this way. Files are VDF (Valve's KeyValues text), read byConvertFrom-Vdfwithout escape sequences, as steamcmd reads"..\content\"; a.vdfthat does not parse warns.- Review prompts (only in a project that uses Steam): a line of a
.gd,.cs,.tscn,.tres,.csvor.pofile outsideaddons/,store/,steam/,docs/and the test folders that asks the player to leave a review or to rate the game ("Leave a positive review", "Rate this game", "Review us on Steam") is a WARN with its line, at most five. Valve's developer rules (store/reviews) forbid asking customers to review the product from within the application and rewarding a review. It reads text, so a comment-like string or a variable name can be a false alarm. - Rich presence: any
.vdfinsteam/whose root is"lang", one language per file ("Language" "english"with"Tokens") or all in one ("english" { "Tokens" { ... } }). Asteam_displayvalue the code sets (setRichPresence("steam_display", "#Token")in any binding) that does not start with#fails, and so does a token missing from the English tokens, since Steam then shows nothing; a token in the file without#, a{#Token}it refers to that is not defined, and a file with no English warn. Tokens in the code with no file insteam/: INFO. - Achievements and stats:
steam/achievements.json,{ "achievements": [...], "stats": [...] }with the API names as created in Steamworks (the C# example'sPlatformCatalogrule: letters, digits and underscores, each once; a name that breaks it warns). A name the code passes toSetAchievement,ClearAchievement,GetAchievement,IndicateAchievementProgress,UnlockAchievement, GodotSteam'ssetAchievementorCall("setAchievement", ...)or Facepunch'snew Achievement(...), or toSteamUserStats.SetStat/GetStat/AddStator GodotSteam'sSteam.setStatInt/setStatFloat/getStatInt/getStatFloat, that the file does not list (compared with case) fails: it would never reach Steam. A list is checked only for the keys it has; achievements in the code with no file: INFO. - steamcmd app build scripts: any
.vdfinsteam/whose root is"AppBuild", with paths relative to the script as steamcmd takes them. Fails:AppIDnot a number, noDepots, a depot ID that is not a number, a depot with noFileMappingholdingLocalPathandDepotPath(inline or in the depot build script it names), a depot build script that does not exist, and a key or value that looks like a secret (a password, token, login or a 32-character hex key; steamcmd takes the login on its command line). Warns:AppID480 (Spacewar), noContentRootor an absolute one (one machine only),SetLive "default"("you can not set the build live on the default branch automatically", and Valve adds that a released game asks for an authorization on the account's Steam Mobile Authenticator or by SMS), aSetLivebranch name with a space (a branch name has none; FAIL), and asteam_appid.txtinContentRootthat a depot does not exclude withFileExclusion. AContentRootthat does not exist yet (the export has not run) is INFO.
- Review prompts (only in a project that uses Steam): a line of a
- Self-test
tests/Test-Steam.ps1ontests/steam-fixture(laid out like GodotSteam 4.23.2, no Godot needed): a Linux export finds every library; adding macOS reports the two misnamed libraries and notlibsteam_api.dylib; the stale copy in.godot/is ignored; a Windows export finds its missing library; AnyCPU 2026.165.2 fails and 2026.164.4 passes; NuGetSteamworks.NETwarns; Facepunch passes for Windows only and warns with Linux;Test-UsesSteamis true for the fixture and false forexamples/gdscript. The VDF reader keeps repeated keys and backslashes and throws on broken braces and quotes.steam-fixture/steamholds a rich presence file,achievements.json, a working app build script with a depot build script, and a broken one;progress.gd(GodotSteam) misspells an achievement and a stat, sets a token the file lacks and a value without#, whilePlatform.cs(Steamworks.NET) is right. Each of the 16 problems is named and nothing else, the depot that excludessteam_appid.txtpasses, and doctor fails on a copy of the fixture. SteamAppId=480:Test-UsesSteam(ci/godot/lib/Detect.ps1) is true when the project hasaddons/godotsteamor a.csprojthat referencesSteamworks.NET*,Facepunch.Steamworks*orGodotSteam*.ci/godot/hyperdot.ps1then setsSteamAppId=480for every command, unlessSteamAppIdis already set, so a relaunch through Steam cannot end a smoke orexport -Runearly.- The C# example's
Platform/(examples/csharp/src/Example.Core/Platform/):IPlatformServices(achievements, an integer stat, a leaderboard, rich presence,StoreStats);PlatformCatalog(Steam API names, letters, digits and underscores, no duplicates);LocalPlatformkeeps state in one JSON file written atomically only byStoreStats, throws on unknown ids, keeps the higher score forKeepBest, enforces the rich presence limits and a#token forsteam_display, and sets a damaged file aside as.bad.BreakoutProgressunlocks on floors and combos and stores stats when a floor is cleared and when the run ends.PlatformTests.cscovers: an unlock survives a restart, nothing is kept withoutStoreStats, misspelt ids throw, KeepBest and ForceUpdate, the rich presence limits, a damaged file, and a full run. - The C# example's Steam backend (added 2026-10-29; tested without a client):
SteamPlatform(Platform/SteamPlatform.cs, no Steam dependency) implementsIPlatformServicesoverISteamApi, a seam of the calls it makes. It checks ids against the catalog, sendsStoreStatsonly when something changed since the last one, logs a failed Steam call once, and sharesLocalPlatform's rich presence limits.PlatformFactory.Createtries Steam when the build has it, and onSteamAPI.InitExfailing, a missing native library (DllNotFoundException,BadImageFormatException,EntryPointNotFoundException) or anUnavailableresult it logs one line and returnsLocalPlatform; aRestartAppIfNecessarytrue is passed on asQuitRequested. The realSteamworksApiis inexamples/csharp/src/Example.Steam/, a project outsideExample.slnthat referencesSteamworks.NET.AnyCPU2026.164.4; the game references it only withdotnet build -p:Steam=true(which also definesSTEAM), andgame/Platform/GamePlatform.cspicks the backend at start-up. A default build restores no Steam package and holds no Valve library (checked: nosteam_api*orSteamworks*file under.godot/mono/temp); with-p:Steam=truethe package's natives (runtimes/<rid>/native/) land next to the game. Cloud saves are not in the interface (Auto-Cloud needs no code).SteamPlatformTests.cs(fakeISteamApi): an unlock goes through once, 50SetStatcalls are oneStoreStats, no change is no call, a refused call is logged once, a score Steam already has counts as the best, every failed start falls back and logs once.tests/Example.Steam.Testsloads the real library with no client:libsteam_api.sois in the output,SteamAPI.InitExreturnsk_ESteamAPIInitResult_FailedGeneric("Failed to load module '/root/.steam/sdk64/steamclient.so'") and the factory falls back;PlatformStartTest(gdUnit4) runsGamePlatform.Start()in the engine in both builds (33 game tests green each way). The game'sMaindoes not callGamePlatform.Start()yet. Chosen over GodotSteam because the calls are typed one to one with the API and need no GDExtension,.gdextensionfile or Godot-side singleton; both are MIT (GodotSteam'slicense.mdwas read: "MIT License, Copyright (c) 2015-Current GP Garcia, Chris Ridenour, and Contributors"; the AnyCPU package's nuspec says MIT, rlabrecque's upstreamLICENSE.txtis MIT, "Copyright (c) 2013-2022 Riley Labrecque", and the fork's ownLICENSEfile could not be read, since its repository was not reachable here). Valve's libraries inside the package are covered by the Steamworks SDK access agreement, not by MIT; the repository holds none, and only the public test app 480 appears (GamePlatform.AppId, set asSteamAppIdthroughOS.SetEnvironmentin debug builds only). Your own App ID replaces 480;steam_appid.txtstays out of the shipped depot. SaveStore.SaveResolved: saves the copy the player chose as a descendant of both sides (merges the other side's history, takes the higher generation).AResolvedConflictSyncsOneWayOnBothDevicesplays a PC and a Deck offline into aConflict; keeping the PC's copy then givesUploadLocalon the PC andDownloadCloudon the Deck, keeping the Deck's givesInSyncon both.
Not confirmed
- The SDK release-notes table above is from the
Readme.txt, not from calls: none of those functions was run, and which of the 1.65 additions each binding exposes was not checked (the pinned package has SDK 1.64). - Everything that needs the Steam client: overlay, popups, real
StoreStats, leaderboards, Cloud dialogs, Dynamic Cloud Sync, glyphs, keyboards (including whereShowFloatingGamepadTextInputplaces itself), the relaunch and DLC. - steamcmd
app_info_print,app_update -betaand anonymous server downloads: steamcmd could not reach Steam through this sandbox's proxy ("Steamcmd needs to be online to update"), and they need an owning account. - GodotSteam 4.23.2 on macOS, and Steamworks.NET.AnyCPU 2026.165.x on macOS (same code path as Linux; not run); 2026.165.1 was not run on Linux either.
- That .NET's
Environment.SetEnvironmentVariableis invisible to Steam on macOS (tested on Linux only). - NoSteamClient's message (needs an installed but stopped client); Facepunch's callbacks and Godot's threads.
- The
user://to Auto-Cloud root mapping end to end, and Windows roots under Proton; that the game cannot learn the player's choice in Steam's conflict dialog (no event is documented). SteamDeck=1,OS.GetDistributionName()returning "SteamOS" on a Deck, focus events under gamescope.- Which clock Godot's ticks follow across a real system sleep (only SIGSTOP was simulated), so whether a clock jump
against
Time.GetTicksMsec()reveals a resume. - Whether Valve's 9 px means the font size or the letters' height;
textsizefails the first and warns on the second. - Whether one exported
.pckcan serve both the Windows and Linux depots (same files, bytes in a different order). - Valve's SteamCMD wiki (a proof-of-work wall) and GitHub's web and API pages (blocked here;
git cloneworked).