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
+22 -25
View File
@@ -53,8 +53,8 @@ on the partner dashboard — the dashboard's Downloads page only re-offers the S
| SDK zip (sign in to partner first) | `partner.steamgames.com/downloads/steamworks_sdk.zip` |
| Docs home | `partner.steamgames.com/doc/home` |
| SteamPipe upload (steamcmd + VDF fields) | `partner.steamgames.com/doc/sdk/uploading` |
| DLC configuration | `partner.steamgames.com/doc/store/application/dlc` |
| `BIsDlcInstalled` | `partner.steamgames.com/doc/api/ISteamApps` |
| Demo configuration | `partner.steamgames.com/doc/store/application/demos` |
| Paid app ownership (`BIsSubscribedApp`) | `partner.steamgames.com/doc/api/ISteamApps#BIsSubscribedApp` |
| Store artwork sizes | `partner.steamgames.com/doc/store/assets/standard` |
| Coming Soon / 2-week public window | `partner.steamgames.com/doc/store/coming_soon` |
| Wallet rule (in-app purchases) | `partner.steamgames.com/doc/features/microtransactions` |
@@ -64,17 +64,17 @@ on the partner dashboard — the dashboard's Downloads page only re-offers the S
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.
2. **App IDs.** One paid app and its associated free Demo. The Demo has its own App ID; it checks
ownership of the paid app with `BIsSubscribedApp(paidAppId)`.
3. **Builds.** Keep one codebase. Steam gives the Demo a separate release checklist/build setup;
shared depots can avoid duplicating common files once the paid app is released.
4. **Steamworks SDK.** `./scripts/steam-sdk.sh ~/Downloads/steamworks_sdk_165.zip` extracts
`steam_api64.dll` from `sdk/redistributable_bin/win64/` into `steam/` (gitignored) and stages
`steam_appid.txt` = 480 — Valve's Spacewar app, so `SteamAPI.Init()` works locally before
LlamaCasty has an App ID. Add `--contentbuilder` on the machine that uploads a depot. The csproj
copies the DLL **beside the exe** (`Link`) with `ExcludeFromSingleFile`, and copies
`steam_appid.txt` in Debug only — never into a depot. The managed side is the `Steamworks.NET`
package (`SteamApps.BIsDlcInstalled`).
package (`SteamApps.BIsSubscribedApp`).
5. **Store page** configured and made public **two weeks before release** — Valve's rule, and it is
the window where wishlists accumulate.
@@ -89,31 +89,28 @@ publish → steam-stage → steamcmd + app_build.vdf → build upload → set li
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. **Verify**: install the Demo from a clean Steam account, confirm paid-app ownership is false,
then test with an account that owns the paid app and confirm the higher tiers unlock.
**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` (`Services/Entitlement/`) has one method, `IsPro`. **Steam DLC** answers it today
(`SteamEntitlement`, `Steamworks.NET` + `steam_api64.dll`); the **Polar** monthly rail plugs in
beside it. Resolution is `Pro = steamDlc || polarLease`, then `MainViewModel.HasProOutput` adds
the Debug-only `DeveloperMode`; anything unresolved is the free profile.
- **`SteamEntitlement.NotConfigured()` is the shipping default** until the DLC app id exists — it
answers false without touching the native library. Set the id when Steamworks creates the DLC.
- `IEntitlement` reports `IsAppOwned`. Debug uses a fake owned result; Release checks the paid app
ID through `BIsSubscribedApp`. `MainViewModel.IsAppOwned` is the app-wide binary ownership flag.
- The paid app ID is not yet configured. Until it is assigned, the Release path fails closed without
touching the native library.
- **Fail closed on every path:** no id, no client, missing DLL, thrown load, not owned.
- **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.
- The EULA should expressly allow commercial/monetized streaming use; confirm with counsel or Valve
that this satisfies the Steam Subscriber Agreement's software terms.
- **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.
- Paid-app ownership false → free profile; true → higher output tiers 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.
@@ -123,15 +120,15 @@ version banner** — Steam is the update path, and that policy has not changed.
- **`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.
- **`PRIVACY.md`** must name any third party the app contacts — Google/YouTube. 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.**
- **What actually protects the product** is in `commercial.md` → "Anti-piracy posture": a useful
free tier, Steam's own delivery, and Steam ownership of the paid app. **Not obfuscation, not a
native DLL, not hardware locking.**
## 7. Effort, once
@@ -141,4 +138,4 @@ version banner** — Steam is the update path, and that policy has not changed.
| 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 |
| Clean-account Demo install + paid-app ownership test | 0.5 day |