docs(distribution): Steam is the only channel — the zip, winget and the Store are dead
The site said "Coming soon to Steam" while the app docs still said direct download plus winget primary with Steam parked. One of them had to move, and the creator's ruling is Steam exclusive: a free app on Steam with the paid edition as a Steam DLC, and the $10/month plan sold off-Steam by Polar. Records the constraint that shapes the code, not just the prose: - Steamworks requires in-app purchases to use the Steam Wallet (Microtransactions doc, quoted in commercial.md), so the monthly is *stated*, never *sold*, inside a Steam build; the lifetime is plain DLC bought on the store page and read with BIsDlcInstalled. - Entitlement is `steamDlcOwned || polarKeyValid`, both resolving to one OutputProfile. steam_api64.dll ships beside the exe and is a functional dependency, not the rejected "IP protection" native DLL — without it DLC ownership cannot be read. Launched without Steam, the app falls back to the free profile rather than refusing to start. - Signing is moot: Steam installs never meet SmartScreen. Artifact Signing stays on the shelf for the day a non-Steam download exists. Year-one fixed cost is ~$115. - The exe rename must land before the first depot upload — Steamworks warns about renaming a live exe. Distribution.md is rewritten to SteamPipe mechanics (publish output, depot, steamcmd, DLC wiring, clean-account purchase/refund test). product.md, decisions.md (a new "Steam exclusive" entry with every dead vector), TASKS.md (Steamworks entitlement row, Steam packaging row), ai.md, index.md and the TASK 48 banner follow. MyMistakes.md gains the Steam rules that cost real time today: installs skip SmartScreen, updates are free, the Wallet rule, and the recurring-billing caveat.
This commit is contained in:
+71
-114
@@ -1,153 +1,110 @@
|
||||
# Distribution & release mechanics
|
||||
|
||||
How the build becomes a file someone installs. **Policy — prices, entitlement, which channels
|
||||
exist and why — is [`docs/ai/commercial.md`](docs/ai/commercial.md).** This file is the how.
|
||||
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.
|
||||
|
||||
> The previous version of this document was a 736-line Polar/MSIX/obfuscation plan. It was
|
||||
> superseded on 2026-10-31; the reasoning that still matters lives in `commercial.md` and
|
||||
> `docs/ai/decisions.md`, and the full text is in `git log -- Distribution.md`.
|
||||
> 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
|
||||
|
||||
One self-contained single-file exe plus the bundled ffmpeg folder, zipped. No .NET install on the
|
||||
target machine, no installer framework, no admin rights.
|
||||
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
|
||||
-p:PublishReadyToRun=true -o artifacts/steam-stage
|
||||
```
|
||||
|
||||
- **`PublishTrimmed` stays OFF.** Trimming breaks WPF's XAML reflection (`Binding`, `DataTemplate`,
|
||||
`StaticResource`). The exe is therefore fat — expect ~110–150 MB compressed with the runtime
|
||||
inside, and that is the accepted cost of "copy it and run it".
|
||||
- **ffmpeg stays outside the exe** — `ffmpeg\` beside it (already the app's locator contract, see
|
||||
[`output.md`](output.md) → "FFmpeg locator"). Embedding it passes 200 MB and adds an extraction
|
||||
step for no benefit.
|
||||
- **WebView2 is the one machine dependency**: the Evergreen runtime ships with Windows 11 and most
|
||||
Windows 10; a genuinely clean machine needs the bootstrapper once. Say so in the download note.
|
||||
- **`AssemblyName` is still `ytLive`** → the shipped exe is `ytLive.exe` today. The rename to
|
||||
`LlamaCasty.exe` is queued (three pack URIs break — see `product.md`), and until it lands every
|
||||
release note names the real file.
|
||||
`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 still `ytLive`** → the exe is `ytLive.exe` today. The rename to
|
||||
`LlamaCasty.exe` is queued (three pack URIs break — see `product.md`), and **must land before the
|
||||
first depot upload**, because Steam's docs are explicit that renaming a live exe is painful.
|
||||
- **Portable mode** (`portable.txt` → `.\data\`) survives as a feature for a copied install; it is
|
||||
not a delivery vector.
|
||||
|
||||
**Portable mode.** A `portable.txt` marker beside the exe keeps the layout DB, `Assets` and
|
||||
settings in `.\data\` instead of `%APPDATA%\ytLlive` (hung off `LayoutPathOverride`). Same binary,
|
||||
USB-stick behaviour, nothing to uninstall.
|
||||
## 2. Steamworks setup, once
|
||||
|
||||
## 2. Where releases live — the public handout repo
|
||||
|
||||
**The source repo stays private.** Releases live in a **second Gitea repo,
|
||||
`llamacasty-releases`**, which is push-mirrored to a **public GitHub repo** (`llamacasty/releases`).
|
||||
That public repo holds nothing but a README, an issue template, a known-issues page and the release
|
||||
assets — so the only thing that can ever leak is content authored for publication.
|
||||
|
||||
Why it is shaped that way: GitHub has no "public issues on a private repo" switch, and issue
|
||||
visibility follows repo visibility. An empty public repo gives a public issue tracker, a durable
|
||||
binary URL for winget, and `releases/latest` for the update notice, without the code going anywhere.
|
||||
|
||||
**The mirror carries refs, not release assets.** Git mirrors branches and tags; GitHub Release
|
||||
uploads live outside the object store, so the zip is uploaded separately.
|
||||
|
||||
**Setup, once:**
|
||||
|
||||
1. Gitea → new repo `llamacasty-releases` (private) with `README.md`,
|
||||
`.github/ISSUE_TEMPLATE/bug.yml`, `known-issues.md`.
|
||||
2. Gitea → that repo → **Settings → Mirror Settings → push mirror** →
|
||||
`https://github.com/llamacasty/releases.git` + a GitHub PAT. One-way and force-updating, so the
|
||||
public copy can never drift from what was authored.
|
||||
3. Create the free GitHub **organisation** `llamacasty` so URLs aren't tied to a personal account.
|
||||
4. Point the Steam store page's support URL at
|
||||
`https://github.com/llamacasty/releases/issues/new/choose` — the template chooser, never the
|
||||
open-issue list.
|
||||
|
||||
**Rejected:** a submodule (publishes a pointer, not files), `git subtree split` (three static files
|
||||
don't justify the history surgery, and one stray private file in the prefix leaks forever), and
|
||||
serving zips from the droplet (uses the VPS as a CDN and makes the update notice hand-rolled).
|
||||
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 → zip → SHA-256 → gh release create → winget manifest PR → update notice
|
||||
publish → steam-stage → steamcmd + app_build.vdf → build upload → set live → announcement
|
||||
```
|
||||
|
||||
1. **Tag and publish.** `v<major>.<minor>.<patch>` tag; upload
|
||||
`LlamaCasty-<version>-win-x64.zip` with
|
||||
`gh release create v1.0.0 LlamaCasty-1.0.0-win-x64.zip --repo llamacasty/releases
|
||||
--notes-file RELEASE.md`. Binaries live there, never on the droplet — free CDN, durable URLs,
|
||||
release notes in the same place, and `wingetcreate` reads `releases/latest` natively.
|
||||
2. **Record the hash.** `sha256sum` the zip; it goes in the winget manifest and in the release
|
||||
notes. This is the anti-tamper wall: the official channel delivers exactly this artifact or
|
||||
fails.
|
||||
3. **winget.** `wingetcreate update LlamaCasty.LlamaCasty --version <v> --urls <release-url>
|
||||
--submit` — it opens the PR against `microsoft/winget-pkgs`. Manifest shape:
|
||||
`InstallerType: portable`, `InstallerSha256`, `NestedInstallerType` where the zip nests the exe.
|
||||
First submission is `wingetcreate new`; the identifier is `LlamaCasty.LlamaCasty`.
|
||||
4. **Update notice.** Commit the current version into the releases repo (so it arrives in the same
|
||||
push or a follow-up commit) or read `releases/latest` from the GitHub API. The app shows one
|
||||
line — *"v1.4 is out — `winget upgrade LlamaCasty.LlamaCasty`"*. **Nothing downloads or
|
||||
executes**: that is Store policy 10.2.2 and a permanent no (see `product.md`).
|
||||
5. **Optional mirrors.** Scoop (a small JSON manifest hosted in a bucket) and Chocolatey (a nupkg
|
||||
with a checksum) — ten minutes each, same audience.
|
||||
6. **MSIX** stays buildable for the parked Store channel: `scripts/package-msix.sh` →
|
||||
`artifacts/LlamaCasty.msix`, submitted nowhere until the (now free) developer account exists.
|
||||
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.
|
||||
|
||||
## 4. Signing — deferred until revenue
|
||||
**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.
|
||||
|
||||
**Unsigned is the launch state.** Artifact Signing is bought at **two subscribers**, not before:
|
||||
it never removes the day-one warning (EV's SmartScreen bypass died in March 2024), so the only
|
||||
thing it changes early is the publisher line. Costs and posture:
|
||||
[`docs/ai/commercial.md`](docs/ai/commercial.md).
|
||||
## 4. Entitlement wiring
|
||||
|
||||
- **Azure Artifact Signing** (renamed from "Trusted Signing") — **$9.99/month Basic**, 5,000
|
||||
signatures, **requires a paid Azure subscription**. Individual identity validation is the slow
|
||||
part (days to weeks) and must be renewed; start it when the purchase is decided.
|
||||
- **⛔ No EV certificate, and no certificate with an HSM.** Microsoft removed the EV instant-bypass
|
||||
in March 2024, so EV ($400+/yr) buys exactly what OV (~$150–300/yr) buys.
|
||||
- **The free alternative is the Microsoft Store**: the individual developer fee was waived, and
|
||||
Microsoft signs the MSIX, so a Store install never meets SmartScreen at all.
|
||||
- **Unsigned costs a warning on every release**, because SmartScreen ties reputation to the file
|
||||
hash. Signing accrues reputation to the **publisher** — the only exit from that loop.
|
||||
- **Signing never clears the warning on day one either.** Plan for weeks.
|
||||
- Secrets: the Trusted Signing profile lives in CI secrets; **never commit** a `.pfx` or
|
||||
`metadata.json` with credentials. `MSIX_PFX`/`MSIX_PFX_PASSWORD` are for local test installs only.
|
||||
- Verify every release: `signtool verify /pa /v <file>`, and confirm the hash in the manifest
|
||||
matches the uploaded zip.
|
||||
- `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`), full test suite green.
|
||||
- `signtool verify /pa` passes on the shipped exe.
|
||||
- Launch on a clean Windows profile: first-run works, recording writes where it claims, ffmpeg is
|
||||
found beside the exe, WebView2-loaded UI renders.
|
||||
- Fresh install through `winget install LlamaCasty.LlamaCasty` on a second machine, then
|
||||
`winget upgrade` over the previous version.
|
||||
- ZIP hash published; manifest PR merged; version notice updated.
|
||||
- Portable mode: `portable.txt` present → data lands in `.\data\`, deleting the folder deletes the
|
||||
app's footprint.
|
||||
- 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. It is what makes a mirror site a licence breach rather than a grey area.
|
||||
- **`PRIVACY.md`** must name any third party the app contacts — today that is Google/YouTube,
|
||||
ffmpeg is local, and **Polar** is added the day licence validation ships.
|
||||
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`, and no GPL component may be linked (see `product.md` → "Licensing").
|
||||
- **DMCA**: statutory damages need a registration ($65), and a takedown needs only the notice —
|
||||
host, link, good-faith statement, signature. Worth doing once there is something to pirate.
|
||||
- **What actually protects the product** is in `commercial.md` → "Anti-piracy posture": signed
|
||||
hash-pinned delivery, short leases, a free tier good enough that a crack buys nothing, and a
|
||||
licence ID in About. **Not obfuscation, not a native DLL, not hardware locking.**
|
||||
`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 |
|
||||
|---|---|
|
||||
| Publish profile + portable mode | 0.5 day |
|
||||
| Releases repo + Gitea→GitHub push mirror | 0.5 day |
|
||||
| `gh release` + hash + version notice | 0.5 day |
|
||||
| First winget manifest (`wingetcreate new`) | 0.5 day, then ~15 min per release |
|
||||
| Clean-machine + `winget` smoke test | 0.5 day |
|
||||
| Artifact Signing profile + CI step (deferred) | 0.5 day, at two subscribers |
|
||||
| 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 |
|
||||
|
||||
Reference in New Issue
Block a user