Check Steam ownership of the paid app with BIsSubscribedApp; keep Debug fake ownership out of Release. Ref: https://partner.steamgames.com/doc/api/ISteamApps#BIsSubscribedApp
142 lines
8.5 KiB
Markdown
142 lines
8.5 KiB
Markdown
# 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`](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 in `commercial.md` and `docs/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.
|
||
|
||
```bash
|
||
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
|
||
```
|
||
|
||
- **`PublishTrimmed` stays 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, see
|
||
[`output.md`](output.md) → "FFmpeg locator").
|
||
- **`steam_api64.dll` ships 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.
|
||
- **`AssemblyName` is `LlamaCasty`** (renamed 2026-11-01, before the first depot because
|
||
Steamworks warns about renaming a live exe) → the artifact is `LlamaCasty.exe` plus its
|
||
`ffmpeg\` folder. `scripts/publish.sh` stages both and refuses to finish if either is missing.
|
||
- **⛔ `ExcludeFromSingleFile="true"` on the ffmpeg content.** Without it, `PublishSingleFile` +
|
||
`IncludeNativeLibrariesForSelfExtract` swallow the libav\*.dll family **into the exe** — a
|
||
182 MB binary with no `ffmpeg\` 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` |
|
||
| Demo configuration | `partner.steamgames.com/doc/store/application/demos` |
|
||
| Paid app ownership (`BIsSubscribedApp`) | `partner.steamgames.com/doc/api/ISteamApps#BIsSubscribedApp` |
|
||
| 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` |
|
||
|
||
1. **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.
|
||
2. **App IDs.** One paid app and its associated free Demo. The Demo has its own App ID; it checks
|
||
ownership of the paid app with `BIsSubscribedApp(paidAppId)`.
|
||
3. **Builds.** Keep one codebase. Steam gives the Demo a separate release checklist/build setup;
|
||
shared depots can avoid duplicating common files once the paid app is released.
|
||
4. **Steamworks SDK.** `./scripts/steam-sdk.sh ~/Downloads/steamworks_sdk_165.zip` extracts
|
||
`steam_api64.dll` from `sdk/redistributable_bin/win64/` into `steam/` (gitignored) and stages
|
||
`steam_appid.txt` = 480 — Valve's Spacewar app, so `SteamAPI.Init()` works locally before
|
||
LlamaCasty has an App ID. Add `--contentbuilder` on the machine that uploads a depot. The csproj
|
||
copies the DLL **beside the exe** (`Link`) with `ExcludeFromSingleFile`, and copies
|
||
`steam_appid.txt` in Debug only — never into a depot. The managed side is the `Steamworks.NET`
|
||
package (`SteamApps.BIsSubscribedApp`).
|
||
5. **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
|
||
```
|
||
|
||
1. **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.
|
||
2. **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.
|
||
3. **Announce** with Steam Events (release notes appear in the library and on the store page).
|
||
4. **Verify**: install the Demo from a clean Steam account, confirm paid-app ownership is false,
|
||
then test with an account that owns the paid app and confirm the higher tiers unlock.
|
||
|
||
**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` reports `IsAppOwned`. Debug uses a fake owned result; Release checks the paid app
|
||
ID through `BIsSubscribedApp`. `MainViewModel.IsAppOwned` is the app-wide binary ownership flag.
|
||
- The paid app ID is not yet configured. Until it is assigned, the Release path fails closed without
|
||
touching the native library.
|
||
- **Fail closed on every path:** no id, no client, missing DLL, thrown load, not owned.
|
||
- The EULA should expressly allow commercial/monetized streaming use; confirm with counsel or Valve
|
||
that this satisfies the Steam Subscriber Agreement's software terms.
|
||
- **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.dll` present; app starts **without** Steam and falls back to the free profile.
|
||
- Paid-app ownership false → free profile; true → higher output tiers offered.
|
||
- `LlamaCasty.exe` rename 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.txt` present → data lands in `.\data\`.
|
||
|
||
## 6. Legal and takedown
|
||
|
||
- **`LICENSE`** (EULA) forbids redistribution, resale, reverse-engineering and circumvention, and
|
||
disclaims warranty.
|
||
- **`PRIVACY.md`** must name any third party the app contacts — Google/YouTube. Steam itself is
|
||
named by Valve's own policy, not ours.
|
||
- **`THIRD-PARTY-NOTICES.txt`** stays shipped and shown in About; the ffmpeg pin stays
|
||
`lgpl-shared`; no GPL component may be linked (see `product.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 useful
|
||
free tier, Steam's own delivery, and Steam ownership of the paid app. **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 Demo install + paid-app ownership test | 0.5 day |
|