Renames AssemblyName to LlamaCasty before the first Steam depot — Steamworks is explicit that renaming a live exe is painful, so this had to land first, not later. Four assembly-qualified pack URIs carried the old name, not three: MainWindow (window icon), OverlayHost (logo), RenameRecordingDialog and KeepBroadcastDialog. Pack URIs resolve by assembly name at runtime, so a stale one compiles, passes every test and then silently renders nothing the first time that view opens. AssemblyIdentityTests guards both halves — the assembly name, and no source XAML still naming the legacy assembly. Packaging/Package.appxmanifest and package-msix.sh carry the exe name too. scripts/publish.sh replaces the by-hand publish: single-file win-x64, ReadyToRun, ffmpeg beside the exe, and it refuses to finish if ffmpeg, LICENSE, PRIVACY.md or THIRD-PARTY-NOTICES.txt are missing, or if a test assembly leaked in. That last guard exists because the first publish was wrong in a way no test could see: PublishSingleFile + IncludeNativeLibrariesForSelfExtract bundled the libav*.dll family into the exe — a 9-file output with no ffmpeg folder and a 182 MB binary, so the locator would have found nothing at runtime. ExcludeFromSingleFile="true" on the content keeps the folder a folder. Diffing against a control publish without the single-file flags found it; guessing at item globs wasted two attempts first (recorded in MyMistakes.md). Measured: 127 MB exe + 129 MB ffmpeg = 257 MB depot. Tests: AssemblyIdentity, StorePackaging, BuildStamp, BundledFonts all pass; app builds with 0 warnings.
115 lines
6.7 KiB
Markdown
115 lines
6.7 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
|
||
|
||
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 **base app** (free) and one **DLC app** (the lifetime Pro). Note both numbers:
|
||
the DLC app ID is what the app passes to `BIsDlcInstalled`.
|
||
3. **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.
|
||
4. **Steamworks SDK.** Drop `steam_api64.dll` (+ `steam_appid.txt` for local runs) into the publish
|
||
output; the managed side is a thin wrapper (`Steamworks.NET` or hand-written interop) behind
|
||
`IEntitlement`.
|
||
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 from a clean Steam account, confirm `BIsDlcInstalled` returns 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` has two implementations: **Steam DLC** (lifetime, local) and **Polar** (monthly,
|
||
network + 7–14 day lease). Resolution is `Pro = steamDlc || polarValid`; anything unresolved is
|
||
the free profile.
|
||
- **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.dll` present; app starts **without** Steam and falls back to the free profile.
|
||
- DLC ownership false → free profile; true → every tier 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 today, **Polar** the
|
||
day licence validation ships. 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 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 |
|