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
This commit is contained in:
2026-11-02 09:18:03 -08:00
parent 54902cb838
commit ff5e8d94ae
17 changed files with 229 additions and 217 deletions
+30 -29
View File
@@ -10,9 +10,9 @@ storefront, support or promo work. Cause of death for every retired path:
- **Free tier — every feature, every scene, every overlay**, free forever, no account, no
watermark, nothing nagged inside the output. It is not a trial.
- **Pro is a one-time Steam purchase: a DLC on the free app.**
**$79 introductory → $99** on a stated date before launch. No subscription, no lifetime SKU, no
third-party checkout.
- **Pro is the paid Steam app; the free Demo is associated with it.** The paid app is **$79 during
the announced launch period, then $99** on the announced date. No subscription or third-party
checkout.
- **The only delta is output quality.** Features are never gated; a payment buys pixels.
- **No watermark, ever**, in either edition. The branding flash is retired as a paid delta and
stays off (`ShowBrandFlash`, default `false`).
@@ -49,25 +49,28 @@ The cap is a pixel budget plus a frame rate, not the word "720p":
- **The greyed menu entry is the sales surface.** It stays clickable, says what it costs, opens
checkout, and is labelled ("720p30 · free tier") — never silently missing, because silence
reads as broken.
- **Activation is live**, not restart-gated: activating enables the high tiers and a lapse
(cancel, revoke, sign-out, channel switch) greys them again on the next refresh.
- **Ownership is checked at app startup** and cached in `IsAppOwned` for that run. Purchases and
refunds are reflected on the next app start; there is no subscription lapse or sign-out path.
- **Bitrate follows the profile** — a capped user is not offered a 1080p bitrate.
## Entitlement — one source
**Pro is `steamDlcOwned`, and nothing else.** It resolves to the `OutputProfile`; it never touches a
feature. Fail closed: anything unresolved is the free tier.
**Paid-app ownership is the one entitlement.** Steam sells one paid app; its associated free demo is
the purchase funnel. Both use the same code. The demo checks whether the current Steam account owns
the paid app, then exposes the higher output tiers. The app-wide `IsAppOwned` value is the binary
ownership result used by later feature checks.
- **Steam owns the SKU.** `BIsDlcInstalled(dlcAppId)` after `SteamAPI.Init`, behind
`IEntitlement` (`Services/Entitlement/`). It is a local check against Steam's own state — no
server, no key, no lease, no account, no email.
- **Fail closed, four ways:** no DLC app id configured, `SteamAPI.Init()` false (no client), a
- **Steam owns the purchase.** The demo calls `BIsSubscribedApp(paidAppId)` after `SteamAPI.Init`,
behind `IEntitlement` (`Services/Entitlement/`). It is a local check against Steam's own state —
no server, key, lease, or LlamaCasty account.
- **Fail closed:** no paid app ID configured, `SteamAPI.Init()` false (no client), a
thrown native load (missing `steam_api64.dll`), and "not owned" all resolve to the free tier.
None of them may crash the app or grant Pro.
- **Steam down or not running** → free profile, with a line explaining why, never a refusal to
start. A Steam build launched outside Steam is the common case for people who pin shortcuts.
- **No licence key exists**, so nothing to paste, email, lose or reclaim. A refunded DLC simply
stops reporting ownership on the next start.
- **Development builds fake ownership as true.** `FakeEntitlement` is excluded from Release builds.
- **No licence key exists**, so nothing to paste, email, lose or reclaim. A refunded purchase stops
reporting ownership on the next Steam check.
- **The upgrade path is the Steam store overlay** (`ActivateGameOverlayToStore`) from the greyed
tier menu — Valve's own surface, so the purchase stays inside the Steam Wallet rules. Never a
checkout of ours, never an external payment page.
@@ -80,20 +83,20 @@ re-derived.
## Channels — Steam only
**Steam is the distributor.** One free app on the store, the paid edition as a Steam **DLC**, and
no other vector: no direct download, no winget, no Scoop/Chocolatey, no Microsoft Store. The store
page is the single authoritative source for the download, the stats and the reviews.
**Steam is the distributor.** One paid app and its associated free Demo; no direct download,
GitHub binary, winget, Scoop/Chocolatey, or Microsoft Store. The store page is the single
authoritative source for the download, stats, and reviews.
- **Pro = Steam DLC, one-time.** Bought on the **store page**; the app only reads ownership
(`ISteamApps::BIsDlcInstalled`). No purchase flow, no Wallet code, nothing to reconcile.
- **The paid app is one-time purchase.** The demo's App ID is separate, but it shares the code and
checks ownership of the paid app (`ISteamApps::BIsSubscribedApp`). No in-app checkout or license
server.
- **The website sends people to Steam.** It publishes the model and links the store page; it takes
no payment, collects no email, and hosts no binaries. One shop, one price, one receipt.
- **No in-app checkout of ours, ever.** Steamworks requires in-app purchases to use the Steam
Wallet, so the only purchase surface is Steam's own store overlay.
- **Boot shape:** the app ships `steam_api64.dll` beside the exe and calls `SteamAPI.Init`. That
is a functional dependency, not the rejected "native helper DLL for IP protection" — Steam
ownership cannot be checked without it. Launched without Steam running, the app must fall back to
the **free profile** and say so, never refuse to start.
- **Boot shape:** the app ships `steam_api64.dll` beside the exe and calls `SteamAPI.Init`. Launched
without Steam running, the app falls back to the **free profile** and says so; it never refuses to
start.
- **Updates are Steam's.** The depot push is the update; the app keeps no updater and no version
banner (the old "no self-updater" rule stays, for the same reason: it works differently).
- **Portable mode is no longer a distributable.** `portable.txt` remains as a feature for anyone who
@@ -135,8 +138,8 @@ repo for bugs that survive the friction of needing an account.
- **The issue tracker is a filter, deliberately.** Bugs go to the public releases repo on GitHub,
which requires an account; Steam Discussions stay pinned and unread; Announcements carry known
issues and release notes. Steam refunds are Valve's own (2 h / 14 days) and cost us nothing.
- **Nobody is locked out over a licence.** A billing hiccup drops the app to the free profile with
a banner and a portal link — never a blocked stream, never a modal.
- **Nobody is locked out of streaming.** If Steam ownership is unavailable or not owned, the app
remains usable at the free output profile; no modal or account portal.
## The introductory price — the only promotion
@@ -176,15 +179,13 @@ pointless and a leak that is traceable — not an unbreakable binary.
- **The free tier is the real defence.** If the only delta is pixels above 921,600, a cracked
build buys almost nothing and forfeits updates, support and the publisher's signature.
- **Steam DRM is optional and not needed.** The DLC check is already the entitlement; adding
- **Steam DRM is optional and not needed.** The app ownership check is the entitlement; adding
Valve's DRM wrapper buys nothing a determined crack respects, and it can break an exe on a
machine that never runs Steam.
- **The distribution is not a handout.** Steam deliveries go through Valve's client and account, so
there is no public zip for a mirror site to repackage, and no hash pin for us to maintain.
- **Steam is the licence state** — a refunded DLC stops reporting ownership the next time Steam is
asked, and the app drops to the free profile on the next start. Nothing to revoke by hand.
- **The Steam purchase is in About** — a leaked build traces back to a Steam account, which Valve
can action; we hold nothing to trace.
- **Steam is the ownership state** — a refunded purchase stops reporting ownership on the next
check. The app holds no customer account or license key.
⛔ **Rejected, do not re-propose:** a native helper DLL for "IP protection", obfuscation, hardware
locking, remote code that downloads and executes, and gating asset downloads as an auth check.
+9 -10
View File
@@ -5,10 +5,11 @@ you are about to re-open one of them. **Live commercial rules (prices, entitleme
signing, support, promo) are in [`commercial.md`](commercial.md)**; the other live rules live in
the subsystem files. This is the graveyard with cause of death.
## The business model — one price, settled 2026-11-01
## The business model — one paid app plus a free Demo, settled 2026-11-02
**$79 one-time, raising to $99 on a stated date. Sold only on Steam, as a DLC on the free app.**
The website publishes the model and links the store page; it never takes payment.
**$79 one-time during the announced launch period, then $99 on the announced date.** Sold only on
Steam as one paid app, with an associated free Demo using the same code. Demo ownership checks the
paid app's Steam App ID. The website links the store page; it never takes payment or hosts binaries.
**What died the same day:**
@@ -49,10 +50,10 @@ overlay.
remote is Gitea). The route is a separate **`llamacasty-releases`** Gitea repo push-mirrored to a
public GitHub repo: issues and assets public, code private, one publish direction.
## Distribution — Steam exclusive (2026-10-31, final)
## Distribution — Steam exclusive (2026-10-31; app structure updated 2026-11-02)
**Steam is the only vector.** A free app on Steam with the paid edition as a Steam **DLC**; the
monthly plan is sold outside Steam and never purchasable in a Steam build. Live rules:
**Steam is the only vector.** One paid app and its associated free Demo; same code, no GitHub
download, subscription, or second paid SKU. Live rules:
[`commercial.md`](commercial.md).
**Why the other vectors died, all on the same day:**
@@ -68,10 +69,8 @@ monthly plan is sold outside Steam and never purchasable in a Steam build. Live
- **The public `llamacasty-releases` repo** survives only as the **issue tracker**; it holds no
binaries, no manifest and no version file.
**The constraint that shapes the app:** Steamworks requires in-app purchases to use the **Steam
Wallet**. Therefore the monthly is *stated*, never *sold*, inside the Steam build — no checkout, no
price, no payment fields. The lifetime is plain DLC bought on the store page, so it needs no
transaction code at all.
**The purchase happens on the paid app's Steam store page.** The Demo links there; Steam owns
checkout, delivery, updates, and the paid-app ownership result. The app contains no checkout flow.
## Support — decided 2026-10-31
+1 -1
View File
@@ -14,7 +14,7 @@ Read the file whose row matches the task. There is no required reading order.
| [`output.md`](output.md) | The bundled `ffmpeg.exe` and the encoder subprocess (args, probe order, seams), local recording (REC/ON-AIR, filenames, private-broadcast close-out), and the WASAPI capture → filter → mix → named-pipe audio chain (TRAX, sync offset, ducking, limiter) | Touching ffmpeg args/bundling, encoder or recording behaviour, REC/ON-AIR, the audio mix and its meters, or loudness/sync problems |
| [`environment.md`](environment.md) | LAN hosts/IPs/SSH and the Gitea push recipe, the Windows `dotnet.exe` build + run commands, the test suite map, and the hermetic real-`MainWindow` test rule | Before building, running, adding a test, or reaching any box on the LAN |
| [`product.md`](product.md) | Brand/naming split, AI transparency, the licensing prohibitions, the freemium posture, auth/account assumptions, and packaging behaviour | Branding, naming, packaging, or a legal/third-party obligation |
| [`commercial.md`](commercial.md) | The live commercial policy: model and prices, the output-profile cap (stream **and** recording), entitlement (`steamDlcOwned \|\| polarKeyValid`; Steam DLC for lifetime, Polar for the monthly, no server), **Steam as the only channel**, signing (moot), the Founding 200 promo, costs, **support policy**, anti-piracy posture | Any question about money or support: pricing, licence keys, what free gets, how it ships, what a sale costs |
| [`commercial.md`](commercial.md) | The live commercial policy: model and prices, the output-profile cap (stream **and** recording), paid-app ownership (`BIsSubscribedApp`), **Steam as the only channel**, signing (moot), costs, **support policy**, anti-piracy posture | Any question about money or support: pricing, what free gets, how it ships, what a sale costs |
| [`youtube-api.md`](youtube-api.md) | Go-live ordering + the broadcast-binding fix, the private-only lock, health parsing, end-stream pre-flight, Test mode, and chat-insert shape | Touching go-live, broadcast lifecycle, health polling, streaming close-out, or the chat service |
| [`overlays.md`](overlays.md) | The social bar, the native alert box (six animations, alert clip, ticker) and the chat box conventions — everything composited over the frame | Adding or changing anything drawn on top of the output |
| [`decisions.md`](decisions.md) | Retired paths with cause of death: the paid-tier history and pricing, the free-forever era, Store IAP, the Store ordering, rejected monetization and anti-piracy ideas, and the permanent out-of-product list | You are about to re-open a decision that was already settled |
+8 -10
View File
@@ -199,9 +199,9 @@ Live channel policy, the artifact and costs: [`commercial.md`](commercial.md). B
mechanics: [`Distribution.md`](../../Distribution.md). Retired routes and cause of death:
[`decisions.md`](decisions.md).
**One vector: Steam.** A free app on Steam with the paid edition as a Steam **DLC**; the monthly
plan is sold outside Steam and is never purchasable inside a Steam build (Steamworks requires the
Steam Wallet for in-app purchases). No direct download, no winget, no Microsoft Store.
**One vector: Steam.** One paid app plus its associated free Demo. They use the same code; the Demo
checks ownership of the paid app to unlock higher output quality. No direct download, no GitHub
binary, no winget, no Microsoft Store, and no subscription.
**Consequences that are architecture, not packaging detail:**
@@ -209,7 +209,7 @@ Steam Wallet for in-app purchases). No direct download, no winget, no Microsoft
app keeps nothing that downloads or executes, for the same Store-policy reason it always had.
`ytLive.Tests/StorePackagingTests.cs` must keep asserting that no self-updater returns.
- **⛔ `steam_api64.dll` is a functional dependency**, not the rejected "native helper DLL for IP
protection" — Steam DLC ownership cannot be read without it. The app must start without Steam
protection" — Steam app ownership cannot be read without it. The app must start without Steam
running and fall back to the free profile.
- **The MSIX package stays buildable but is now unused** — `scripts/package-msix.sh` (TASK 48
slice 2) still produces `artifacts/LlamaCasty.msix` from `Packaging/Package.appxmanifest` +
@@ -265,11 +265,10 @@ a non-Steam download exists:
### Distribution and licensing are independent — the channel never decides billing
The Steam build sells the lifetime as **Steam DLC** and reads it with `BIsDlcInstalled`, while the
monthly plan is billed by **Polar** outside Steam. Steamworks forbids taking payment inside the app
with anything but the Steam Wallet, so the app states where the monthly lives and never sells it.
The lesson worth keeping: the licensing choice rides on a **monetization** decision, not on a
distribution decision.
Steam distributes the paid app and its associated free demo. They use the same code; the demo asks
Steam whether the current account owns the paid app, and the answer controls access to higher output
quality. The paid app is a one-time purchase: $79 during the announced launch period, then $99 on
the announced date. The app's EULA must expressly allow commercial and monetized streaming use.
### ⛔ "Only when the user says so" is enforced by Windows, not by us
@@ -311,4 +310,3 @@ nothing. COPPA is very unlikely to bind (not child-directed), but note the enfor
(FTC Sept 2025 *Apitor*: operators are responsible for what third-party components collect) —
so the answer is a documented inquiry, and ours is "no third-party analytics or ad SDKs, no
persisted chat or reward payloads."