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:
+22
-25
@@ -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` |
|
| SDK zip (sign in to partner first) | `partner.steamgames.com/downloads/steamworks_sdk.zip` |
|
||||||
| Docs home | `partner.steamgames.com/doc/home` |
|
| Docs home | `partner.steamgames.com/doc/home` |
|
||||||
| SteamPipe upload (steamcmd + VDF fields) | `partner.steamgames.com/doc/sdk/uploading` |
|
| SteamPipe upload (steamcmd + VDF fields) | `partner.steamgames.com/doc/sdk/uploading` |
|
||||||
| DLC configuration | `partner.steamgames.com/doc/store/application/dlc` |
|
| Demo configuration | `partner.steamgames.com/doc/store/application/demos` |
|
||||||
| `BIsDlcInstalled` | `partner.steamgames.com/doc/api/ISteamApps` |
|
| Paid app ownership (`BIsSubscribedApp`) | `partner.steamgames.com/doc/api/ISteamApps#BIsSubscribedApp` |
|
||||||
| Store artwork sizes | `partner.steamgames.com/doc/store/assets/standard` |
|
| Store artwork sizes | `partner.steamgames.com/doc/store/assets/standard` |
|
||||||
| Coming Soon / 2-week public window | `partner.steamgames.com/doc/store/coming_soon` |
|
| Coming Soon / 2-week public window | `partner.steamgames.com/doc/store/coming_soon` |
|
||||||
| Wallet rule (in-app purchases) | `partner.steamgames.com/doc/features/microtransactions` |
|
| 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,
|
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.
|
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:
|
2. **App IDs.** One paid app and its associated free Demo. The Demo has its own App ID; it checks
|
||||||
the DLC app ID is what the app passes to `BIsDlcInstalled`.
|
ownership of the paid app with `BIsSubscribedApp(paidAppId)`.
|
||||||
3. **Depot.** One depot holding the publish output; the base app and the DLC share the install
|
3. **Builds.** Keep one codebase. Steam gives the Demo a separate release checklist/build setup;
|
||||||
directory, and the DLC package grants no files of its own — it is entitlement only.
|
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
|
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_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
|
`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
|
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
|
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`
|
`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
|
5. **Store page** configured and made public **two weeks before release** — Valve's rule, and it is
|
||||||
the window where wishlists accumulate.
|
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
|
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.
|
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).
|
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
|
4. **Verify**: install the Demo from a clean Steam account, confirm paid-app ownership is false,
|
||||||
DLC (or use a test licence), confirm the high tiers unlock, then refund the test purchase.
|
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
|
**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.
|
version banner** — Steam is the update path, and that policy has not changed.
|
||||||
|
|
||||||
## 4. Entitlement wiring
|
## 4. Entitlement wiring
|
||||||
|
|
||||||
- `IEntitlement` (`Services/Entitlement/`) has one method, `IsPro`. **Steam DLC** answers it today
|
- `IEntitlement` reports `IsAppOwned`. Debug uses a fake owned result; Release checks the paid app
|
||||||
(`SteamEntitlement`, `Steamworks.NET` + `steam_api64.dll`); the **Polar** monthly rail plugs in
|
ID through `BIsSubscribedApp`. `MainViewModel.IsAppOwned` is the app-wide binary ownership flag.
|
||||||
beside it. Resolution is `Pro = steamDlc || polarLease`, then `MainViewModel.HasProOutput` adds
|
- The paid app ID is not yet configured. Until it is assigned, the Release path fails closed without
|
||||||
the Debug-only `DeveloperMode`; anything unresolved is the free profile.
|
touching the native library.
|
||||||
- **`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.
|
|
||||||
- **Fail closed on every path:** no id, no client, missing DLL, thrown load, not owned.
|
- **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 EULA should expressly allow commercial/monetized streaming use; confirm with counsel or Valve
|
||||||
the Steam Wallet; the app may *state* that monthly plans exist on the site, with no checkout,
|
that this satisfies the Steam Subscriber Agreement's software terms.
|
||||||
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.
|
- **Never gate a feature.** The only delta is the output profile.
|
||||||
|
|
||||||
## 5. Release checklist
|
## 5. Release checklist
|
||||||
|
|
||||||
- Build clean, 0 warnings (`./scripts/verify.sh`), tests green.
|
- Build clean, 0 warnings (`./scripts/verify.sh`), tests green.
|
||||||
- `steam_api64.dll` present; app starts **without** Steam and falls back to the free profile.
|
- `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).
|
- `LlamaCasty.exe` rename landed (never rename a live exe).
|
||||||
- Clean Steam install on a second machine, then an update pushed and observed.
|
- 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.
|
- 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
|
- **`LICENSE`** (EULA) forbids redistribution, resale, reverse-engineering and circumvention, and
|
||||||
disclaims warranty.
|
disclaims warranty.
|
||||||
- **`PRIVACY.md`** must name any third party the app contacts — Google/YouTube today, **Polar** the
|
- **`PRIVACY.md`** must name any third party the app contacts — Google/YouTube. Steam itself is
|
||||||
day licence validation ships. Steam itself is named by Valve's own policy, not ours.
|
named by Valve's own policy, not ours.
|
||||||
- **`THIRD-PARTY-NOTICES.txt`** stays shipped and shown in About; the ffmpeg pin stays
|
- **`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").
|
`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.
|
- **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.
|
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
|
- **What actually protects the product** is in `commercial.md` → "Anti-piracy posture": a useful
|
||||||
good enough that a crack buys nothing, Steam's own delivery, short Polar leases, and a licence ID
|
free tier, Steam's own delivery, and Steam ownership of the paid app. **Not obfuscation, not a
|
||||||
in About. **Not obfuscation, not a native DLL, not hardware locking.**
|
native DLL, not hardware locking.**
|
||||||
|
|
||||||
## 7. Effort, once
|
## 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 |
|
| Publish profile + `steam_api64.dll` + `IEntitlement` Steam path | 1 day |
|
||||||
| Depot + first `steamcmd` upload | 0.5 day, then ~15 min per release |
|
| Depot + first `steamcmd` upload | 0.5 day, then ~15 min per release |
|
||||||
| Store page + assets (screenshots, capsule art) | 1–2 days |
|
| 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 |
|
||||||
|
|||||||
+23
-31
@@ -1,40 +1,32 @@
|
|||||||
# HANDOFF — 2026-11-01 (exe renamed, depot builds, output rule enforced)
|
# HANDOFF — 2026-11-02 (Steam ownership check)
|
||||||
|
|
||||||
## State
|
## State
|
||||||
|
|
||||||
- Branch `main`, level with `origin` through `8f79b20`; this unit is local.
|
- `main`; local work unit, not pushed.
|
||||||
- **The shipped exe is `LlamaCasty.exe`** (`AssemblyName` renamed; four pack URIs fixed — the docs
|
- Distribution plan: one paid Steam app ($79 announced launch price, then $99) plus its associated
|
||||||
said three, `KeepBroadcastDialog.xaml:5` was the fourth). Guarded by `AssemblyIdentityTests`.
|
free Demo. Same code; no subscription, DLC purchase, or GitHub binary.
|
||||||
- **`scripts/publish.sh`** stages `artifacts/steam-stage` and refuses to finish if `ffmpeg\`, the
|
- `IEntitlement.IsAppOwned` feeds the app-wide `MainViewModel.IsAppOwned` flag. Debug uses a fake
|
||||||
licence, privacy or notices file is missing, or test assemblies leaked in. Output 257 MB.
|
owned result; the fake is excluded from Release.
|
||||||
- **The output-profile rule is built and enforced**: `Models/OutputProfile.cs` (921,600 px / 30 fps,
|
- Release ownership uses `BIsSubscribedApp(paidAppId)` and fails closed when the ID or Steam is
|
||||||
fail closed) resolved once in `BuildEncoderOptions` — one ffmpeg process, two outputs.
|
unavailable. **The paid app ID is not configured yet; the Steamworks application is pending.**
|
||||||
`MainViewModel.DeveloperMode` bypasses it in Debug only.
|
- Steam's private-use clause is not, by itself, a proven blocker. EULA wording should expressly
|
||||||
- **Distribution is Steam-exclusive** (2026-10-31): free app + paid edition as a Steam **DLC**; the
|
allow commercial/monetized streaming; OBS is the relevant Steam software precedent. See
|
||||||
**$10/month** plan sold off-Steam by Polar, *stated, never sold*, in-app. Entitlement is
|
`MyMistakes.md` → Steam distribution.
|
||||||
`steamDlcOwned || polarKeyValid`; the app must start without Steam, falling back to free.
|
|
||||||
- **Site live with Steam** (`a9d07ce`); `www.llamacasty.com` 301s to the apex.
|
|
||||||
|
|
||||||
## Next action
|
## Next action
|
||||||
|
|
||||||
**Surge, 2026-11-02 → 11-07 — six days, all remaining code.** Day 1 is partly done ahead of time:
|
Day 1: grey out higher tiers for non-owners and link to the paid Steam page; then the system-message
|
||||||
`IEntitlement` + `SteamEntitlement` (Steamworks.NET, fail-closed on every path) + `FakeEntitlement`,
|
strip. The queued Debug-only test-access rule also needs checking. Keep one integration test per
|
||||||
with `HasProOutput` reading the rail. Undo/redo, the rail gear and the EDIT alignment also landed
|
change.
|
||||||
early. Remaining on day 1: greyed tiers, licence UI's four states, system messages, licence ID in
|
|
||||||
About. Days 2–6 in `TASKS.md` → "The surge".
|
|
||||||
|
|
||||||
## Landmines
|
## Landmines
|
||||||
|
|
||||||
- **⛔ `ExcludeFromSingleFile="true"` on the ffmpeg content** — without it the publisher bundles
|
- The Demo has its own App ID. Ownership must check the paid app ID, never the Demo ID or test ID
|
||||||
libav\*.dll into the exe and the app ships unable to find ffmpeg. Recipe in `MyMistakes.md`.
|
`480`.
|
||||||
- **Dev affordances are compiled out, not hidden** — `DeveloperPanel` is removed from the Release
|
- No fake ownership code or developer panel in Release.
|
||||||
build; `Helpers/DevBuild.cs` is the single source for the flag (never `const`).
|
- No Steam checkout in the app; Steam handles purchase and updates.
|
||||||
- **Nothing may gate a feature.** The only paid delta is the output profile; `ShowBrandFlash` stays
|
- The paid app ID is not available yet; Release stays fail-closed until it is configured.
|
||||||
`false` and entitlement must never enable it.
|
- `verify.sh` clean app build passes; `EntitlementTests` passes 5/5. Full suite currently has 13–14
|
||||||
- **No checkout inside a Steam build** — the Wallet rule. The monthly is named, never sold, in-app.
|
UI/content failures; connection to this change is unconfirmed, investigate separately.
|
||||||
- **The org access token never ships.** `organization_id` is public; the token is not.
|
- Bundled FFmpeg must stay outside the single-file exe (`ExcludeFromSingleFile="true"`).
|
||||||
- **The site publishes the model, never the numbers**, and never takes payment or links a checkout.
|
- Build with Windows `dotnet.exe`; run tests per class (the whole suite can hang in WASAPI teardown).
|
||||||
- **No self-updater and no version banner**: Steam updates the app.
|
|
||||||
- Pre-existing: the **full test suite hangs** (WASAPI teardown) — per-class runs; `ytLive.Tests`
|
|
||||||
carries 3 xUnit warnings the app-only gate does not see.
|
|
||||||
- Not every hex is a theme colour — `BorderColorPalette`/chat defaults are USER DATA, not tokens.
|
|
||||||
|
|||||||
+21
-12
@@ -209,11 +209,11 @@ After: 127 MB exe, `ffmpeg\` present, depot ≈ 257 MB total.
|
|||||||
`scripts/publish.sh` fails loudly when `ffmpeg\`, `LICENSE`, `PRIVACY.md` or
|
`scripts/publish.sh` fails loudly when `ffmpeg\`, `LICENSE`, `PRIVACY.md` or
|
||||||
`THIRD-PARTY-NOTICES.txt` are missing, and when test assemblies leak in.
|
`THIRD-PARTY-NOTICES.txt` are missing, and when test assemblies leak in.
|
||||||
|
|
||||||
### STEAM IS THE DISTRIBUTOR — AND WHAT IT DOES NOT ALLOW (2026-10-31)
|
### STEAM DISTRIBUTION, COMMERCIAL USE, AND THE DEMO OWNERSHIP CHECK (2026-11-02)
|
||||||
|
|
||||||
**Why recorded:** the distribution answer changed three times in one day (winget → zip+GitHub →
|
**Why recorded:** the Steam Subscriber Agreement's private-use wording was misread as a blocker
|
||||||
Steam), and each pivot invalidated a doc written hours earlier. These are the facts that won't
|
for monetized streaming. Check the exception and the established Steam software precedent before
|
||||||
change.
|
raising it again.
|
||||||
|
|
||||||
- **Steam installs skip SmartScreen entirely** — Valve's client delivers the files, so there is no
|
- **Steam installs skip SmartScreen entirely** — Valve's client delivers the files, so there is no
|
||||||
browser download, no Mark-of-the-Web and no publisher warning. An unsigned exe is *fine* in this
|
browser download, no Mark-of-the-Web and no publisher warning. An unsigned exe is *fine* in this
|
||||||
@@ -221,16 +221,25 @@ change.
|
|||||||
- **Steam updates the app** — no self-updater, no version banner, no update endpoint.
|
- **Steam updates the app** — no self-updater, no version banner, no update endpoint.
|
||||||
- **⛔ In-app purchases must use the Steam Wallet.** Steamworks, *Microtransactions (In-Game
|
- **⛔ In-app purchases must use the Steam Wallet.** Steamworks, *Microtransactions (In-Game
|
||||||
Purchases)*: *"For any in-game purchases, you'll need to use the microtransaction API so Steam
|
Purchases)*: *"For any in-game purchases, you'll need to use the microtransaction API so Steam
|
||||||
customers can only make purchases from the Steam Wallet."* So a subscription sold through Polar
|
customers can only make purchases from the Steam Wallet."* LlamaCasty has no in-app checkout.
|
||||||
inside a Steam build is a policy violation. **Lifetime = plain DLC bought on the store page**
|
- **The private-use wording is not, by itself, a commercial-use ban for LlamaCasty.** The Steam
|
||||||
(the app only calls `BIsDlcInstalled`); the monthly is *stated*, never *sold*, in-app.
|
Subscriber Agreement §2.A permits commercial use when the applicable product terms expressly
|
||||||
- **Steam can technically do recurring billing** (`ISteamMicroTxn` + `ProcessAgreement`) but its own
|
allow it; §5 has general private/personal-use wording for third-party software. OBS Studio is
|
||||||
doc opens with *"Recurring Subscriptions is not a fully supported feature"* and requires a server
|
distributed on Steam as video-production software under GPL, and OBS confirms the Steam build is
|
||||||
of yours to poll renewals — more infrastructure than Polar, for a 30% cut.
|
the same as its website build. GPL permits commercial use. LlamaCasty's EULA should expressly
|
||||||
|
allow commercial and monetized streaming; confirm the wording with Valve or counsel. Do not call
|
||||||
|
this a Steam blocker based only on §5.
|
||||||
|
Sources: https://store.steampowered.com/subscriber_agreement/ ·
|
||||||
|
https://obsproject.com/kb/steam-faq · https://www.gnu.org/licenses/gpl-faq.html
|
||||||
|
- **Demo ownership check:** the free Demo uses its own App ID; `ISteamApps::BIsSubscribedApp`
|
||||||
|
checks ownership of the paid app. The paid App ID must be configured; zero/missing ID fails closed.
|
||||||
|
Valve reference: https://partner.steamgames.com/doc/api/ISteamApps#BIsSubscribedApp
|
||||||
|
- **Steam sells one paid app and distributes its associated free Demo.** Same code; no GitHub
|
||||||
|
binary. The demo links to the paid store page and can send a one-time wishlist notification.
|
||||||
- **Renaming a live exe is painful** (Steamworks → *Tip – Renaming a live exe*). Do the
|
- **Renaming a live exe is painful** (Steamworks → *Tip – Renaming a live exe*). Do the
|
||||||
`LlamaCasty.exe` rename **before** the first depot upload, not after.
|
`LlamaCasty.exe` rename **before** the first depot upload, not after.
|
||||||
- **The Steam store page must be public two weeks before release**, and the $100 Steam Direct fee
|
- **The paid app's Steam store page must be public two weeks before release.** The Steamworks
|
||||||
must be paid before the page can be configured at all.
|
application and $100 fee are already submitted; Valve's decision is pending.
|
||||||
|
|
||||||
### CONVERTING A MEDIAPIPE .tflite (WITH CUSTOM OPS) TO ONNX — AND PROVING IT (RECIPE, TASK 51)
|
### CONVERTING A MEDIAPIPE .tflite (WITH CUSTOM OPS) TO ONNX — AND PROVING IT (RECIPE, TASK 51)
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
namespace ytLive.Services.Entitlement;
|
namespace ytLive.Services.Entitlement;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Test double for the entitlement rail. Production never uses it — a fake that could be reached
|
/// Development/test ownership result. Release builds exclude this type.
|
||||||
/// in a shipped build is a licence bypass, so it lives here only because tests need to drive both
|
|
||||||
/// answers without a Steam client or a network.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class FakeEntitlement : IEntitlement
|
public sealed class FakeEntitlement : IEntitlement
|
||||||
{
|
{
|
||||||
public FakeEntitlement(bool isPro = false) => IsPro = isPro;
|
public FakeEntitlement(bool isAppOwned = false) => IsAppOwned = isAppOwned;
|
||||||
|
|
||||||
public bool IsPro { get; set; }
|
public bool IsAppOwned { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,12 @@
|
|||||||
namespace ytLive.Services.Entitlement;
|
namespace ytLive.Services.Entitlement;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Where "is this copy licensed for Pro output?" comes from.
|
/// Whether the current Steam account owns the paid app.
|
||||||
///
|
///
|
||||||
/// <para><b>One question, two sources.</b> The lifetime SKU is a Steam DLC; the monthly is a Polar
|
/// <para>Steam is the source of truth. Failure to reach Steam, load its native library, or
|
||||||
/// lease. Both answer this interface, and everything downstream — the output profile, the greyed
|
/// resolve the paid app ID returns <c>false</c>.</para>
|
||||||
/// tier list, the notices — reads only this. Nothing outside <c>Services/Entitlement</c> may know
|
|
||||||
/// which rail proved it.</para>
|
|
||||||
///
|
|
||||||
/// <para><b>Fail closed.</b> An implementation that cannot reach its source, cannot load its
|
|
||||||
/// native library, or has no product id configured returns <c>false</c>. Pro is never a default,
|
|
||||||
/// never a cached "probably", and never the result of an exception being swallowed as true.</para>
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IEntitlement
|
public interface IEntitlement
|
||||||
{
|
{
|
||||||
/// <summary>True when this session may emit above the free output budget.</summary>
|
bool IsAppOwned { get; }
|
||||||
bool IsPro { get; }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,41 +3,50 @@ using Steamworks;
|
|||||||
namespace ytLive.Services.Entitlement;
|
namespace ytLive.Services.Entitlement;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The lifetime SKU: a Steam **DLC** on the free app, read locally from the Steam client.
|
/// Reads ownership of the paid app from Steam. In the free Demo, this checks the associated paid
|
||||||
///
|
/// app ID; no key, server, or LlamaCasty account is involved. A missing ID, Steam client, or native
|
||||||
/// <para><b>This is the whole purchase story.</b> There is no key, no server and no lease — the
|
/// library fails closed so the app can continue with the free output profile.
|
||||||
/// customer buys the DLC on the store page and Steam already knows. The app asks once and caches
|
|
||||||
/// the answer; a refunded DLC stops reporting ownership the next time the app starts.</para>
|
|
||||||
///
|
|
||||||
/// <para><b>⛔ Never sell the monthly plan through this.</b> Steamworks requires in-app purchases
|
|
||||||
/// to use the Steam Wallet, so anything sold from inside the app would be a policy violation. The
|
|
||||||
/// monthly lives on the website and is billed by Polar (<see cref="PolarEntitlement"/> when it
|
|
||||||
/// lands); this class only ever reads what Valve already sold.</para>
|
|
||||||
///
|
|
||||||
/// <para><b>Steam absent is normal, not exceptional.</b> A copied folder, a shortcut run without
|
|
||||||
/// the client, or a CI machine has no Steam — <see cref="SteamAPI.Init"/> returns false and the
|
|
||||||
/// app runs as the free tier. It must never refuse to start over an entitlement lookup.</para>
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class SteamEntitlement : IEntitlement
|
public sealed class SteamEntitlement : IEntitlement
|
||||||
{
|
{
|
||||||
/// <summary>The DLC's own app id, not the base app's. Zero = not configured yet, which reads
|
private readonly uint _paidAppId;
|
||||||
/// as "no entitlement" rather than "try anyway".</summary>
|
private readonly Func<bool> _initializeSteam;
|
||||||
private readonly uint _dlcAppId;
|
private readonly Func<uint, bool> _isAppSubscribed;
|
||||||
|
private readonly Action _shutdownSteam;
|
||||||
|
|
||||||
private bool _initAttempted;
|
private bool _initAttempted;
|
||||||
private bool _available;
|
private bool _available;
|
||||||
private bool? _cached;
|
private bool? _cached;
|
||||||
|
|
||||||
public SteamEntitlement(uint dlcAppId) => _dlcAppId = dlcAppId;
|
public SteamEntitlement(uint paidAppId)
|
||||||
|
: this(
|
||||||
|
paidAppId,
|
||||||
|
SteamAPI.Init,
|
||||||
|
appId => SteamApps.BIsSubscribedApp(new AppId_t(appId)),
|
||||||
|
SteamAPI.Shutdown)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Configured with no product id — the state before the Steamworks app exists.</summary>
|
internal SteamEntitlement(
|
||||||
|
uint paidAppId,
|
||||||
|
Func<bool> initializeSteam,
|
||||||
|
Func<uint, bool> isAppSubscribed,
|
||||||
|
Action shutdownSteam)
|
||||||
|
{
|
||||||
|
_paidAppId = paidAppId;
|
||||||
|
_initializeSteam = initializeSteam;
|
||||||
|
_isAppSubscribed = isAppSubscribed;
|
||||||
|
_shutdownSteam = shutdownSteam;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Configured with no paid app ID; ownership fails closed without touching Steam.</summary>
|
||||||
public static SteamEntitlement NotConfigured() => new(0);
|
public static SteamEntitlement NotConfigured() => new(0);
|
||||||
|
|
||||||
public bool IsPro
|
public bool IsAppOwned
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (_dlcAppId == 0) return false;
|
if (_paidAppId == 0) return false;
|
||||||
if (_cached is { } known) return known;
|
if (_cached is { } known) return known;
|
||||||
|
|
||||||
if (!_initAttempted)
|
if (!_initAttempted)
|
||||||
@@ -45,12 +54,10 @@ public sealed class SteamEntitlement : IEntitlement
|
|||||||
_initAttempted = true;
|
_initAttempted = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// No steam_appid.txt and no client → false. That is the documented free path.
|
_available = _initializeSteam();
|
||||||
_available = SteamAPI.Init();
|
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
// A missing steam_api64.dll throws on first touch. Free tier, never a crash.
|
|
||||||
_available = false;
|
_available = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,7 +66,7 @@ public sealed class SteamEntitlement : IEntitlement
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var owned = SteamApps.BIsDlcInstalled(new AppId_t(_dlcAppId));
|
var owned = _isAppSubscribed(_paidAppId);
|
||||||
_cached = owned;
|
_cached = owned;
|
||||||
return owned;
|
return owned;
|
||||||
}
|
}
|
||||||
@@ -70,11 +77,9 @@ public sealed class SteamEntitlement : IEntitlement
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Matches Steam's own lifecycle callbacks; called from the frame pump's pump thread
|
|
||||||
/// would be wrong, so nothing here schedules it automatically.</summary>
|
|
||||||
public void Shutdown()
|
public void Shutdown()
|
||||||
{
|
{
|
||||||
if (_available) SteamAPI.Shutdown();
|
if (_available) _shutdownSteam();
|
||||||
_available = false;
|
_available = false;
|
||||||
_cached = null;
|
_cached = null;
|
||||||
_initAttempted = false;
|
_initAttempted = false;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ corrected. No new scope mid-week; anything discovered goes on a list for day 6.
|
|||||||
|
|
||||||
| Day | Units | Done when |
|
| Day | Units | Done when |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **1 · Mon** — licensing | `IEntitlement` seam + Steam DLC impl + fake · `HasProOutput` on the seam · greyed tier entries · licence UI four states + 7/1/0-day notices + "monthly is on the website, not Steam" · licence ID in About · system messages (`SystemMessage`/`MessageCenter` strip) · Debug-only `InternalsVisibleTo` | Fake drives all four states; Steam-less launch is the free profile; About shows the licence ID |
|
| **1 · Mon** — licensing | Paid-app ownership check (`BIsSubscribedApp`) + Debug fake + app-wide `IsAppOwned` · greyed tier entries · simple demo-to-paid status and Steam store link · system messages (`SystemMessage`/`MessageCenter` strip) · Debug-only `InternalsVisibleTo` | Fake covers owned/not-owned; Release fails closed without Steam or the paid app ID; ownership sets the app-wide flag |
|
||||||
| **2 · Tue** — release blockers | Visibility picker + `UpdateBroadcast` · `StreamConfig` defaults · TASK 53 output contract (geometry test per tier, 9:16 e2e) · 9·5 webcam identity key · Move transitions (per-layer carry-across-scenes) | A public broadcast starts and changes while live; every tier pinned by a test |
|
| **2 · Tue** — release blockers | Visibility picker + `UpdateBroadcast` · `StreamConfig` defaults · TASK 53 output contract (geometry test per tier, 9:16 e2e) · 9·5 webcam identity key · Move transitions (per-layer carry-across-scenes) | A public broadcast starts and changes while live; every tier pinned by a test |
|
||||||
| **3 · Wed** — resilience | TASK 32: blip retry → measured grace → amber RECONNECTING → one-click Back on air → fragmented MP4 → ingest pre-flight · 33 bandwidth step-down | Killing the network mid-broadcast recovers without ending it |
|
| **3 · Wed** — resilience | TASK 32: blip retry → measured grace → amber RECONNECTING → one-click Back on air → fragmented MP4 → ingest pre-flight · 33 bandwidth step-down | Killing the network mid-broadcast recovers without ending it |
|
||||||
| **4 · Thu** — content & media | 3·17 Text layer · 21 media acquisition wiring + file picker + loop/volume UI · 49 chat profanity filter · 34 scheduled streams | Text layer renders/persists; media file plays, loops, has volume; scheduled stream created |
|
| **4 · Thu** — content & media | 3·17 Text layer · 21 media acquisition wiring + file picker + loop/volume UI · 49 chat profanity filter · 34 scheduled streams | Text layer renders/persists; media file plays, loops, has volume; scheduled stream created |
|
||||||
@@ -57,13 +57,13 @@ persisted; renderer mirrors `Services/ChatBoxRenderer.cs`.
|
|||||||
| 49 | ☐ | Chat profanity filter: local, opt-in, **non-persistent**, user-supplied word list | [`task-49`](TASKS/task-49-chat-profanity-filter.md) |
|
| 49 | ☐ | Chat profanity filter: local, opt-in, **non-persistent**, user-supplied word list | [`task-49`](TASKS/task-49-chat-profanity-filter.md) |
|
||||||
| 50 | ☐ | One-button publish of the local recording (resumable upload) | [`task-50`](TASKS/task-50-publish-recording.md) |
|
| 50 | ☐ | One-button publish of the local recording (resumable upload) | [`task-50`](TASKS/task-50-publish-recording.md) |
|
||||||
| 53 | ☐ | Output-format contract: pin every tier with a geometry test, prove 9:16 end-to-end, recorded file matches output | [`task-53`](TASKS/task-53-output-formats.md) |
|
| 53 | ☐ | Output-format contract: pin every tier with a geometry test, prove 9:16 end-to-end, recorded file matches output | [`task-53`](TASKS/task-53-output-formats.md) |
|
||||||
| 54 | 🔶 | Freemium: EULA + privacy shipped; **no licence path built** — needs the entitlement seam, the output-profile cap and the licence UI below; site copy still says free forever | [`task-54`](TASKS/task-54-marcom-free-merch.md) |
|
| 54 | 🔶 | Freemium: EULA + privacy shipped; free Steam Demo and paid app use one codebase; ownership path and demo conversion UI remain in the rows below | [`task-54`](TASKS/task-54-marcom-free-merch.md) |
|
||||||
| — | 🔶 | **Steamworks entitlement** — `IEntitlement` + `SteamEntitlement` (Steamworks.NET) + fake **done**; needs the DLC app id from Steamworks. **Polar path retired** — one-time Steam DLC only | [`commercial`](docs/ai/commercial.md) |
|
| — | 🔶 | **Steam ownership** — `BIsSubscribedApp(paidAppId)` + Debug fake + app-wide `IsAppOwned`; paid app ID awaits Steamworks | [`commercial`](docs/ai/commercial.md) |
|
||||||
| — | ❌ | **Polar subscription path** — retired 2026-11-01 with the one-price model. Design kept in `decisions.md`, not built | [`decisions`](docs/ai/decisions.md) |
|
| — | ❌ | **Polar subscription path** — retired 2026-11-01 with the one-price model. Design kept in `decisions.md`, not built | [`decisions`](docs/ai/decisions.md) |
|
||||||
| — | 🔶 | **OutputProfile gate** — resolver + `EncoderOptions` wiring **done** (`Models/OutputProfile.cs`, one ffmpeg/two outputs, dev override); remaining: greyed tier entries in the dropdown, entitlement wiring | [`commercial`](docs/ai/commercial.md) |
|
| — | 🔶 | **OutputProfile gate** — resolver + `EncoderOptions` wiring **done** (`Models/OutputProfile.cs`, one ffmpeg/two outputs, dev override); remaining: greyed tier entries driven by `IsAppOwned` | [`commercial`](docs/ai/commercial.md) |
|
||||||
| — | ☐ | **Licence UI** — two states only: owned → Pro · not owned → greyed tiers + "unlock on Steam" (opens the store overlay). No key entry, no notices, no portal | [`commercial`](docs/ai/commercial.md) |
|
| — | ☐ | **Demo conversion UI** — owned users get higher tiers; demo users see greyed tiers and a Steam store link. No key entry, subscription notices, or portal | [`commercial`](docs/ai/commercial.md) |
|
||||||
| — | ☐ | **System messages** — `SystemMessage` + `MessageCenter`, strip above the dock, dedupe by key, short history, never composited | [`ui-surface`](docs/ai/ui-surface.md) |
|
| — | ☐ | **System messages** — `SystemMessage` + `MessageCenter`, strip above the dock, dedupe by key, short history, never composited | [`ui-surface`](docs/ai/ui-surface.md) |
|
||||||
| — | ☐ | **Steam packaging** — rename, `publish` output + `steam_api64.dll`, depot, `steamcmd` upload, store page + assets, DLC app | [`Distribution.md`](Distribution.md) |
|
| — | ☐ | **Steam packaging** — `publish` output + `steam_api64.dll`, paid app and associated Demo setup, depot, `steamcmd` upload, store pages + assets | [`Distribution.md`](Distribution.md) |
|
||||||
| — | ☐ | **Training-video series** — capture the first 8–10 recurring questions as videos on `@llamacasty`; each is documentation, channel content and monetization at once | MARCOM → support→content loop |
|
| — | ☐ | **Training-video series** — capture the first 8–10 recurring questions as videos on `@llamacasty`; each is documentation, channel content and monetization at once | MARCOM → support→content loop |
|
||||||
| 9·5 | ☐ | Webcam identity key reconciliation | [`task-09`](TASKS/task-09-live-stream-management.md) |
|
| 9·5 | ☐ | Webcam identity key reconciliation | [`task-09`](TASKS/task-09-live-stream-management.md) |
|
||||||
| 14 | 🔶 | Creator feedback batch — branch 3+ | [`task-14`](TASKS/task-14-creator-feedback-batch.md) |
|
| 14 | 🔶 | Creator feedback batch — branch 3+ | [`task-14`](TASKS/task-14-creator-feedback-batch.md) |
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# TASK 48 — Distribution & packaging: route decision, MSIX, signing
|
# TASK 48 — Distribution & packaging: route decision, MSIX, signing
|
||||||
|
|
||||||
> ⛔ **THE ROUTE IN THIS FILE IS SUPERSEDED — 2026-10-31.** Distribution is **Steam exclusive**: a
|
> ⛔ **THE ROUTE IN THIS FILE IS SUPERSEDED — 2026-10-31.** Distribution is **Steam exclusive**: one
|
||||||
> free app on Steam with the paid edition as a Steam DLC; the monthly plan is sold off-Steam. There
|
> paid app and its associated free Demo, using the same code. There is no subscription, GitHub
|
||||||
> is no direct download, no winget and no Store submission. Live policy:
|
> binary, direct download, winget or Store submission. Live policy:
|
||||||
> [`docs/ai/commercial.md`](../docs/ai/commercial.md); mechanics: [`Distribution.md`](../Distribution.md).
|
> [`docs/ai/commercial.md`](../docs/ai/commercial.md); mechanics: [`Distribution.md`](../Distribution.md).
|
||||||
> The MSIX build steps below still work and are simply unused.
|
> The MSIX build steps below still work and are simply unused.
|
||||||
|
|
||||||
|
|||||||
@@ -4,44 +4,45 @@ using ytLive.Services.Entitlement;
|
|||||||
namespace ytLive.ViewModels;
|
namespace ytLive.ViewModels;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The entitlement surface: whether this session may emit output above the free budget.
|
/// The app-wide Steam ownership flag and its output-profile consumer.
|
||||||
///
|
///
|
||||||
/// <para>Deliberately a separate partial from <c>License.cs</c>: that file owns *how* a
|
/// <para>Only the entitlement service decides whether the current Steam account owns the paid app.
|
||||||
/// licence is validated (Polar today), this one owns *what the answer means* for output.
|
/// Development builds use a fake owned result; Release builds use Steam.</para>
|
||||||
/// Steam's DLC check and Polar's lease both collapse into <see cref="IsPremium"/>; nothing
|
|
||||||
/// downstream of <see cref="HasProOutput"/> cares which one proved it.</para>
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class MainViewModel
|
public partial class MainViewModel
|
||||||
{
|
{
|
||||||
/// <summary>
|
private IEntitlement _entitlement = CreateEntitlement();
|
||||||
/// The single input to <see cref="Models.OutputProfile.Resolve"/>.
|
private bool _isAppOwned;
|
||||||
///
|
|
||||||
/// <para><b>Fail closed:</b> an unresolved or unreadable licence is simply false, so a
|
|
||||||
/// build with no entitlement emits the free profile. There is no "unknown" branch to get
|
|
||||||
/// wrong.</para>
|
|
||||||
///
|
|
||||||
/// <para><b>Developer mode never ships:</b> <see cref="DeveloperMode"/> is only ever
|
|
||||||
/// settable inside a development build (see <see cref="DevBuild"/>), so this expression
|
|
||||||
/// reduces to <c>IsPremium</c> in a Release binary.</para>
|
|
||||||
/// </summary>
|
|
||||||
public bool HasProOutput => IsPremium || DeveloperMode || _entitlement.IsPro;
|
|
||||||
|
|
||||||
/// <summary>
|
public bool IsAppOwned
|
||||||
/// The entitlement rail for this session. Lifetime = Steam DLC; until the DLC app id exists it
|
{
|
||||||
/// is <see cref="SteamEntitlement.NotConfigured"/>, which reads false (fail closed). Tests
|
get => _isAppOwned;
|
||||||
/// inject a fake through <see cref="EntitlementOverride"/>.
|
private set
|
||||||
/// </summary>
|
{
|
||||||
private IEntitlement _entitlement = SteamEntitlement.NotConfigured();
|
if (!SetProperty(ref _isAppOwned, value)) return;
|
||||||
|
OnPropertyChanged(nameof(HasProOutput));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Test seam: swap the rail without a Steam client. Internal — the test assembly is
|
public bool HasProOutput => DeveloperMode || IsAppOwned;
|
||||||
/// the only caller, and a public setter here would be a licence bypass surface.</summary>
|
|
||||||
|
private static IEntitlement CreateEntitlement()
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
return new FakeEntitlement(true);
|
||||||
|
#else
|
||||||
|
return SteamEntitlement.NotConfigured();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Test seam: swap the ownership source without a Steam client.</summary>
|
||||||
internal IEntitlement EntitlementOverride
|
internal IEntitlement EntitlementOverride
|
||||||
{
|
{
|
||||||
get => _entitlement;
|
get => _entitlement;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_entitlement = value;
|
_entitlement = value;
|
||||||
OnPropertyChanged(nameof(HasProOutput));
|
IsAppOwned = value.IsAppOwned;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -192,6 +192,7 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
public MainViewModel()
|
public MainViewModel()
|
||||||
{
|
{
|
||||||
AppLog.Write("MainViewModel ctor begin");
|
AppLog.Write("MainViewModel ctor begin");
|
||||||
|
IsAppOwned = _entitlement.IsAppOwned;
|
||||||
_youtubeAuth = new YouTubeAuthService(OAuthCredentials.ClientId, OAuthCredentials.ClientSecret,
|
_youtubeAuth = new YouTubeAuthService(OAuthCredentials.ClientId, OAuthCredentials.ClientSecret,
|
||||||
sessionChanged: ch => TokenStore.Save(ch));
|
sessionChanged: ch => TokenStore.Save(ch));
|
||||||
_youtubeStream = new YouTubeStreamService(_youtubeAuth);
|
_youtubeStream = new YouTubeStreamService(_youtubeAuth);
|
||||||
|
|||||||
+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
|
- **Free tier — every feature, every scene, every overlay**, free forever, no account, no
|
||||||
watermark, nothing nagged inside the output. It is not a trial.
|
watermark, nothing nagged inside the output. It is not a trial.
|
||||||
- **Pro is a one-time Steam purchase: a DLC on the free app.**
|
- **Pro is the paid Steam app; the free Demo is associated with it.** The paid app is **$79 during
|
||||||
**$79 introductory → $99** on a stated date before launch. No subscription, no lifetime SKU, no
|
the announced launch period, then $99** on the announced date. No subscription or third-party
|
||||||
third-party checkout.
|
checkout.
|
||||||
- **The only delta is output quality.** Features are never gated; a payment buys pixels.
|
- **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
|
- **No watermark, ever**, in either edition. The branding flash is retired as a paid delta and
|
||||||
stays off (`ShowBrandFlash`, default `false`).
|
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
|
- **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
|
checkout, and is labelled ("720p30 · free tier") — never silently missing, because silence
|
||||||
reads as broken.
|
reads as broken.
|
||||||
- **Activation is live**, not restart-gated: activating enables the high tiers and a lapse
|
- **Ownership is checked at app startup** and cached in `IsAppOwned` for that run. Purchases and
|
||||||
(cancel, revoke, sign-out, channel switch) greys them again on the next refresh.
|
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.
|
- **Bitrate follows the profile** — a capped user is not offered a 1080p bitrate.
|
||||||
|
|
||||||
## Entitlement — one source
|
## Entitlement — one source
|
||||||
|
|
||||||
**Pro is `steamDlcOwned`, and nothing else.** It resolves to the `OutputProfile`; it never touches a
|
**Paid-app ownership is the one entitlement.** Steam sells one paid app; its associated free demo is
|
||||||
feature. Fail closed: anything unresolved is the free tier.
|
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
|
- **Steam owns the purchase.** The demo calls `BIsSubscribedApp(paidAppId)` after `SteamAPI.Init`,
|
||||||
`IEntitlement` (`Services/Entitlement/`). It is a local check against Steam's own state — no
|
behind `IEntitlement` (`Services/Entitlement/`). It is a local check against Steam's own state —
|
||||||
server, no key, no lease, no account, no email.
|
no server, key, lease, or LlamaCasty account.
|
||||||
- **Fail closed, four ways:** no DLC app id configured, `SteamAPI.Init()` false (no client), a
|
- **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.
|
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.
|
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
|
- **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.
|
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
|
- **Development builds fake ownership as true.** `FakeEntitlement` is excluded from Release builds.
|
||||||
stops reporting ownership on the next start.
|
- **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
|
- **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
|
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.
|
checkout of ours, never an external payment page.
|
||||||
@@ -80,20 +83,20 @@ re-derived.
|
|||||||
|
|
||||||
## Channels — Steam only
|
## Channels — Steam only
|
||||||
|
|
||||||
**Steam is the distributor.** One free app on the store, the paid edition as a Steam **DLC**, and
|
**Steam is the distributor.** One paid app and its associated free Demo; no direct download,
|
||||||
no other vector: no direct download, no winget, no Scoop/Chocolatey, no Microsoft Store. The store
|
GitHub binary, winget, Scoop/Chocolatey, or Microsoft Store. The store page is the single
|
||||||
page is the single authoritative source for the download, the stats and the reviews.
|
authoritative source for the download, stats, and reviews.
|
||||||
|
|
||||||
- **Pro = Steam DLC, one-time.** Bought on the **store page**; the app only reads ownership
|
- **The paid app is one-time purchase.** The demo's App ID is separate, but it shares the code and
|
||||||
(`ISteamApps::BIsDlcInstalled`). No purchase flow, no Wallet code, nothing to reconcile.
|
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
|
- **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 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
|
- **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.
|
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
|
- **Boot shape:** the app ships `steam_api64.dll` beside the exe and calls `SteamAPI.Init`. Launched
|
||||||
is a functional dependency, not the rejected "native helper DLL for IP protection" — Steam
|
without Steam running, the app falls back to the **free profile** and says so; it never refuses to
|
||||||
ownership cannot be checked without it. Launched without Steam running, the app must fall back to
|
start.
|
||||||
the **free profile** and say so, never refuse to start.
|
|
||||||
- **Updates are Steam's.** The depot push is the update; the app keeps no updater and no version
|
- **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).
|
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
|
- **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,
|
- **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
|
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.
|
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
|
- **Nobody is locked out of streaming.** If Steam ownership is unavailable or not owned, the app
|
||||||
a banner and a portal link — never a blocked stream, never a modal.
|
remains usable at the free output profile; no modal or account portal.
|
||||||
|
|
||||||
## The introductory price — the only promotion
|
## 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
|
- **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.
|
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
|
Valve's DRM wrapper buys nothing a determined crack respects, and it can break an exe on a
|
||||||
machine that never runs Steam.
|
machine that never runs Steam.
|
||||||
- **The distribution is not a handout.** Steam deliveries go through Valve's client and account, so
|
- **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.
|
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
|
- **Steam is the ownership state** — a refunded purchase stops reporting ownership on the next
|
||||||
asked, and the app drops to the free profile on the next start. Nothing to revoke by hand.
|
check. The app holds no customer account or license key.
|
||||||
- **The Steam purchase is in About** — a leaked build traces back to a Steam account, which Valve
|
|
||||||
can action; we hold nothing to trace.
|
|
||||||
|
|
||||||
⛔ **Rejected, do not re-propose:** a native helper DLL for "IP protection", obfuscation, hardware
|
⛔ **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.
|
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
|
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 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.**
|
**$79 one-time during the announced launch period, then $99 on the announced date.** Sold only on
|
||||||
The website publishes the model and links the store page; it never takes payment.
|
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:**
|
**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
|
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.
|
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
|
**Steam is the only vector.** One paid app and its associated free Demo; same code, no GitHub
|
||||||
monthly plan is sold outside Steam and never purchasable in a Steam build. Live rules:
|
download, subscription, or second paid SKU. Live rules:
|
||||||
[`commercial.md`](commercial.md).
|
[`commercial.md`](commercial.md).
|
||||||
|
|
||||||
**Why the other vectors died, all on the same day:**
|
**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
|
- **The public `llamacasty-releases` repo** survives only as the **issue tracker**; it holds no
|
||||||
binaries, no manifest and no version file.
|
binaries, no manifest and no version file.
|
||||||
|
|
||||||
**The constraint that shapes the app:** Steamworks requires in-app purchases to use the **Steam
|
**The purchase happens on the paid app's Steam store page.** The Demo links there; Steam owns
|
||||||
Wallet**. Therefore the monthly is *stated*, never *sold*, inside the Steam build — no checkout, no
|
checkout, delivery, updates, and the paid-app ownership result. The app contains no checkout flow.
|
||||||
price, no payment fields. The lifetime is plain DLC bought on the store page, so it needs no
|
|
||||||
transaction code at all.
|
|
||||||
|
|
||||||
## Support — decided 2026-10-31
|
## 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 |
|
| [`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 |
|
| [`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 |
|
| [`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 |
|
| [`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 |
|
| [`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 |
|
| [`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:
|
mechanics: [`Distribution.md`](../../Distribution.md). Retired routes and cause of death:
|
||||||
[`decisions.md`](decisions.md).
|
[`decisions.md`](decisions.md).
|
||||||
|
|
||||||
**One vector: Steam.** A free app on Steam with the paid edition as a Steam **DLC**; the monthly
|
**One vector: Steam.** One paid app plus its associated free Demo. They use the same code; the Demo
|
||||||
plan is sold outside Steam and is never purchasable inside a Steam build (Steamworks requires the
|
checks ownership of the paid app to unlock higher output quality. No direct download, no GitHub
|
||||||
Steam Wallet for in-app purchases). No direct download, no winget, no Microsoft Store.
|
binary, no winget, no Microsoft Store, and no subscription.
|
||||||
|
|
||||||
**Consequences that are architecture, not packaging detail:**
|
**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.
|
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.
|
`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
|
- **⛔ `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.
|
running and fall back to the free profile.
|
||||||
- **The MSIX package stays buildable but is now unused** — `scripts/package-msix.sh` (TASK 48
|
- **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` +
|
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
|
### 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
|
Steam distributes the paid app and its associated free demo. They use the same code; the demo asks
|
||||||
monthly plan is billed by **Polar** outside Steam. Steamworks forbids taking payment inside the app
|
Steam whether the current account owns the paid app, and the answer controls access to higher output
|
||||||
with anything but the Steam Wallet, so the app states where the monthly lives and never sells it.
|
quality. The paid app is a one-time purchase: $79 during the announced launch period, then $99 on
|
||||||
The lesson worth keeping: the licensing choice rides on a **monetization** decision, not on a
|
the announced date. The app's EULA must expressly allow commercial and monetized streaming use.
|
||||||
distribution decision.
|
|
||||||
|
|
||||||
### ⛔ "Only when the user says so" is enforced by Windows, not by us
|
### ⛔ "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) —
|
(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
|
so the answer is a documented inquiry, and ours is "no third-party analytics or ad SDKs, no
|
||||||
persisted chat or reward payloads."
|
persisted chat or reward payloads."
|
||||||
|
|
||||||
|
|||||||
@@ -72,4 +72,4 @@ PY
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Next: the entitlement seam (IEntitlement + SteamApps.BIsDlcInstalled)."
|
echo "Next: configure the paid Steam App ID for IEntitlement (SteamApps.BIsSubscribedApp)."
|
||||||
|
|||||||
@@ -7,12 +7,10 @@ using ytLive.ViewModels;
|
|||||||
namespace ytLive.Tests;
|
namespace ytLive.Tests;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The entitlement seam (2026-11-01): one question — "may this session emit Pro output?" — with a
|
/// Steam ownership is cached once into the app-wide binary flag.
|
||||||
/// Steam DLC rail, a Polar rail to come, and a fail-closed default.
|
|
||||||
///
|
///
|
||||||
/// <para>The rule that must never break: **no configured rail means the free profile**. A missing
|
/// <para>The rule that must never break: a missing paid app ID, Steam client, or native library
|
||||||
/// DLC id, a missing Steam client or a thrown native load all resolve to false; none of them may
|
/// fails closed.</para>
|
||||||
/// crash the app or quietly grant Pro.</para>
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
// Shares the static MainViewModel.LayoutPathOverride with the sibling class; without a
|
// Shares the static MainViewModel.LayoutPathOverride with the sibling class; without a
|
||||||
// shared collection xUnit runs them in parallel and the two view models fight over one temp DB.
|
// shared collection xUnit runs them in parallel and the two view models fight over one temp DB.
|
||||||
@@ -20,27 +18,44 @@ namespace ytLive.Tests;
|
|||||||
public class EntitlementTests
|
public class EntitlementTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void No_configured_dlc_id_reads_as_no_entitlement()
|
public void No_configured_paid_app_id_reads_as_not_owned()
|
||||||
{
|
{
|
||||||
// The state before the Steamworks app exists: pure false, no native call attempted.
|
Assert.False(SteamEntitlement.NotConfigured().IsAppOwned);
|
||||||
Assert.False(SteamEntitlement.NotConfigured().IsPro);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void A_steam_entitlement_without_a_client_is_the_free_tier_not_a_crash()
|
public void Steam_ownership_checks_the_paid_app_id()
|
||||||
{
|
{
|
||||||
// No steam_appid.txt in the test host and no client running: Init fails, we report false.
|
const uint paidAppId = 1234567u;
|
||||||
var entitlement = new SteamEntitlement(1234567u);
|
uint queriedAppId = 0;
|
||||||
Assert.False(entitlement.IsPro);
|
var entitlement = new SteamEntitlement(
|
||||||
|
paidAppId,
|
||||||
|
initializeSteam: () => true,
|
||||||
|
isAppSubscribed: appId =>
|
||||||
|
{
|
||||||
|
queriedAppId = appId;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
shutdownSteam: () => { });
|
||||||
|
|
||||||
|
Assert.True(entitlement.IsAppOwned);
|
||||||
|
Assert.Equal(paidAppId, queriedAppId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void A_steam_entitlement_without_a_client_fails_closed()
|
||||||
|
{
|
||||||
|
var entitlement = new SteamEntitlement(1234567u, () => false, _ => true, () => { });
|
||||||
|
Assert.False(entitlement.IsAppOwned);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void The_fake_reports_what_a_test_sets()
|
public void The_fake_reports_what_a_test_sets()
|
||||||
{
|
{
|
||||||
var fake = new FakeEntitlement(true);
|
var fake = new FakeEntitlement(true);
|
||||||
Assert.True(fake.IsPro);
|
Assert.True(fake.IsAppOwned);
|
||||||
fake.IsPro = false;
|
fake.IsAppOwned = false;
|
||||||
Assert.False(fake.IsPro);
|
Assert.False(fake.IsAppOwned);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -52,14 +67,16 @@ public class EntitlementTests
|
|||||||
{
|
{
|
||||||
var vm = new MainViewModel();
|
var vm = new MainViewModel();
|
||||||
|
|
||||||
// Default rail is NotConfigured → free output.
|
Assert.True(vm.IsAppOwned);
|
||||||
Assert.False(vm.HasProOutput);
|
|
||||||
|
|
||||||
vm.EntitlementOverride = new FakeEntitlement(true);
|
|
||||||
Assert.True(vm.HasProOutput);
|
Assert.True(vm.HasProOutput);
|
||||||
|
|
||||||
vm.EntitlementOverride = new FakeEntitlement(false);
|
vm.EntitlementOverride = new FakeEntitlement(false);
|
||||||
|
Assert.False(vm.IsAppOwned);
|
||||||
Assert.False(vm.HasProOutput);
|
Assert.False(vm.HasProOutput);
|
||||||
|
|
||||||
|
vm.EntitlementOverride = new FakeEntitlement(true);
|
||||||
|
Assert.True(vm.IsAppOwned);
|
||||||
|
Assert.True(vm.HasProOutput);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -95,6 +95,7 @@
|
|||||||
reference to it and compiles cleanly. -->
|
reference to it and compiles cleanly. -->
|
||||||
<Page Remove="Controls\DeveloperPanel.xaml" />
|
<Page Remove="Controls\DeveloperPanel.xaml" />
|
||||||
<Compile Remove="Controls\DeveloperPanel.xaml.cs" />
|
<Compile Remove="Controls\DeveloperPanel.xaml.cs" />
|
||||||
|
<Compile Remove="Services\Entitlement\FakeEntitlement.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Steamworks runtime (scripts/steam-sdk.sh extracts it; steam/ is gitignored).
|
<!-- Steamworks runtime (scripts/steam-sdk.sh extracts it; steam/ is gitignored).
|
||||||
|
|||||||
Reference in New Issue
Block a user