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:
+30
-29
@@ -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
@@ -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
@@ -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
@@ -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."
|
||||
|
||||
|
||||
Reference in New Issue
Block a user