The first half of surge day 1. One question, "may this session emit Pro output?", with one method on the interface so nothing downstream knows which rail proved it. SteamEntitlement reads the lifetime SKU the way Valve intends: SteamAPI.Init() then SteamApps.BIsDlcInstalled(dlcAppId). Four different failure paths all resolve to the free tier rather than to Pro or to a crash — no DLC id configured, no Steam client, steam_api64.dll missing (a throw on first touch), and simply not owned. A copied folder run without Steam is the common case, not an exceptional one. NotConfigured() is the shipping default until Steamworks creates the DLC app; it answers false without ever touching the native library, so today's builds behave exactly as they did before this commit. HasProOutput is now IsPremium || DeveloperMode || rail, with an internal EntitlementOverride test seam (internal, because a public setter here is a licence bypass). Four tests: no id reads false, no client reads false without throwing, the fake reports what a test sets, and the view model takes its answer from the rail. Steamworks.NET 2024.8.0 added; the native DLL still comes from scripts/steam-sdk.sh, so the csproj change is three lines and no formatting churn.
8.9 KiB
Distribution & release mechanics
How the build becomes an install on someone's PC. Policy — prices, entitlement, why Steam is the
only channel, support — is docs/ai/commercial.md. This file is the how.
Superseded twice on 2026-10-31. The 736-line Polar/MSIX/obfuscation plan is in
git log -- Distribution.md; the zip-plus-winget plan written earlier the same day is in the same place. The reasoning that survives lives incommercial.mdanddocs/ai/decisions.md.
1. The artifact
A self-contained win-x64 publish plus the bundled ffmpeg\ folder, uploaded to a Steam depot. No
installer, no admin rights, no zip for anyone to mirror.
dotnet publish ytLive.csproj -c Release -r win-x64 --self-contained true \
-p:PublishSingleFile=true \
-p:IncludeNativeLibrariesForSelfExtract=true \
-p:EnableCompressionInSingleFile=true \
-p:PublishReadyToRun=true -o artifacts/steam-stage
PublishTrimmedstays OFF. Trimming breaks WPF's XAML reflection (Binding,DataTemplate,StaticResource). Expect ~110–150 MB in the depot; that is the cost of "no .NET install".- ffmpeg stays outside the exe —
ffmpeg\beside it (the app's locator contract, seeoutput.md→ "FFmpeg locator"). steam_api64.dllships beside the exe and is loaded at startup; without it the app cannot read DLC ownership. Its absence must degrade to the free profile, never to a crash.- WebView2 is the one machine dependency. Valve's Common Redistributables configure the Evergreen bootstrapper as a prerequisite, so the depot declares it instead of us shipping it.
AssemblyNameisLlamaCasty(renamed 2026-11-01, before the first depot because Steamworks warns about renaming a live exe) → the artifact isLlamaCasty.exeplus itsffmpeg\folder.scripts/publish.shstages both and refuses to finish if either is missing.- ⛔
ExcludeFromSingleFile="true"on the ffmpeg content. Without it,PublishSingleFile+IncludeNativeLibrariesForSelfExtractswallow the libav*.dll family into the exe — a 182 MB binary with noffmpeg\folder beside it, so the locator finds nothing and every stream fails at runtime. The publish output is ~257 MB: 127 MB exe + 129 MB ffmpeg. - Portable mode (
portable.txt→.\data\) survives as a feature for a copied install; it is not a delivery vector.
2. Steamworks setup, once
The SDK zip is the only download. It is ~24 MB and contains the runtime
(sdk/redistributable_bin/win64/steam_api64.dll) and the release tooling
(sdk/tools/ContentBuilder/ → steamcmd.exe for Windows, steamcmd.sh for Linux/macOS, plus the
app_build_*.vdf / depot_build_*.vdf templates). There is no separate "ContentBuilder download"
on the partner dashboard — the dashboard's Downloads page only re-offers the SDK.
Reference links, so nobody hunts again:
| What | Where |
|---|---|
| SDK zip (sign in to partner first) | partner.steamgames.com/downloads/steamworks_sdk.zip |
| Docs home | partner.steamgames.com/doc/home |
| SteamPipe upload (steamcmd + VDF fields) | partner.steamgames.com/doc/sdk/uploading |
| DLC configuration | partner.steamgames.com/doc/store/application/dlc |
BIsDlcInstalled |
partner.steamgames.com/doc/api/ISteamApps |
| Store artwork sizes | partner.steamgames.com/doc/store/assets/standard |
| Coming Soon / 2-week public window | partner.steamgames.com/doc/store/coming_soon |
| Wallet rule (in-app purchases) | partner.steamgames.com/doc/features/microtransactions |
| Recurring subscriptions | partner.steamgames.com/doc/store/pricing/subscriptions |
| Partner support | help.steampowered.com/en/wizard/HelpWithPublishing |
| Steamworks.NET (managed wrapper) | nuget.org/packages/Steamworks.NET |
- Steamworks account → onboarding (NDA, identity, company details, tax interview, payout bank, then the $100 Steam Direct fee per app). The store page cannot be configured before the fee.
- App IDs. One base app (free) and one DLC app (the lifetime Pro). Note both numbers:
the DLC app ID is what the app passes to
BIsDlcInstalled. - Depot. One depot holding the publish output; the base app and the DLC share the install directory, and the DLC package grants no files of its own — it is entitlement only.
- Steamworks SDK.
./scripts/steam-sdk.sh ~/Downloads/steamworks_sdk_165.zipextractssteam_api64.dllfromsdk/redistributable_bin/win64/intosteam/(gitignored) and stagessteam_appid.txt= 480 — Valve's Spacewar app, soSteamAPI.Init()works locally before LlamaCasty has an App ID. Add--contentbuilderon the machine that uploads a depot. The csproj copies the DLL beside the exe (Link) withExcludeFromSingleFile, and copiessteam_appid.txtin Debug only — never into a depot. The managed side is theSteamworks.NETpackage (SteamApps.BIsDlcInstalled). - Store page configured and made public two weeks before release — Valve's rule, and it is the window where wishlists accumulate.
3. Release
publish → steam-stage → steamcmd + app_build.vdf → build upload → set live → announcement
- Upload the depot with
steamcmd +login <user> +run_app_build ..\scripts\app_build_<appid>.vdf +quit. The VDF names the app, depot, content root and branch. - Set the build live in Steamworks → Builds → the new build → Set Build Live on the default branch. That is the release; there is no artifact to hash and no manifest to submit.
- Announce with Steam Events (release notes appear in the library and on the store page).
- Verify: install from a clean Steam account, confirm
BIsDlcInstalledreturns false, buy the DLC (or use a test licence), confirm the high tiers unlock, then refund the test purchase.
Updates are the same upload with a higher build ID. The app keeps no self-updater and no version banner — Steam is the update path, and that policy has not changed.
4. Entitlement wiring
IEntitlement(Services/Entitlement/) has one method,IsPro. Steam DLC answers it today (SteamEntitlement,Steamworks.NET+steam_api64.dll); the Polar monthly rail plugs in beside it. Resolution isPro = steamDlc || polarLease, thenMainViewModel.HasProOutputadds the Debug-onlyDeveloperMode; anything unresolved is the free profile.SteamEntitlement.NotConfigured()is the shipping default until the DLC app id exists — it answers false without touching the native library. Set the id when Steamworks creates the DLC.- Fail closed on every path: no id, no client, missing DLL, thrown load, not owned.
- Never sell the monthly inside the Steam build. Steamworks requires in-app purchases to use the Steam Wallet; the app may state that monthly plans exist on the site, with no checkout, no price and no payment field. Confirm the wording with Steamworks support before shipping it.
- Never gate a feature. The only delta is the output profile.
5. Release checklist
- Build clean, 0 warnings (
./scripts/verify.sh), tests green. steam_api64.dllpresent; app starts without Steam and falls back to the free profile.- DLC ownership false → free profile; true → every tier offered.
LlamaCasty.exerename landed (never rename a live exe).- Clean Steam install on a second machine, then an update pushed and observed.
- Store page public for the full two weeks; support contact and announcements configured.
- Portable mode:
portable.txtpresent → data lands in.\data\.
6. Legal and takedown
LICENSE(EULA) forbids redistribution, resale, reverse-engineering and circumvention, and disclaims warranty.PRIVACY.mdmust name any third party the app contacts — Google/YouTube today, Polar the day licence validation ships. Steam itself is named by Valve's own policy, not ours.THIRD-PARTY-NOTICES.txtstays shipped and shown in About; the ffmpeg pin stayslgpl-shared; no GPL component may be linked (seeproduct.md→ "Licensing").- DMCA: a takedown needs only the notice — host, link, good-faith statement, signature. Statutory damages need a registration ($65). Worth doing once there is something to pirate.
- What actually protects the product is in
commercial.md→ "Anti-piracy posture": a free tier good enough that a crack buys nothing, Steam's own delivery, short Polar leases, and a licence ID in About. Not obfuscation, not a native DLL, not hardware locking.
7. Effort, once
| Step | Effort |
|---|---|
| Steamworks onboarding + $100 fee | 0.5–1 day, mostly waiting on verification |
Publish profile + steam_api64.dll + IEntitlement Steam path |
1 day |
Depot + first steamcmd upload |
0.5 day, then ~15 min per release |
| Store page + assets (screenshots, capsule art) | 1–2 days |
| Clean-account install + DLC purchase/refund test | 0.5 day |