Files
LlamaCasty/Distribution.md
T
gramps abfab25685 build: the exe is LlamaCasty.exe, and the publish script proves the artifact
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.
2026-11-01 07:01:50 -08:00

6.7 KiB
Raw Blame History

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 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.

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 → "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\.
  • 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