From a2748f687d7e9252b82f96eeb8c1dc5e22905483 Mon Sep 17 00:00:00 2001 From: AriusII <35581726+AriusII@users.noreply.github.com> Date: Mon, 21 Sep 2026 19:32:03 +0200 Subject: [PATCH] Separate capability implementation, host evidence, and policy --- docs/adr/0004-capability-matrix.md | 12 + .../PublicAPI.Unshipped.txt | 38 +++ .../CheatEngine.Client.Abstractions/README.md | 5 + .../Runtime/ClientCapabilities.cs | 2 +- .../Runtime/ClientCapabilityAvailability.cs | 38 ++- .../Runtime/ClientCapabilityEvidence.cs | 201 ++++++++++++++ .../Runtime/ClientCapabilityEvidenceGate.cs | 34 +++ .../Runtime/ClientCapabilityEvidenceState.cs | 20 ++ .../Events/UnavailableCapabilityFailure.cs | 5 +- .../Domains/ProbeResult.cs | 49 +++- .../Domains/RuntimeClient.cs | 262 +++++++++++------- .../README.md | 3 +- .../Runtime/ClientCapabilitiesTests.cs | 52 ++++ .../Domains/RuntimeClientTests.cs | 82 +++++- 14 files changed, 664 insertions(+), 139 deletions(-) create mode 100644 libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityEvidence.cs create mode 100644 libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityEvidenceGate.cs create mode 100644 libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityEvidenceState.cs diff --git a/docs/adr/0004-capability-matrix.md b/docs/adr/0004-capability-matrix.md index 0be6b3a..f767687 100644 --- a/docs/adr/0004-capability-matrix.md +++ b/docs/adr/0004-capability-matrix.md @@ -32,6 +32,18 @@ only because an abstraction can describe it. assuming an unproven SDK ownership, affinity, or cancellation contract. - **Deferred**: V1 intentionally provides no operational route through the aggregate Client. +### Runtime evidence + +`ClientCapabilityAvailability.Evidence` records six independent gates for every Client capability: +implementation, the consumed package artifact, host observation, live qualification, activation policy, and +activation lifetime. The legacy `State` is only a projection of that evidence: it is `Available` when every gate is +satisfied, `Unavailable` when a gate is known to be missing, and `Unknown` when the remaining evidence is unknown, +faulted, or malformed. A faulted or malformed host observation is deliberately not reported as an unavailable host. + +An explicit `EnableUnsafeLuaExecution()` opt-in satisfies the policy gate only. It never establishes that the package +contains an implementation, that the active Cheat Engine host exposes the required globals, or that the operation has +been live-qualified. + | Area | Public surface | Current source status | Boundary before live qualification | |---|---|---|---| | Plugin lifecycle and DI | `CheatEngineClientPlugin`, `CheatEnginePluginBuilder`, modules, options | Implemented | Exercise enable, rollback, disable, and repeated epoch activation in a CE host. | diff --git a/libs/CheatEngine.Client.Abstractions/PublicAPI.Unshipped.txt b/libs/CheatEngine.Client.Abstractions/PublicAPI.Unshipped.txt index 5fc66a3..a2939ba 100644 --- a/libs/CheatEngine.Client.Abstractions/PublicAPI.Unshipped.txt +++ b/libs/CheatEngine.Client.Abstractions/PublicAPI.Unshipped.txt @@ -17,6 +17,44 @@ static CheatEngine.Client.Runtime.ClientCapabilityId.Hotkeys.get -> CheatEngine. static CheatEngine.Client.Runtime.ClientCapabilityId.RemoteExecution.get -> CheatEngine.Client.Runtime.ClientCapabilityId static CheatEngine.Client.Runtime.ClientCapabilityId.Speed.get -> CheatEngine.Client.Runtime.ClientCapabilityId static CheatEngine.Client.Runtime.ClientCapabilityId.Timers.get -> CheatEngine.Client.Runtime.ClientCapabilityId +CheatEngine.Client.Runtime.ClientCapabilityAvailability.ClientCapabilityAvailability(CheatEngine.Client.Runtime.ClientCapabilityId capability, CheatEngine.Client.Runtime.ClientCapabilityEvidence evidence) -> void +CheatEngine.Client.Runtime.ClientCapabilityAvailability.Evidence.get -> CheatEngine.Client.Runtime.ClientCapabilityEvidence +CheatEngine.Client.Runtime.ClientCapabilityEvidence +CheatEngine.Client.Runtime.ClientCapabilityEvidence.ClientCapabilityEvidence() -> void +CheatEngine.Client.Runtime.ClientCapabilityEvidence.ClientCapabilityEvidence(CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate implementation, CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate package, CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate host, CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate liveQualification, CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate policy, CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate lifetime) -> void +CheatEngine.Client.Runtime.ClientCapabilityEvidence.AvailabilityState.get -> CheatEngine.Client.Runtime.ClientCapabilityAvailabilityState +CheatEngine.Client.Runtime.ClientCapabilityEvidence.EffectiveReason.get -> string! +CheatEngine.Client.Runtime.ClientCapabilityEvidence.Host.get -> CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate +CheatEngine.Client.Runtime.ClientCapabilityEvidence.Implementation.get -> CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate +CheatEngine.Client.Runtime.ClientCapabilityEvidence.IsExecutable.get -> bool +CheatEngine.Client.Runtime.ClientCapabilityEvidence.Lifetime.get -> CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate +CheatEngine.Client.Runtime.ClientCapabilityEvidence.LiveQualification.get -> CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate +CheatEngine.Client.Runtime.ClientCapabilityEvidence.Package.get -> CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate +CheatEngine.Client.Runtime.ClientCapabilityEvidence.Policy.get -> CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate +CheatEngine.Client.Runtime.ClientCapabilityEvidence.Equals(CheatEngine.Client.Runtime.ClientCapabilityEvidence other) -> bool +CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate +CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate.ClientCapabilityEvidenceGate() -> void +CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate.ClientCapabilityEvidenceGate(CheatEngine.Client.Runtime.ClientCapabilityEvidenceState state, string! reason) -> void +CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate.IsSatisfied.get -> bool +CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate.Reason.get -> string! +CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate.State.get -> CheatEngine.Client.Runtime.ClientCapabilityEvidenceState +CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate.Equals(CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate other) -> bool +CheatEngine.Client.Runtime.ClientCapabilityEvidenceState +CheatEngine.Client.Runtime.ClientCapabilityEvidenceState.Faulted = 3 -> CheatEngine.Client.Runtime.ClientCapabilityEvidenceState +CheatEngine.Client.Runtime.ClientCapabilityEvidenceState.Malformed = 4 -> CheatEngine.Client.Runtime.ClientCapabilityEvidenceState +CheatEngine.Client.Runtime.ClientCapabilityEvidenceState.Missing = 2 -> CheatEngine.Client.Runtime.ClientCapabilityEvidenceState +CheatEngine.Client.Runtime.ClientCapabilityEvidenceState.Satisfied = 1 -> CheatEngine.Client.Runtime.ClientCapabilityEvidenceState +CheatEngine.Client.Runtime.ClientCapabilityEvidenceState.Unknown = 0 -> CheatEngine.Client.Runtime.ClientCapabilityEvidenceState +override CheatEngine.Client.Runtime.ClientCapabilityEvidence.GetHashCode() -> int +override CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate.GetHashCode() -> int +static CheatEngine.Client.Runtime.ClientCapabilityEvidence.operator !=(CheatEngine.Client.Runtime.ClientCapabilityEvidence left, CheatEngine.Client.Runtime.ClientCapabilityEvidence right) -> bool +static CheatEngine.Client.Runtime.ClientCapabilityEvidence.operator ==(CheatEngine.Client.Runtime.ClientCapabilityEvidence left, CheatEngine.Client.Runtime.ClientCapabilityEvidence right) -> bool +static CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate.operator !=(CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate left, CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate right) -> bool +static CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate.operator ==(CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate left, CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate right) -> bool +~override CheatEngine.Client.Runtime.ClientCapabilityEvidence.Equals(object obj) -> bool +~override CheatEngine.Client.Runtime.ClientCapabilityEvidence.ToString() -> string +~override CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate.Equals(object obj) -> bool +~override CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate.ToString() -> string CheatEngine.Client.Processes.IProcessClient.AttachForeground(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Processes.ProcessSnapshot CheatEngine.Client.Processes.IProcessClient.Create(CheatEngine.Client.Processes.ProcessStartRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Processes.ProcessSnapshot CheatEngine.Client.Processes.IProcessClient.GetPauseState(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Processes.ProcessPauseSnapshot diff --git a/libs/CheatEngine.Client.Abstractions/README.md b/libs/CheatEngine.Client.Abstractions/README.md index d344bae..bb98602 100644 --- a/libs/CheatEngine.Client.Abstractions/README.md +++ b/libs/CheatEngine.Client.Abstractions/README.md @@ -74,6 +74,11 @@ The contracts describe runtime, process, memory, inspection, AOB scanning, table and explicitly disposable value-scan sessions. A contract is not an availability promise: callers must inspect `ICheatEngineRuntime` capability observations or handle `CapabilityUnavailable`. +Each `ClientCapabilityAvailability` also exposes immutable `Evidence`: implementation, consumed package artifact, +host observation, live qualification, policy, and activation lifetime are distinct gates. `Available` requires all six; +missing, faulted, and malformed host observations remain distinguishable instead of being collapsed into a generic +unavailable result. + In particular, the value-scan contract and state model are published, but the Core implementation does **not** currently create a live `MemScan`/`FoundList` session. The next SDK line now contains a production owner factory with parent rollback and child-before-parent teardown, but Client diff --git a/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilities.cs b/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilities.cs index a3ff002..3d3ea11 100644 --- a/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilities.cs +++ b/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilities.cs @@ -63,7 +63,7 @@ public static ClientCapabilities Create(ReadOnlySpanCreates an availability projection from independently sourced prerequisite evidence. + public ClientCapabilityAvailability(ClientCapabilityId capability, ClientCapabilityEvidence evidence) + { + if (capability.IsEmpty) + { + throw new ArgumentException("A Client capability identifier is required.", nameof(capability)); + } + + _ = evidence.EffectiveReason; + Capability = capability; + Evidence = evidence; + State = evidence.AvailabilityState; + Reason = evidence.EffectiveReason; } /// Gets the stable Client-owned capability identifier. @@ -44,6 +60,12 @@ public ClientCapabilityAvailabilityState State get; } + /// Gets the separately observed implementation, package, host, qualification, policy, and lifetime gates. + public ClientCapabilityEvidence Evidence + { + get; + } + /// Gets the explicit probe, policy, or gate reason behind the state. public string Reason { @@ -55,4 +77,16 @@ public string Reason /// Gets whether this capability was explicitly established as available or unavailable. public bool IsKnown => State != ClientCapabilityAvailabilityState.Unknown; + + private static ClientCapabilityEvidence CreateLegacyEvidence(ClientCapabilityAvailabilityState state, string reason) + { + ClientCapabilityEvidenceState evidenceState = state switch + { + ClientCapabilityAvailabilityState.Available => ClientCapabilityEvidenceState.Satisfied, + ClientCapabilityAvailabilityState.Unavailable => ClientCapabilityEvidenceState.Missing, + _ => ClientCapabilityEvidenceState.Unknown + }; + ClientCapabilityEvidenceGate gate = new(evidenceState, reason); + return new ClientCapabilityEvidence(gate, gate, gate, gate, gate, gate); + } } diff --git a/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityEvidence.cs b/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityEvidence.cs new file mode 100644 index 0000000..8917c9d --- /dev/null +++ b/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityEvidence.cs @@ -0,0 +1,201 @@ +namespace CheatEngine.Client.Runtime; + +/// +/// Immutable, independently sourced prerequisites for one Client capability. A satisfied policy never establishes +/// package, host, qualification, or activation-lifetime evidence. +/// +public readonly record struct ClientCapabilityEvidence +{ + /// Creates validated evidence for one Client capability. + public ClientCapabilityEvidence( + ClientCapabilityEvidenceGate implementation, + ClientCapabilityEvidenceGate package, + ClientCapabilityEvidenceGate host, + ClientCapabilityEvidenceGate liveQualification, + ClientCapabilityEvidenceGate policy, + ClientCapabilityEvidenceGate lifetime) + { + Validate(implementation, nameof(implementation)); + Validate(package, nameof(package)); + Validate(host, nameof(host)); + Validate(liveQualification, nameof(liveQualification)); + Validate(policy, nameof(policy)); + Validate(lifetime, nameof(lifetime)); + + Implementation = implementation; + Package = package; + Host = host; + LiveQualification = liveQualification; + Policy = policy; + Lifetime = lifetime; + } + + /// Gets whether an operational Client adapter is delivered for this capability. + public ClientCapabilityEvidenceGate Implementation + { + get; + } + + /// Gets whether the required primitive is established in the consumed package artifact. + public ClientCapabilityEvidenceGate Package + { + get; + } + + /// Gets the independent observation of the required Cheat Engine host primitive. + public ClientCapabilityEvidenceGate Host + { + get; + } + + /// Gets whether the required host profile has passed its explicit live qualification gate. + public ClientCapabilityEvidenceGate LiveQualification + { + get; + } + + /// Gets whether Client policy permits this capability for the current activation. + public ClientCapabilityEvidenceGate Policy + { + get; + } + + /// Gets whether the activation lifetime required by this capability is current. + public ClientCapabilityEvidenceGate Lifetime + { + get; + } + + /// Gets the legacy availability projection without collapsing the underlying evidence dimensions. + public ClientCapabilityAvailabilityState AvailabilityState + { + get + { + if (HasState(ClientCapabilityEvidenceState.Missing)) + { + return ClientCapabilityAvailabilityState.Unavailable; + } + + return AllSatisfied + ? ClientCapabilityAvailabilityState.Available + : ClientCapabilityAvailabilityState.Unknown; + } + } + + /// Gets whether every prerequisite has been independently established. + public bool IsExecutable => AllSatisfied; + + /// Gets the reason for the highest-priority missing, faulted, malformed, or unknown prerequisite. + public string EffectiveReason => GetEffectiveGate().Reason; + + private bool AllSatisfied => + Implementation.IsSatisfied && Package.IsSatisfied && Host.IsSatisfied && LiveQualification.IsSatisfied && + Policy.IsSatisfied && Lifetime.IsSatisfied; + + private ClientCapabilityEvidenceGate GetEffectiveGate() + { + if (Lifetime.State == ClientCapabilityEvidenceState.Missing) + { + return Lifetime; + } + + if (Policy.State == ClientCapabilityEvidenceState.Missing) + { + return Policy; + } + + if (Implementation.State == ClientCapabilityEvidenceState.Missing) + { + return Implementation; + } + + if (Package.State == ClientCapabilityEvidenceState.Missing) + { + return Package; + } + + if (Host.State == ClientCapabilityEvidenceState.Missing) + { + return Host; + } + + if (LiveQualification.State == ClientCapabilityEvidenceState.Missing) + { + return LiveQualification; + } + + if (Host.State is ClientCapabilityEvidenceState.Faulted or ClientCapabilityEvidenceState.Malformed) + { + return Host; + } + + if (Package.State is ClientCapabilityEvidenceState.Faulted or ClientCapabilityEvidenceState.Malformed) + { + return Package; + } + + if (LiveQualification.State is ClientCapabilityEvidenceState.Faulted or ClientCapabilityEvidenceState.Malformed) + { + return LiveQualification; + } + + if (Implementation.State is ClientCapabilityEvidenceState.Faulted or ClientCapabilityEvidenceState.Malformed) + { + return Implementation; + } + + if (Policy.State is ClientCapabilityEvidenceState.Faulted or ClientCapabilityEvidenceState.Malformed) + { + return Policy; + } + + if (Lifetime.State is ClientCapabilityEvidenceState.Faulted or ClientCapabilityEvidenceState.Malformed) + { + return Lifetime; + } + + if (Host.State == ClientCapabilityEvidenceState.Unknown) + { + return Host; + } + + if (Package.State == ClientCapabilityEvidenceState.Unknown) + { + return Package; + } + + if (LiveQualification.State == ClientCapabilityEvidenceState.Unknown) + { + return LiveQualification; + } + + if (Implementation.State == ClientCapabilityEvidenceState.Unknown) + { + return Implementation; + } + + if (Policy.State == ClientCapabilityEvidenceState.Unknown) + { + return Policy; + } + + return Lifetime; + } + + private bool HasState(ClientCapabilityEvidenceState state) + { + return Implementation.State == state || Package.State == state || Host.State == state || + LiveQualification.State == state || Policy.State == state || Lifetime.State == state; + } + + private static void Validate(ClientCapabilityEvidenceGate gate, string parameterName) + { + if (!Enum.IsDefined(gate.State)) + { + throw new ArgumentOutOfRangeException(parameterName, gate.State, + "The Client capability evidence state is not defined."); + } + + ArgumentException.ThrowIfNullOrWhiteSpace(gate.Reason); + } +} diff --git a/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityEvidenceGate.cs b/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityEvidenceGate.cs new file mode 100644 index 0000000..3ee5775 --- /dev/null +++ b/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityEvidenceGate.cs @@ -0,0 +1,34 @@ +namespace CheatEngine.Client.Runtime; + +/// One immutable capability prerequisite together with its explicit observation reason. +public readonly record struct ClientCapabilityEvidenceGate +{ + /// Creates a validated capability prerequisite observation. + public ClientCapabilityEvidenceGate(ClientCapabilityEvidenceState state, string reason) + { + if (!Enum.IsDefined(state)) + { + throw new ArgumentOutOfRangeException(nameof(state), state, + "The Client capability evidence state is not defined."); + } + + ArgumentException.ThrowIfNullOrWhiteSpace(reason); + State = state; + Reason = reason; + } + + /// Gets the independently observed prerequisite state. + public ClientCapabilityEvidenceState State + { + get; + } + + /// Gets the bounded reason for this prerequisite observation. + public string Reason + { + get; + } + + /// Gets whether the prerequisite has been established. + public bool IsSatisfied => State == ClientCapabilityEvidenceState.Satisfied; +} diff --git a/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityEvidenceState.cs b/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityEvidenceState.cs new file mode 100644 index 0000000..77d45a6 --- /dev/null +++ b/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityEvidenceState.cs @@ -0,0 +1,20 @@ +namespace CheatEngine.Client.Runtime; + +/// Describes one independently established prerequisite in a Client capability evidence record. +public enum ClientCapabilityEvidenceState : byte +{ + /// The Client has not established this prerequisite for the current observation. + Unknown = 0, + + /// The prerequisite was established for the current observation. + Satisfied = 1, + + /// The prerequisite is known to be absent or denied. + Missing = 2, + + /// The prerequisite probe reached the host but failed before it could establish a result. + Faulted = 3, + + /// The host returned a result that cannot satisfy the documented prerequisite shape. + Malformed = 4 +} diff --git a/libs/CheatEngine.Client.Core/Domains/Events/UnavailableCapabilityFailure.cs b/libs/CheatEngine.Client.Core/Domains/Events/UnavailableCapabilityFailure.cs index 15e68e8..3f1b6b6 100644 --- a/libs/CheatEngine.Client.Core/Domains/Events/UnavailableCapabilityFailure.cs +++ b/libs/CheatEngine.Client.Core/Domains/Events/UnavailableCapabilityFailure.cs @@ -17,8 +17,9 @@ internal static CheatEngineFailure Create(string capabilityName, string operatio ? new CheatEngineFailure(CheatEngineFailureKind.Cancelled, operation, "The operation was cancelled before Cheat Engine work began.") : new CheatEngineFailure(CheatEngineFailureKind.CapabilityUnavailable, operation, - $"{capabilityName} is unavailable until its ownership, thread-affinity, cleanup, disable, re-enable, " + - "and target-change behavior pass the required Cheat Engine 7.7 x64 live gate."); + $"{capabilityName} is unavailable because this Client package currently composes an unavailable adapter. " + + "Promotion also requires its ownership, thread-affinity, cleanup, disable, re-enable, and target-change " + + "behavior to pass the required Cheat Engine 7.7 x64 live gate."); } internal static T Throw(CheatEngineFailure failure) diff --git a/libs/CheatEngine.Client.Core/Domains/ProbeResult.cs b/libs/CheatEngine.Client.Core/Domains/ProbeResult.cs index c076e2a..bda6008 100644 --- a/libs/CheatEngine.Client.Core/Domains/ProbeResult.cs +++ b/libs/CheatEngine.Client.Core/Domains/ProbeResult.cs @@ -1,29 +1,54 @@ +using CheatEngine.Client.Runtime; using CheatEngine.SDK.Engine.Runtime; namespace CheatEngine.Client.Core.Domains; -internal readonly record struct ProbeResult( - RuntimeCapabilityId Capability, - RuntimeCapabilityAvailabilityState State, - T? Value) +internal readonly record struct ProbeResult(ClientCapabilityEvidenceGate Evidence, T? Value) { - internal static ProbeResult Available(RuntimeCapabilityId capability, T value) + internal bool HasValue => Evidence.State == ClientCapabilityEvidenceState.Satisfied; + + internal static ProbeResult Available(T value) + { + return new ProbeResult(new ClientCapabilityEvidenceGate(ClientCapabilityEvidenceState.Satisfied, + "The runtime probe returned a value in its documented shape."), value); + } + + internal static ProbeResult MissingGlobal() + { + return new ProbeResult(new ClientCapabilityEvidenceGate(ClientCapabilityEvidenceState.Missing, + "The required Cheat Engine runtime global is not available in this host."), default); + } + + internal static ProbeResult MissingCapability() + { + return new ProbeResult(new ClientCapabilityEvidenceGate(ClientCapabilityEvidenceState.Missing, + "The required Cheat Engine runtime capability is not supported by this host."), default); + } + + internal static ProbeResult Unknown(string reason) { - return new ProbeResult(capability, RuntimeCapabilityAvailabilityState.Available, value); + return new ProbeResult(new ClientCapabilityEvidenceGate(ClientCapabilityEvidenceState.Unknown, reason), default); } - internal static ProbeResult Unavailable(RuntimeCapabilityId capability) + internal static ProbeResult Faulted(string reason) { - return new ProbeResult(capability, RuntimeCapabilityAvailabilityState.Unavailable, default); + return new ProbeResult(new ClientCapabilityEvidenceGate(ClientCapabilityEvidenceState.Faulted, reason), default); } - internal static ProbeResult Unknown(RuntimeCapabilityId capability) + internal static ProbeResult Malformed(string reason) { - return new ProbeResult(capability, RuntimeCapabilityAvailabilityState.Unknown, default); + return new ProbeResult(new ClientCapabilityEvidenceGate(ClientCapabilityEvidenceState.Malformed, reason), + default); } - internal RuntimeCapabilityAvailability ToAvailability() + internal RuntimeCapabilityAvailability ToAvailability(RuntimeCapabilityId capability) { - return new RuntimeCapabilityAvailability(Capability, State, RuntimeCapabilityContract.Unknown); + RuntimeCapabilityAvailabilityState state = Evidence.State switch + { + ClientCapabilityEvidenceState.Satisfied => RuntimeCapabilityAvailabilityState.Available, + ClientCapabilityEvidenceState.Missing => RuntimeCapabilityAvailabilityState.Unavailable, + _ => RuntimeCapabilityAvailabilityState.Unknown + }; + return new RuntimeCapabilityAvailability(capability, state, RuntimeCapabilityContract.Unknown); } } diff --git a/libs/CheatEngine.Client.Core/Domains/RuntimeClient.cs b/libs/CheatEngine.Client.Core/Domains/RuntimeClient.cs index fdfd1fd..6280cdd 100644 --- a/libs/CheatEngine.Client.Core/Domains/RuntimeClient.cs +++ b/libs/CheatEngine.Client.Core/Domains/RuntimeClient.cs @@ -1,5 +1,3 @@ -using System.Globalization; - using CheatEngine.Client.Core.Infrastructure; using CheatEngine.Client.Dispatching; using CheatEngine.Client.Results; @@ -15,6 +13,7 @@ internal sealed class RuntimeClient : ICheatEngineRuntime private readonly Version _clientAssemblyVersion; private readonly ICheatEngineDispatcher _dispatcher; private readonly Func _getEpoch; + private readonly Func _isActivationCurrent; private readonly CoreClientPolicy _policy; private readonly IRuntimeProbe _probe; private readonly Version _sdkAssemblyVersion; @@ -26,7 +25,8 @@ internal RuntimeClient(ICheatEngineDispatcher dispatcher, CoreLifetime lifetime, () => lifetime.Epoch, typeof(ICheatEngineRuntime).Assembly.GetName().Version, typeof(RuntimeInfo).Assembly.GetName().Version, - policy) + policy, + () => lifetime.IsCurrent) { ArgumentNullException.ThrowIfNull(lifetime); } @@ -37,11 +37,13 @@ internal RuntimeClient( Func getEpoch, Version? clientAssemblyVersion = null, Version? sdkAssemblyVersion = null, - CoreClientPolicy? policy = null) + CoreClientPolicy? policy = null, + Func? isActivationCurrent = null) { _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); _probe = probe ?? throw new ArgumentNullException(nameof(probe)); _getEpoch = getEpoch ?? throw new ArgumentNullException(nameof(getEpoch)); + _isActivationCurrent = isActivationCurrent ?? (static () => true); _policy = policy ?? CoreClientPolicy.SafeDefaults; _clientAssemblyVersion = clientAssemblyVersion ?? typeof(ICheatEngineRuntime).Assembly.GetName().Version ?? throw new InvalidOperationException("The Client assembly does not declare an assembly version."); @@ -168,39 +170,28 @@ public ClientCapabilityAvailability GetClientCapability( private CheatEngineRuntimeSnapshot Capture() { - ProbeResult version = Probe(RuntimeCapabilityId.CheatEngineVersion, _probe.GetCheatEngineVersion); - ProbeResult systemArchitecture = - Probe(RuntimeCapabilityId.SystemArchitecture, _probe.GetSystemArchitecture); - ProbeResult targetAbi = Probe(RuntimeCapabilityId.TargetAbi, _probe.GetTargetAbi); + ProbeResult version = ValidateVersion(Probe(_probe.GetCheatEngineVersion)); + ProbeResult systemArchitecture = Probe(_probe.GetSystemArchitecture); + ProbeResult targetAbi = Probe(_probe.GetTargetAbi); + ProbeResult openedProcess = ValidateOpenedProcess(Probe(_probe.GetOpenedProcessId)); - bool hasTarget = TryReadOpenedProcess(out long openedProcessId) && openedProcessId > 0; + bool hasTarget = openedProcess.HasValue && openedProcess.Value > 0; ProbeResult targetArchitecture = hasTarget - ? Probe(RuntimeCapabilityId.TargetArchitecture, _probe.TargetIs64Bit) - : ProbeResult.Unknown(RuntimeCapabilityId.TargetArchitecture); + ? Probe(_probe.TargetIs64Bit) + : ProbeResult.Unknown("No target process is selected, so target architecture was not probed."); - double? observedVersion = version.State == RuntimeCapabilityAvailabilityState.Available - ? version.Value - : null; - if (observedVersion is { } reported && (!double.IsFinite(reported) || reported < 0)) - { - throw new EngineMarshallingException( - "Runtime.GetCheatEngineVersion", - EngineMarshallingDirection.Result, - "a finite non-negative number", - reported.ToString(CultureInfo.InvariantCulture)); - } - - CheatEngineArchitecture decodedSystemArchitecture = DecodeSystemArchitecture(systemArchitecture); - TargetAbi decodedTargetAbi = DecodeTargetAbi(targetAbi); + double? observedVersion = version.HasValue ? version.Value : null; + CheatEngineArchitecture decodedSystemArchitecture = DecodeSystemArchitecture(ref systemArchitecture); + TargetAbi decodedTargetAbi = DecodeTargetAbi(ref targetAbi); CheatEngineArchitecture decodedTargetArchitecture = - DecodeTargetArchitecture(targetArchitecture, decodedTargetAbi); + DecodeTargetArchitecture(ref targetArchitecture, decodedTargetAbi); RuntimeCapabilityAvailability[] capabilities = [ - version.ToAvailability(), - systemArchitecture.ToAvailability(), - targetArchitecture.ToAvailability(), - targetAbi.ToAvailability() + version.ToAvailability(RuntimeCapabilityId.CheatEngineVersion), + systemArchitecture.ToAvailability(RuntimeCapabilityId.SystemArchitecture), + targetArchitecture.ToAvailability(RuntimeCapabilityId.TargetArchitecture), + targetAbi.ToAvailability(RuntimeCapabilityId.TargetAbi) ]; return new CheatEngineRuntimeSnapshot( @@ -213,130 +204,187 @@ private CheatEngineRuntimeSnapshot Capture() PointerSize.FromArchitecture(decodedTargetArchitecture), decodedTargetAbi), RuntimeCapabilities.Create(capabilities), - CreateClientCapabilities()); + CreateClientCapabilities(openedProcess)); } - private ClientCapabilities CreateClientCapabilities() + private ClientCapabilities CreateClientCapabilities(ProbeResult openedProcess) { + ClientCapabilityEvidenceGate lifetime = _isActivationCurrent() + ? Satisfied("The Client activation is current.") + : Missing("The Client activation is no longer current."); + ClientCapabilityEvidenceGate packageUnknown = UnknownEvidence( + "The runtime snapshot does not establish the identity of the consumed SDK package artifact."); + ClientCapabilityEvidenceGate qualificationUnknown = UnknownEvidence( + "No complete Cheat Engine 7.7 x64 live qualification record is attached to this capability observation."); + ClientCapabilityEvidenceGate policyNotRequired = Satisfied( + "This capability has no additional activation policy opt-in."); + ClientCapabilityEvidenceGate unprobedHost = UnknownEvidence( + "The runtime snapshot does not probe every host primitive required by this capability."); + ClientCapabilityEvidenceGate implemented = Satisfied( + "The Client composes an operational adapter for this capability."); + ClientCapabilityEvidenceGate contractOnly = Missing( + "The Client package currently composes only an unavailable adapter for this capability."); + ClientCapabilityAvailability[] capabilities = [ - Unknown(ClientCapabilityId.ProcessSelection, - "The runtime snapshot does not probe every backing process-selection primitive, including openProcess."), - Unknown(ClientCapabilityId.TypedMemory, - "The runtime snapshot does not probe every backing typed-memory primitive."), - Unknown(ClientCapabilityId.PatternScanning, - "The runtime snapshot does not probe every backing AOB scan primitive."), - new( - ClientCapabilityId.ValueScanning, - ClientCapabilityAvailabilityState.Unavailable, - "Value scan sessions remain unavailable until the CE 7.7 live ownership and cleanup gate passes."), - Unknown(ClientCapabilityId.Inspection, - "The runtime snapshot does not probe every backing inspection primitive."), - Unknown(ClientCapabilityId.Tables, - "The runtime snapshot does not probe every backing Address List and table primitive."), - Unknown(ClientCapabilityId.ProtectedLua, - "The runtime snapshot does not probe every backing protected Lua primitive."), - _policy.EnableUnsafeLuaExecution - ? new ClientCapabilityAvailability( - ClientCapabilityId.UnsafeLuaExecution, - ClientCapabilityAvailabilityState.Available, - "Unsafe Lua execution was explicitly enabled for this activation.") - : new ClientCapabilityAvailability( - ClientCapabilityId.UnsafeLuaExecution, - ClientCapabilityAvailabilityState.Unavailable, - "Unsafe Lua execution requires explicit EnableUnsafeLuaExecution opt-in for this activation."), - Unavailable(ClientCapabilityId.Allocations, - "Owned target allocations remain unavailable until the SDK production owner and CE 7.7 x64 cleanup gate pass."), - Unknown(ClientCapabilityId.Assembly, - "Assembly primitives and Auto Assembler ownership have not yet passed their CE 7.7 x64 live gate."), - Unavailable(ClientCapabilityId.RemoteExecution, - "Remote execution remains unavailable until allocation, timeout, and cleanup behavior pass the CE 7.7 x64 live gate."), - Unavailable(ClientCapabilityId.Debugger, - "Debugger callback ownership and synchronous continuation have not yet passed the CE 7.7 x64 live gate."), - Unavailable(ClientCapabilityId.Hotkeys, - "Hotkey callback ownership has not yet passed the CE 7.7 x64 live gate."), - Unavailable(ClientCapabilityId.Timers, - "Timer callback ownership has not yet passed the CE 7.7 x64 live gate."), - Unknown(ClientCapabilityId.Speed, - "The runtime snapshot does not yet probe the complete speed-control contract."), - Unknown(ClientCapabilityId.Hashing, - "The runtime snapshot does not yet probe target-memory and file hashing independently."), - Unavailable(ClientCapabilityId.Dbvm, - "DBVM observation, explicit initialization, and watch ownership have not yet passed the CE 7.7 x64 live gate.") + Describe(ClientCapabilityId.ProcessSelection, implemented, packageUnknown, openedProcess.Evidence, + qualificationUnknown, policyNotRequired, lifetime), + Describe(ClientCapabilityId.TypedMemory, implemented, packageUnknown, unprobedHost, qualificationUnknown, + policyNotRequired, lifetime), + Describe(ClientCapabilityId.PatternScanning, implemented, packageUnknown, unprobedHost, qualificationUnknown, + policyNotRequired, lifetime), + Describe(ClientCapabilityId.ValueScanning, contractOnly, + Missing("CheatEngine.SDK 1.0.0 does not provide the public MemScan and FoundList ownership factory required by Client."), + unprobedHost, qualificationUnknown, policyNotRequired, lifetime), + Describe(ClientCapabilityId.Inspection, implemented, packageUnknown, unprobedHost, qualificationUnknown, + policyNotRequired, lifetime), + Describe(ClientCapabilityId.Tables, implemented, packageUnknown, unprobedHost, qualificationUnknown, + policyNotRequired, lifetime), + Describe(ClientCapabilityId.ProtectedLua, implemented, packageUnknown, unprobedHost, qualificationUnknown, + policyNotRequired, lifetime), + Describe(ClientCapabilityId.UnsafeLuaExecution, implemented, packageUnknown, unprobedHost, + qualificationUnknown, + _policy.EnableUnsafeLuaExecution + ? Satisfied("Unsafe Lua execution was explicitly enabled for this activation.") + : Missing("Unsafe Lua execution requires explicit EnableUnsafeLuaExecution opt-in for this activation."), + lifetime), + Describe(ClientCapabilityId.Allocations, contractOnly, packageUnknown, unprobedHost, qualificationUnknown, + policyNotRequired, lifetime), + Describe(ClientCapabilityId.Assembly, contractOnly, packageUnknown, unprobedHost, qualificationUnknown, + policyNotRequired, lifetime), + Describe(ClientCapabilityId.RemoteExecution, contractOnly, packageUnknown, unprobedHost, qualificationUnknown, + policyNotRequired, lifetime), + Describe(ClientCapabilityId.Debugger, contractOnly, packageUnknown, unprobedHost, qualificationUnknown, + policyNotRequired, lifetime), + Describe(ClientCapabilityId.Hotkeys, contractOnly, packageUnknown, unprobedHost, qualificationUnknown, + policyNotRequired, lifetime), + Describe(ClientCapabilityId.Timers, contractOnly, packageUnknown, unprobedHost, qualificationUnknown, + policyNotRequired, lifetime), + Describe(ClientCapabilityId.Speed, contractOnly, packageUnknown, unprobedHost, qualificationUnknown, + policyNotRequired, lifetime), + Describe(ClientCapabilityId.Hashing, contractOnly, packageUnknown, unprobedHost, qualificationUnknown, + policyNotRequired, lifetime), + Describe(ClientCapabilityId.Dbvm, contractOnly, packageUnknown, unprobedHost, qualificationUnknown, + policyNotRequired, lifetime) ]; return ClientCapabilities.Create(capabilities); } - private static ClientCapabilityAvailability Unknown(ClientCapabilityId capability, string reason) + private static ClientCapabilityAvailability Describe( + ClientCapabilityId capability, + ClientCapabilityEvidenceGate implementation, + ClientCapabilityEvidenceGate package, + ClientCapabilityEvidenceGate host, + ClientCapabilityEvidenceGate qualification, + ClientCapabilityEvidenceGate policy, + ClientCapabilityEvidenceGate lifetime) + { + return new ClientCapabilityAvailability(capability, + new ClientCapabilityEvidence(implementation, package, host, qualification, policy, lifetime)); + } + + private static ClientCapabilityEvidenceGate Satisfied(string reason) { - return new ClientCapabilityAvailability(capability, ClientCapabilityAvailabilityState.Unknown, reason); + return new ClientCapabilityEvidenceGate(ClientCapabilityEvidenceState.Satisfied, reason); } - private static ClientCapabilityAvailability Unavailable(ClientCapabilityId capability, string reason) + private static ClientCapabilityEvidenceGate Missing(string reason) { - return new ClientCapabilityAvailability(capability, ClientCapabilityAvailabilityState.Unavailable, reason); + return new ClientCapabilityEvidenceGate(ClientCapabilityEvidenceState.Missing, reason); } - private static CheatEngineArchitecture DecodeSystemArchitecture(ProbeResult probe) + private static ClientCapabilityEvidenceGate UnknownEvidence(string reason) { - return probe is { State: RuntimeCapabilityAvailabilityState.Available, Value: int architecture } && - RuntimeInfo.TryDecodeSystemArchitecture(architecture, out CheatEngineArchitecture decoded) - ? decoded - : CheatEngineArchitecture.Unknown; + return new ClientCapabilityEvidenceGate(ClientCapabilityEvidenceState.Unknown, reason); } - private static TargetAbi DecodeTargetAbi(ProbeResult probe) + private static ProbeResult ValidateVersion(ProbeResult probe) { - return probe is { State: RuntimeCapabilityAvailabilityState.Available, Value: int abi } && - RuntimeInfo.TryDecodeTargetAbi(abi, out TargetAbi decoded) - ? decoded - : TargetAbi.Unknown; + return probe.HasValue && probe.Value is { } version && (!double.IsFinite(version) || version < 0) + ? ProbeResult.Malformed("Cheat Engine returned a version that is not a finite non-negative number.") + : probe; } - private static CheatEngineArchitecture DecodeTargetArchitecture(ProbeResult probe, TargetAbi targetAbi) + private static ProbeResult ValidateOpenedProcess(ProbeResult probe) { - if (targetAbi != TargetAbi.Windows || - probe is not { State: RuntimeCapabilityAvailabilityState.Available, Value: bool is64Bit }) + return probe.HasValue && probe.Value is { } processId && + (processId < 0 || processId > int.MaxValue) + ? ProbeResult.Malformed("Cheat Engine returned an opened process identifier outside the supported PID range.") + : probe; + } + + private static CheatEngineArchitecture DecodeSystemArchitecture(ref ProbeResult probe) + { + if (!probe.HasValue) { return CheatEngineArchitecture.Unknown; } - return is64Bit ? CheatEngineArchitecture.X64 : CheatEngineArchitecture.X86; + if (RuntimeInfo.TryDecodeSystemArchitecture(probe.Value, out CheatEngineArchitecture architecture)) + { + return architecture; + } + + probe = ProbeResult.Malformed("Cheat Engine returned an unsupported system architecture code."); + return CheatEngineArchitecture.Unknown; } - private bool TryReadOpenedProcess(out long processId) + private static TargetAbi DecodeTargetAbi(ref ProbeResult probe) { - try + if (!probe.HasValue) { - processId = _probe.GetOpenedProcessId(); - return true; + return TargetAbi.Unknown; } - catch (EngineGlobalUnavailableException) + + if (RuntimeInfo.TryDecodeTargetAbi(probe.Value, out TargetAbi targetAbi)) { - processId = 0; - return false; + return targetAbi; } - catch (EngineCapabilityUnavailableException) + + probe = ProbeResult.Malformed("Cheat Engine returned an unsupported target ABI code."); + return TargetAbi.Unknown; + } + + private static CheatEngineArchitecture DecodeTargetArchitecture(ref ProbeResult probe, TargetAbi targetAbi) + { + if (!probe.HasValue || targetAbi == TargetAbi.Unknown) { - processId = 0; - return false; + return CheatEngineArchitecture.Unknown; } + + if (targetAbi != TargetAbi.Windows) + { + probe = ProbeResult.Unknown( + "The target architecture probe is not qualified for the observed target ABI."); + return CheatEngineArchitecture.Unknown; + } + + return probe.Value ? CheatEngineArchitecture.X64 : CheatEngineArchitecture.X86; } - private static ProbeResult Probe(RuntimeCapabilityId capability, Func probe) + private static ProbeResult Probe(Func probe) { try { - return ProbeResult.Available(capability, probe()); + return ProbeResult.Available(probe()); } catch (EngineGlobalUnavailableException) { - return ProbeResult.Unavailable(capability); + return ProbeResult.MissingGlobal(); } catch (EngineCapabilityUnavailableException) { - return ProbeResult.Unavailable(capability); + return ProbeResult.MissingCapability(); + } + catch (EngineMarshallingException) + { + return ProbeResult.Malformed("The Cheat Engine runtime probe returned a malformed result."); + } + catch (EngineException exception) + { + return ProbeResult.Faulted( + $"The Cheat Engine runtime probe failed with {exception.GetType().Name}."); } } } diff --git a/libs/CheatEngine.Client.Extensions.DependencyInjection/README.md b/libs/CheatEngine.Client.Extensions.DependencyInjection/README.md index f5234b4..fc9af98 100644 --- a/libs/CheatEngine.Client.Extensions.DependencyInjection/README.md +++ b/libs/CheatEngine.Client.Extensions.DependencyInjection/README.md @@ -45,7 +45,8 @@ The builder also provides explicit extension points: modules in that order and disables them in reverse order. - `AddMemoryCodec()` adds a singleton deterministic codec without reflective structure marshalling. - `EnableUnsafeLuaExecution()` registers the unsafe Lua facade only for the current activation policy. It never exposes - an SDK `LuaState`. + an SDK `LuaState`. The opt-in satisfies only the policy evidence gate; it does not prove package support, host + globals, or live qualification. ```csharp using CheatEngine.Client.Extensions.DependencyInjection; diff --git a/tests/CheatEngine.Client.Abstractions.Tests/Runtime/ClientCapabilitiesTests.cs b/tests/CheatEngine.Client.Abstractions.Tests/Runtime/ClientCapabilitiesTests.cs index dac2490..c7eee6f 100644 --- a/tests/CheatEngine.Client.Abstractions.Tests/Runtime/ClientCapabilitiesTests.cs +++ b/tests/CheatEngine.Client.Abstractions.Tests/Runtime/ClientCapabilitiesTests.cs @@ -30,6 +30,58 @@ public void CollectionPreservesDistinctObservedCapabilitiesAndTheirReasons() Assert.True(foundValueScanning.IsKnown); } + [Fact] + public void EvidenceSeparatesIndependentPrerequisitesWithoutPromotingAnOptIn() + { + ClientCapabilityEvidence evidence = new( + new ClientCapabilityEvidenceGate(ClientCapabilityEvidenceState.Satisfied, + "The adapter is implemented."), + new ClientCapabilityEvidenceGate(ClientCapabilityEvidenceState.Unknown, + "The consumed package identity has not been established."), + new ClientCapabilityEvidenceGate(ClientCapabilityEvidenceState.Unknown, + "The host primitive has not been probed."), + new ClientCapabilityEvidenceGate(ClientCapabilityEvidenceState.Unknown, + "No live qualification record is attached."), + new ClientCapabilityEvidenceGate(ClientCapabilityEvidenceState.Satisfied, + "The explicit policy opt-in is present."), + new ClientCapabilityEvidenceGate(ClientCapabilityEvidenceState.Satisfied, + "The activation is current.")); + + ClientCapabilityAvailability availability = new(ClientCapabilityId.UnsafeLuaExecution, evidence); + + Assert.Equal(ClientCapabilityEvidenceState.Satisfied, availability.Evidence.Policy.State); + Assert.Equal(ClientCapabilityEvidenceState.Unknown, availability.Evidence.Host.State); + Assert.Equal(ClientCapabilityEvidenceState.Unknown, availability.Evidence.LiveQualification.State); + Assert.Equal(ClientCapabilityAvailabilityState.Unknown, availability.State); + Assert.False(availability.IsAvailable); + Assert.Contains("host primitive", availability.Reason, StringComparison.OrdinalIgnoreCase); + } + + [Theory] + [InlineData(ClientCapabilityEvidenceState.Missing)] + [InlineData(ClientCapabilityEvidenceState.Faulted)] + [InlineData(ClientCapabilityEvidenceState.Malformed)] + public void EvidenceKeepsMissingFaultedAndMalformedHostPrerequisitesDistinct( + ClientCapabilityEvidenceState hostState) + { + ClientCapabilityEvidenceGate established = new(ClientCapabilityEvidenceState.Satisfied, "Established."); + ClientCapabilityEvidence evidence = new( + established, + established, + new ClientCapabilityEvidenceGate(hostState, "Host-specific result."), + established, + established, + established); + + ClientCapabilityAvailability availability = new(ClientCapabilityId.ProcessSelection, evidence); + + Assert.Equal(hostState, availability.Evidence.Host.State); + Assert.Equal(hostState == ClientCapabilityEvidenceState.Missing + ? ClientCapabilityAvailabilityState.Unavailable + : ClientCapabilityAvailabilityState.Unknown, availability.State); + Assert.Equal("Host-specific result.", availability.Reason); + } + [Fact] public void CollectionRejectsDuplicateCapabilityIdentifiers() { diff --git a/tests/CheatEngine.Client.Core.Tests/Domains/RuntimeClientTests.cs b/tests/CheatEngine.Client.Core.Tests/Domains/RuntimeClientTests.cs index f78156f..8718e3a 100644 --- a/tests/CheatEngine.Client.Core.Tests/Domains/RuntimeClientTests.cs +++ b/tests/CheatEngine.Client.Core.Tests/Domains/RuntimeClientTests.cs @@ -134,7 +134,9 @@ public void SnapshotReportsClientGatesWithoutClaimingUnprobedDomainsAreAvailable Assert.True(snapshot.ClientCapabilities.TryGet(ClientCapabilityId.ValueScanning, out ClientCapabilityAvailability valueScanning)); Assert.Equal(ClientCapabilityAvailabilityState.Unavailable, valueScanning.State); - Assert.Contains("live ownership", valueScanning.Reason, StringComparison.OrdinalIgnoreCase); + Assert.Equal(ClientCapabilityEvidenceState.Missing, valueScanning.Evidence.Implementation.State); + Assert.Equal(ClientCapabilityEvidenceState.Missing, valueScanning.Evidence.Package.State); + Assert.Contains("unavailable adapter", valueScanning.Reason, StringComparison.OrdinalIgnoreCase); Assert.True(snapshot.ClientCapabilities.TryGet(ClientCapabilityId.TypedMemory, out ClientCapabilityAvailability typedMemory)); Assert.Equal(ClientCapabilityAvailabilityState.Unknown, typedMemory.State); @@ -158,22 +160,22 @@ public void SnapshotReportsClientGatesWithoutClaimingUnprobedDomainsAreAvailable Assert.False(availability.IsAvailable); } - ClientCapabilityId[] unknownCapabilities = + ClientCapabilityId[] additionallyUnavailableCapabilities = [ ClientCapabilityId.Assembly, ClientCapabilityId.Speed, ClientCapabilityId.Hashing ]; - foreach (ClientCapabilityId capability in unknownCapabilities) + foreach (ClientCapabilityId capability in additionallyUnavailableCapabilities) { Assert.True(snapshot.ClientCapabilities.TryGet(capability, out ClientCapabilityAvailability availability)); - Assert.Equal(ClientCapabilityAvailabilityState.Unknown, availability.State); - Assert.False(availability.IsKnown); + Assert.Equal(ClientCapabilityAvailabilityState.Unavailable, availability.State); + Assert.Equal(ClientCapabilityEvidenceState.Missing, availability.Evidence.Implementation.State); } } [Fact] - public void SnapshotReportsUnsafeLuaAvailableOnlyForTheExplicitActivationOptIn() + public void SnapshotReportsUnsafeLuaPolicyWithoutTreatingItAsHostEvidence() { RuntimeClient runtime = new( new InlineDispatcher(), @@ -189,23 +191,59 @@ public void SnapshotReportsUnsafeLuaAvailableOnlyForTheExplicitActivationOptIn() Assert.True(succeeded); Assert.Equal(default, failure); - Assert.Equal(ClientCapabilityAvailabilityState.Available, availability.State); - Assert.True(availability.IsAvailable); + Assert.Equal(ClientCapabilityEvidenceState.Satisfied, availability.Evidence.Policy.State); + Assert.Equal(ClientCapabilityEvidenceState.Unknown, availability.Evidence.Host.State); + Assert.Equal(ClientCapabilityEvidenceState.Unknown, availability.Evidence.LiveQualification.State); + Assert.Equal(ClientCapabilityAvailabilityState.Unknown, availability.State); + Assert.False(availability.IsAvailable); } [Fact] - public void GetSnapshotThrowsTheClassifiedFailureWhenTheHostReportsAnInvalidVersion() + public void SnapshotClassifiesAnInvalidVersionAsMalformedWithoutCallingItUnavailable() { RuntimeClient runtime = new( new InlineDispatcher(), new FakeRuntimeProbe { ReportedVersion = double.NaN }, static () => 1); - CheatEngineOperationException exception = Assert.Throws(() => - runtime.GetSnapshot(TestContext.Current.CancellationToken)); + CheatEngineRuntimeSnapshot snapshot = runtime.GetSnapshot(TestContext.Current.CancellationToken); + + Assert.Null(snapshot.ObservedCheatEngineVersion); + Assert.True(snapshot.SdkCapabilities.TryGet(RuntimeCapabilityId.CheatEngineVersion, + out RuntimeCapabilityAvailability availability)); + Assert.Equal(RuntimeCapabilityAvailabilityState.Unknown, availability.State); + } - Assert.Equal(CheatEngineFailureKind.InvalidHostResult, exception.Failure.Kind); - Assert.Equal("Dispatcher.Invoke", exception.Failure.Operation); + [Fact] + public void SnapshotSeparatesMissingFaultedAndMalformedOpenedProcessEvidence() + { + AssertOpenedProcessEvidence( + new FakeRuntimeProbe { OpenedProcessException = new EngineGlobalUnavailableException("Runtime.Process") }, + ClientCapabilityEvidenceState.Missing, + ClientCapabilityAvailabilityState.Unavailable); + AssertOpenedProcessEvidence( + new FakeRuntimeProbe { OpenedProcessException = new EngineOperationFailedException("Runtime.Process") }, + ClientCapabilityEvidenceState.Faulted, + ClientCapabilityAvailabilityState.Unknown); + AssertOpenedProcessEvidence( + new FakeRuntimeProbe { OpenedProcessId = -1 }, + ClientCapabilityEvidenceState.Malformed, + ClientCapabilityAvailabilityState.Unknown); + } + + [Fact] + public void SnapshotReportsAnInactiveActivationAsALifetimeGate() + { + RuntimeClient runtime = new(new InlineDispatcher(), new FakeRuntimeProbe(), static () => 7, + isActivationCurrent: static () => false); + + CheatEngineRuntimeSnapshot snapshot = runtime.GetSnapshot(TestContext.Current.CancellationToken); + + Assert.True(snapshot.ClientCapabilities.TryGet(ClientCapabilityId.TypedMemory, + out ClientCapabilityAvailability availability)); + Assert.Equal(ClientCapabilityEvidenceState.Missing, availability.Evidence.Lifetime.State); + Assert.Equal(ClientCapabilityAvailabilityState.Unavailable, availability.State); + Assert.Contains("no longer current", availability.Reason, StringComparison.OrdinalIgnoreCase); } [Fact] @@ -230,7 +268,8 @@ public void CapabilityQueriesReturnTheObservedSnapshotEntryOrTheDocumentedUnknow Assert.True(clientSucceeded); Assert.Equal(default, clientFailure); Assert.Equal(ClientCapabilityAvailabilityState.Unknown, clientAvailability.State); - Assert.Contains("does not probe", clientAvailability.Reason, StringComparison.OrdinalIgnoreCase); + Assert.Equal(ClientCapabilityEvidenceState.Unknown, clientAvailability.Evidence.Package.State); + Assert.Contains("package artifact", clientAvailability.Reason, StringComparison.OrdinalIgnoreCase); } [Fact] @@ -275,6 +314,21 @@ public void SnapshotMarksUnavailableAndDetachedHostProbesWithoutInventingRuntime Assert.Equal(RuntimeCapabilityAvailabilityState.Unavailable, system.State); } + private static void AssertOpenedProcessEvidence( + FakeRuntimeProbe probe, + ClientCapabilityEvidenceState expectedHostState, + ClientCapabilityAvailabilityState expectedAvailabilityState) + { + RuntimeClient runtime = new(new InlineDispatcher(), probe, static () => 7); + + CheatEngineRuntimeSnapshot snapshot = runtime.GetSnapshot(TestContext.Current.CancellationToken); + + Assert.True(snapshot.ClientCapabilities.TryGet(ClientCapabilityId.ProcessSelection, + out ClientCapabilityAvailability availability)); + Assert.Equal(expectedHostState, availability.Evidence.Host.State); + Assert.Equal(expectedAvailabilityState, availability.State); + } + private sealed class FakeRuntimeProbe : IRuntimeProbe { internal double ReportedVersion