The frame-rate boundary is correct as a product rule, but it only bites in a Release build, where DevBuild.IsDevelopment is false and DeveloperMode cannot unlock anything — exactly the build whose performance numbers are worth reading (see docs/ai/environment.md: Debug pixel paths are several times slower). 60 fps could not be tested at all. OutputProfile.EntitlementEnforced is false: every shipped tier is selectable by any build, and HasProOutput — the one property the tier setter, the dropdown's strikethrough and the locked-item style all read — answers yes regardless of ownership. static readonly rather than const, so the consumed `||` does not fold into unreachable operand code (same reason as DevBuild.IsDevelopment). The licence indicator row is suppressed while the switch is off: it exists to explain what the boundary costs, and claiming "Steam ID Limit: 60fps" to an unowned build would be a false statement. Docs corrected in place — the enforced rule is unchanged, only its state is recorded. Tests: one integration case covers both switch states (unowned build reaches 1080p60 with the switch off, 1080p30 with it on), so flipping at launch leaves no red test behind. The two suites that anchor on the pinned licence row early-return while it is absent; EntitlementTests covers that state meanwhile. 667 pass, build 0 warnings.
70 lines
3.5 KiB
C#
70 lines
3.5 KiB
C#
namespace ytLive.Models;
|
|
|
|
/// <summary>
|
|
/// The one entitlement rule, as pure policy: what a build is allowed to emit.
|
|
///
|
|
/// <para><b>Frame rate is the paid boundary.</b> Every supported resolution at 30 fps is free;
|
|
/// 60 fps requires paid ownership, regardless of frame dimensions.</para>
|
|
///
|
|
/// <para><b>Fail closed.</b> Callers pass <c>pro: false</c> when entitlement is unknown,
|
|
/// unreadable or unresolved; the free profile is the default state, never the fallback
|
|
/// after an error.</para>
|
|
///
|
|
/// <para><b>This is not a feature gate.</b> Every feature, scene, overlay and the recording
|
|
/// itself stay available; only the emitted frame gets smaller. Never call it for anything
|
|
/// else — a cracked build buys almost nothing, which is the entire anti-piracy posture.</para>
|
|
/// </summary>
|
|
public static class OutputProfile
|
|
{
|
|
/// <summary>Free ceiling: 30 frames per second.</summary>
|
|
public const int FreeMaxFps = 30;
|
|
|
|
/// <summary>
|
|
/// Pre-release switch. <c>false</c> means the frame-rate boundary is not enforced yet, so every
|
|
/// shipped tier is selectable by any build — the app is still being tested at 60 fps in
|
|
/// <b>Release</b>, where <c>DevBuild.IsDevelopment</c> is false and the developer override cannot
|
|
/// help. Flip to <c>true</c> when the paid edition ships; the rule itself already exists and its
|
|
/// tests do not change.
|
|
///
|
|
/// <para><b>Why <c>static readonly</c>:</b> a <c>const</c> folds the consuming <c>||</c> at compile
|
|
/// time and leaves an unreachable operand, which is a warning. The JIT folds a readonly field
|
|
/// just as well, and the build stays at zero warnings.</para>
|
|
/// </summary>
|
|
public static readonly bool EntitlementEnforced = false;
|
|
|
|
/// <summary>The default free tier and fallback when no shipped tier is available.</summary>
|
|
public const string FreeFallbackName = "1080p30";
|
|
|
|
/// <summary>Is this tier within the free budget? Pro may use anything shipped.</summary>
|
|
public static bool IsAllowed(QualityOption tier, bool pro)
|
|
=> pro
|
|
|| tier.Fps <= FreeMaxFps;
|
|
|
|
/// <summary>
|
|
/// The tier a session will actually encode. A permitted selection passes through
|
|
/// untouched (so Pro and the free tier's own 720p30 keep their exact bitrate); a
|
|
/// disallowed one resolves to the best tier the budget allows — most pixels first, then
|
|
/// most frames, then the cheapest bitrate — because silently encoding something the user
|
|
/// did not pick is only acceptable if it is the closest legal thing to it.
|
|
/// </summary>
|
|
/// <param name="requested">What the dropdown says.</param>
|
|
/// <param name="shipped">Every tier the build offers.</param>
|
|
/// <param name="pro">Entitlement, already resolved. Unknown must be passed as false.</param>
|
|
public static QualityOption Resolve(
|
|
QualityOption requested, IEnumerable<QualityOption> shipped, bool pro)
|
|
{
|
|
if (IsAllowed(requested, pro)) return requested;
|
|
|
|
var candidate = shipped
|
|
.Where(tier => IsAllowed(tier, pro))
|
|
.OrderByDescending(tier => tier.Width * tier.Height)
|
|
.ThenByDescending(tier => tier.Fps)
|
|
.ThenBy(tier => tier.Bitrate)
|
|
.FirstOrDefault();
|
|
|
|
// No shipped tier fits the budget: build the floor explicitly rather than trusting
|
|
// the table. A build whose tiers were all redefined must still emit a legal frame.
|
|
return candidate ?? new QualityOption(FreeFallbackName, FreeMaxFps, 8.0, 1920, 1080);
|
|
}
|
|
}
|