Files
LlamaCasty/Distribution.md
T
gramps ff5e8d94ae feat(entitlement): pivot to paid app ownership
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
2026-11-02 09:18:03 -08:00

142 lines
8.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 |