diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5bbd905..ce9b073 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -2,11 +2,6 @@ name: Client validation
on:
workflow_call:
- inputs:
- collect-coverage:
- description: Emit Visual Studio XML coverage for the SonarQube Cloud analysis workflow.
- type: boolean
- default: false
permissions:
contents: read
@@ -49,8 +44,6 @@ jobs:
run: dotnet build CheatEngine.Client.slnx --configuration Release --no-restore --warnaserror
- name: Run unit tests through Microsoft Testing Platform
- env:
- COLLECT_COVERAGE: ${{ inputs.collect-coverage }}
run: |
$projects = @(Get-ChildItem -Path tests -Filter '*.Tests.csproj' -Recurse -File | Sort-Object Name)
if ($projects.Count -eq 0) {
@@ -67,10 +60,6 @@ jobs:
'--results-directory', $results,
'--fail-skips', 'on'
)
- if ($env:COLLECT_COVERAGE -eq 'true') {
- $options += '--coverage', '--coverage-output-format', 'xml', '--coverage-output', 'coverage.xml'
- }
-
dotnet test @options
}
@@ -106,15 +95,6 @@ jobs:
if-no-files-found: warn
retention-days: 14
- - name: Upload Sonar coverage
- if: ${{ inputs.collect-coverage && !cancelled() }}
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
- with:
- name: coverage-client
- path: artifacts/test-results/*/coverage.xml
- if-no-files-found: error
- retention-days: 7
-
- name: Pack and validate public package APIs
run: dotnet pack CheatEngine.Client.slnx --configuration Release --no-build --no-restore
diff --git a/.github/workflows/main-ci.yml b/.github/workflows/main-ci.yml
index c820499..404b801 100644
--- a/.github/workflows/main-ci.yml
+++ b/.github/workflows/main-ci.yml
@@ -32,14 +32,12 @@ jobs:
ci:
name: CI
uses: ./.github/workflows/ci.yml
- with:
- collect-coverage: ${{ github.ref == 'refs/heads/main' }}
# Main analysis is the SonarQube Cloud baseline for pull-request new-code comparisons.
sonar:
name: Sonar
needs: ci
- if: github.ref == 'refs/heads/main'
+ if: vars.SONAR_CI_ENABLED == 'true' && github.ref == 'refs/heads/main'
uses: ./.github/workflows/sonar.yml
secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml
index fb67677..aef0f7c 100644
--- a/.github/workflows/pull-request-ci.yml
+++ b/.github/workflows/pull-request-ci.yml
@@ -30,17 +30,14 @@ jobs:
name: CI
if: github.event.pull_request.draft == false
uses: ./.github/workflows/ci.yml
- with:
- collect-coverage: >-
- ${{ github.event.pull_request.head.repo.full_name == github.repository
- && github.actor != 'dependabot[bot]' }}
- # Fork and Dependabot pull requests receive no repository secret. Keep this condition equal to collect-coverage.
+ # Fork and Dependabot pull requests receive no repository secret, so Sonar skips them.
sonar:
name: Sonar
needs: ci
if: >-
- github.event.pull_request.head.repo.full_name == github.repository
+ vars.SONAR_CI_ENABLED == 'true'
+ && github.event.pull_request.head.repo.full_name == github.repository
&& github.actor != 'dependabot[bot]'
uses: ./.github/workflows/sonar.yml
secrets:
diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml
index 57650ee..1b9a4b2 100644
--- a/.github/workflows/sonar.yml
+++ b/.github/workflows/sonar.yml
@@ -12,12 +12,12 @@ on:
type: string
default: cheatenginenet
wait-quality-gate:
- description: Fail the analysis job when SonarQube Cloud reports a failing Quality Gate.
+ description: Fail the job when the quality gate fails.
type: boolean
default: false
secrets:
SONAR_TOKEN:
- description: SonarQube Cloud token. Analysis is skipped when it is unavailable.
+ description: SonarQube Cloud token. The analysis is skipped when it is empty.
required: false
permissions:
@@ -96,13 +96,6 @@ jobs:
**/packages.lock.json
Directory.Packages.props
- - name: Download coverage
- if: env.SONAR_ENABLED == 'true'
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
- with:
- name: coverage-client
- path: ${{ runner.temp }}/coverage
-
- name: Install scanner
if: env.SONAR_ENABLED == 'true'
run: >-
@@ -118,22 +111,40 @@ jobs:
env:
SONARQUBE_SCANNER_PARAMS: '{"sonar.token":"${{ secrets.SONAR_TOKEN }}"}'
run: |
- $coverage = "$env:RUNNER_TEMP/coverage"
- $reports = @(Get-ChildItem -Path $coverage -Filter *.xml -Recurse -File -ErrorAction SilentlyContinue)
- if ($reports.Count -eq 0) {
- Write-Host '::error::No coverage report was downloaded. Call ci.yml with collect-coverage set to true.'
- exit 1
- }
-
- Write-Host "Coverage reports: $($reports.Count)"
+ # These findings conflict with deliberate repository contracts. Keep them in the scanner configuration so
+ # production and test sources do not need Sonar-only attributes or code changes.
+ $ignoredIssues = @(
+ @{ Key = 'noLinq'; Rule = 'csharpsquid:S3267'; Resource = '**/*.cs' }
+ @{ Key = 'unsafeInterop'; Rule = 'csharpsquid:S6640'; Resource = '**/*.cs' }
+ @{ Key = 'nullableFlow'; Rule = 'csharpsquid:S8970'; Resource = '**/*.cs' }
+ @{ Key = 'apiOverloadLayout'; Rule = 'csharpsquid:S4136'; Resource = 'libs/**' }
+ @{ Key = 'generatorInstances'; Rule = 'csharpsquid:S2325'; Resource = 'source-generators/**' }
+ @{ Key = 'emittedFragments'; Rule = 'csharpsquid:S1192'; Resource = 'source-generators/**' }
+ @{ Key = 'generatorComments'; Rule = 'csharpsquid:S125'; Resource = 'source-generators/**' }
+ @{ Key = 'testReflection'; Rule = 'csharpsquid:S3011'; Resource = 'tests/**' }
+ @{ Key = 'testLiterals'; Rule = 'csharpsquid:S1192'; Resource = 'tests/**' }
+ @{ Key = 'testGc'; Rule = 'csharpsquid:S1215'; Resource = 'tests/**' }
+ @{ Key = 'testStaticHooks'; Rule = 'csharpsquid:S2696'; Resource = 'tests/**' }
+ @{ Key = 'testDoubleDispose'; Rule = 'csharpsquid:S3966'; Resource = 'tests/**' }
+ @{ Key = 'testBooleanTables'; Rule = 'csharpsquid:S1125'; Resource = 'tests/**' }
+ @{ Key = 'testComplexity'; Rule = 'csharpsquid:S3776'; Resource = 'tests/**' }
+ @{ Key = 'testParameters'; Rule = 'csharpsquid:S107'; Resource = 'tests/**' }
+ @{ Key = 'testMarkerClasses'; Rule = 'csharpsquid:S1118'; Resource = 'tests/**' }
+ @{ Key = 'testDuplicateScenarios'; Rule = 'csharpsquid:S4144'; Resource = 'tests/**' }
+ @{ Key = 'testFixedDoubles'; Rule = 'csharpsquid:S3400'; Resource = 'tests/**' }
+ @{ Key = 'testComments'; Rule = 'csharpsquid:S125'; Resource = 'tests/**' }
+ )
$arguments = @(
- "/k:$env:SONAR_PROJECT_KEY",
- "/o:$env:SONAR_ORGANIZATION",
- '/d:sonar.exclusions=artifacts/**',
- '/d:sonar.coverage.exclusions=tests/**',
- "/d:sonar.cs.vscoveragexml.reportsPaths=$coverage/**/*.xml",
+ "/k:$env:SONAR_PROJECT_KEY"
+ "/o:$env:SONAR_ORGANIZATION"
+ '/d:sonar.exclusions=artifacts/**,tests/CheatEngine.Client.Benchmarks/**'
+ "/d:sonar.issue.ignore.multicriteria=$(($ignoredIssues.Key) -join ',')"
"/d:sonar.qualitygate.wait=$env:SONAR_WAIT_QUALITY_GATE"
)
+ foreach ($issue in $ignoredIssues) {
+ $arguments += "/d:sonar.issue.ignore.multicriteria.$($issue.Key).ruleKey=$($issue.Rule)"
+ $arguments += "/d:sonar.issue.ignore.multicriteria.$($issue.Key).resourceKey=$($issue.Resource)"
+ }
& "$env:RUNNER_TEMP/sonar-scanner/dotnet-sonarscanner.exe" begin @arguments
- name: Build
diff --git a/CheatEngine.Client.slnx b/CheatEngine.Client.slnx
index 710e369..c680960 100644
--- a/CheatEngine.Client.slnx
+++ b/CheatEngine.Client.slnx
@@ -25,14 +25,21 @@
+
+
+
+
+
diff --git a/Directory.Build.targets b/Directory.Build.targets
index f7d374e..5c1ef69 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -36,8 +36,9 @@
BeforeTargets="Pack">
+
<_CheatEngineClientDirectProjectReference Include="@(ProjectReference)"
- Condition="'%(ProjectReference.DefiningProjectName)' == '$(MSBuildProjectName)'"/>
+ Condition="'%(ProjectReference.DefiningProjectName)' == '$(MSBuildProjectName)' and '%(ProjectReference.ReferenceOutputAssembly)' != 'false'"/>
<_CheatEngineClientSdkReference Include="@(PackageReference)"
Condition="'%(PackageReference.Identity)' == 'CheatEngine.SDK' and '%(PackageReference.DefiningProjectName)' == '$(MSBuildProjectName)'"/>
<_CheatEngineClientSdkRangeReference Include="@(_CheatEngineClientSdkReference)"
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 07c6748..ab4e128 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -17,12 +17,14 @@
+
-
+
+
diff --git a/README.md b/README.md
index 4a6b39e..b338b1f 100644
--- a/README.md
+++ b/README.md
@@ -220,9 +220,12 @@ following table is a delivery statement, not a substitute for a live host check.
| Value scanning | **Capability-gated** | The public state machine exists, but Client session creation stays unavailable until the internal `MemScan`/`FoundList` ownership path passes its Cheat Engine 7.7 x64 live gate |
| Arbitrary Lua source | Policy-gated and off by default | Requires explicit unsafe opt-in; raw Lua state remains hidden |
-IPC, remote clients, UI/forms, debugger and breakpoints, Auto Assembler, injection, remote allocations, structures,
-hotkeys/timers, speedhack, DBVM, Mono/IL2CPP, and advanced ABI hooks are outside v0.1. They have no placeholder
-public API. The full current-state rationale is in [ADR 0004](docs/adr/0004-capability-matrix.md).
+Target allocations; assembly and Auto Assembler; remote execution and DLL injection; debugger and breakpoints;
+hotkeys and timers; speed; hashing; and DBVM now have public Client API contracts. Each remains
+`Unknown` or `Unavailable` until its primitive, ownership, and lifecycle behavior passes the corresponding Cheat
+Engine 7.7 x64 live gate. IPC, remote clients, UI/forms, structures, Mono/IL2CPP, and advanced ABI hooks remain
+outside v0.1 and have no placeholder public API. The full current-state rationale is in
+[ADR 0004](docs/adr/0004-capability-matrix.md).
## AOT, trimming, and deployment
@@ -258,7 +261,9 @@ dotnet publish tests/CheatEngine.Client.AotProbe/CheatEngine.Client.AotProbe.csp
The [Windows CI workflow](.github/workflows/ci.yml) runs the locked restore, Release build, Microsoft Testing Platform
tests, package API validation, isolated package smoke test, template smoke test, and Native AOT graph probe. The
Cheat Engine 7.7 x64 live suite is opt-in and intentionally excluded from ordinary CI; no CI result should be read as
-proof that an untested live-host feature is available.
+proof that an untested live-host feature is available. The required success, failure, cleanup, disable, re-enable, and
+target-change evidence for every advanced capability is defined in the
+[live-capability gate protocol](docs/live-capability-gates.md).
## Security and scope
diff --git a/docs/live-capability-gates.md b/docs/live-capability-gates.md
new file mode 100644
index 0000000..dff1bc8
--- /dev/null
+++ b/docs/live-capability-gates.md
@@ -0,0 +1,146 @@
+# Cheat Engine 7.7 x64 live-capability gates
+
+## Purpose and promotion rule
+
+This document is the release gate for advanced CheatEngine.Client capabilities. It is deliberately stricter than a build, unit test, package smoke test, or Native AOT probe: every listed operation crosses a Cheat Engine-owned Lua, ABI, callback, or ownership boundary whose real lifecycle must be observed in the supported host.
+
+Every capability below starts as Unknown or Unavailable. A public interface, a completed implementation, an SDK binding, or an ordinary-CI result is not evidence that it may return Available. The only promotion rule is:
+
+> A capability may return Available only after its complete domain scenario below has passed in Cheat Engine 7.7 x64, the required evidence has been captured and reviewed, and the result has been linked from the capability matrix. A partial result, another CE version or architecture, or a run without cleanup, disable, re-enable, and target replacement leaves the capability Unknown or Unavailable.
+
+Run gates only against a local process the operator is authorised to control and that is disposable. Use a purpose-made test executable with known memory, code, file, and timing behaviour. Never use a production process, an online game, or a target that cannot be restored. The test must use an actual 64-bit Cheat Engine 7.7 host rather than an SDK fake or simulated Lua runtime.
+
+The supported artifact remains a framework-dependent managed net10.0 plugin folder. Client libraries are analysed for trimming and Native AOT compatibility, and the AOT probe validates that analysis. Neither analysis nor the probe means Cheat Engine can load a Native AOT plugin DLL. Exercise the managed plugin with its .deps.json, .runtimeconfig.json, Client and SDK assemblies, and native SDK bridge deployed together.
+
+## Common protocol and evidence
+
+Capture this bundle for every domain:
+
+1. **Host:** CE version/build, x64, Windows version, Client/SDK package or commit versions, plugin hash, timestamp.
+2. **Target:** fixture executable path/hash, PID, bitness, selection epoch, and confirmation it is local, authorised, and disposable. Do not log sensitive target-memory contents.
+3. **Operation:** request metadata, copied Client result, stable CheatEngineFailure on errors, and host-side evidence (log, UI screenshot, process/module or memory-map observation) of the intended effect.
+4. **Lifecycle:** enable, success, intentional failure, cleanup, disable, fresh re-enable, and target replacement; record Client epoch and capability observation at every boundary.
+5. **Negative checks:** rejected work makes no host mutation; stale leases/callbacks never act in a new epoch; no callback blocks a Cheat Engine or Lua thread.
+
+The raw log remains access controlled; the attached review summary is redacted. It names the exact host, SDK, and Client versions. A changed SDK ABI or CE version requires a new relevant gate run; older evidence is not inherited.
+
+| Required phase | Assertion |
+|---|---|
+| Initial state | The service is resolvable, returns Unknown/Unavailable, and makes no speculative mutation. |
+| Success | Only copied Client data or activation-scoped Client leases return; the host effect is independently observed. |
+| Expected failure | Invalid input, unavailable primitive, and host error become stable failures; no partial resource, callback, patch, or allocation survives. |
+| Cleanup | A lease disposes once in the documented order and the host returns to its recorded baseline. |
+| Disable | Admission closes before SDK/Lua teardown; Client resources are neutralised and released while the SDK context is valid. |
+| Re-enable | A new Client epoch has only new resources; old leases, callbacks, and subscriptions are rejected or inert. |
+| Target change | Old selection-bound resources are reclaimed/invalidated before a new PID is used and cannot act on it. |
+
+## Allocations — Allocations
+
+**Preconditions.** Select the disposable local target, record PID and selection epoch, and choose a positive bounded size with a documented protection mode. The allocation must not enter a cache or pool. Start with the Allocations capability observed as Unknown/Unavailable.
+
+**Success.** Allocate(TargetAllocationRequest) returns ITargetMemoryLease with a non-zero address, requested size, and current selection epoch. Verify the range belongs to the selected PID and, where supported, write/read one bounded fixture value. No SDK owner or raw handle may escape in its result.
+
+**Failure, cleanup, and lifecycle.** Exercise zero, oversized, unsupported, and injected SDK allocation failures; each must yield CapabilityUnavailable or a classified failure without a remote range leak. Dispose a successful lease twice and prove deallocation occurs at most once. Selection change and disable release the old range before detachment; an old lease cannot touch the replacement target. Re-enable then allocates a fresh range in a new epoch.
+
+**Evidence.** Capture address, size, PID, old/new epochs, allocation/deallocation statuses, memory-map evidence, and no-leak proof after failure, disable, and target replacement.
+
+## Assembly and Auto Assembler — Assembly
+
+**Preconditions.** Use a known instruction sequence in the fixture. Capture original bounded byte and disassembly snapshots. A test Auto Assembler script is reversible, has a valid [DISABLE] path, contains neither targetSelf nor host pointers, and touches only the fixture target. Start Unknown/Unavailable.
+
+**Success.** Verify copied disassembly, instruction size, previous-instruction lookup, comments, and a one-instruction assembly result. Apply the reversible script and obtain IAutoAssemblerPatchLease; independently observe its one intended effect. Dispose it and prove the SDK executes retained disableInfo once and restores original bytes.
+
+**Failure, cleanup, and lifecycle.** Test malformed assembly, invalid address, and a script failing part way through. Each returns a classified error and leaves original bytes intact; a partially-created patch rolls back. Double disposal cannot execute [DISABLE] twice. Disable and target change restore a live patch before Lua detaches; an old lease cannot disable a patch from a new epoch or PID. Re-enable uses a newly created lease only.
+
+**Evidence.** Capture script hash, original/patched/restored byte hashes, copied instruction values, disableInfo status, PID/epochs, failures, and host disassembly evidence.
+
+## Remote execution and DLL injection — RemoteExecution
+
+**Preconditions.** Select the authorised disposable x64 target. Supply a trusted target-compatible test DLL using an existing absolute path. Calls use a strictly positive finite timeout; parameters flow through a Client-owned allocation lease, never a caller-supplied host pointer. Begin Unknown/Unavailable.
+
+**Success.** Demonstrate deterministic injection and a harmless exported fixture action with a bounded copied result. Verify parameter allocation belongs to the PID, the action honours its timeout, and allocation is released on both normal completion and remote error. A public result retains no remote pointer.
+
+**Failure, cleanup, and lifecycle.** Reject relative, missing, inaccessible, and wrong-bitness DLL paths; zero or negative timeout; bad parameters; remote exception; and timeout. None may leak an allocation, pending callback, or unrelated mutation. If CE cannot safely unload an injected DLL, state that limitation and use a new fixture process for remaining phases instead of claiming unload semantics. Disable while a call is pending, re-enable, and prove old state cannot complete into the new epoch. On target change, release old allocations/call state before new PID use.
+
+**Evidence.** Capture canonical DLL path/hash, PID/epochs, timeout, allocation lifecycle, exit/result status, host logs, and process-module evidence.
+
+## Debugger and breakpoints — Debugger
+
+**Preconditions.** Use a debugger-safe fixture with a known breakpoint address and controlled trigger. An IBreakpointLease callback receives a copied event and returns BreakpointDisposition synchronously; it never awaits an async consumer or blocks Lua/CE. Any event stream has positive capacity and named overflow policy. Begin Unknown/Unavailable.
+
+**Success.** Trigger once and verify copied address/register/context values and immediate disposition against fixture behaviour. Saturate an optional stream to prove its selected overflow policy and observable drop count without blocking the producer.
+
+**Failure, cleanup, and lifecycle.** Invalid/unmapped registration and a handler exception leave no armed breakpoint or uncaught host exception. Dispose, trigger again, and prove no admission. On disable: close admission, neutralise the SDK callback, complete streams, release the lease, then detach Lua. Re-enable has a new scope; a retained old closure/subscription is inert. Changing target before trigger never delivers old breakpoint work to the new PID.
+
+**Evidence.** Capture breakpoint IDs, event sequence, disposition, stream capacity/drop count, callback-thread proof, PID/epochs, and removal proof.
+
+## Hotkeys — Hotkeys
+
+**Preconditions.** Choose a non-conflicting reversible fixture chord in a controlled environment. A handler receives only copied events. An asynchronous projection always supplies positive-capacity EventStreamOptions and an explicit overflow policy (DropOldest default, DropNewest, or FailSubscription). Begin Unknown/Unavailable.
+
+**Success.** Register IHotkeyLease, trigger the chord through the host, and prove exactly one copied event reaches the active epoch. Deliberately fill a stream and record loss behaviour without blocking the Lua/registration thread.
+
+**Failure, cleanup, and lifecycle.** A duplicate/conflicting chord and handler failure leave no partial registration or host exception. Disposal prevents the chord reaching the plugin. Disable closes admission, neutralises callbacks, completes subscribers, releases host hotkey, then permits Lua detach. A fresh re-enable lease works while the former delegate/stream remains inert. Changing target must not retain old selection-bound state or direct work to it.
+
+**Evidence.** Capture chord, registration/removal statuses, event/loss counts, epochs, and host UI/log proof.
+
+## Timers — Timers
+
+**Preconditions.** Use a positive bounded interval and callbacks that record copied timestamps only. Every async consumer uses positive-capacity EventStreamOptions; a timer callback never waits for a consumer or calls a blocking dispatcher. Begin Unknown/Unavailable.
+
+**Success.** Acquire ITimerLease, observe a bounded number of ticks within documented tolerance, and prove timer and stream are activation-scoped. Fill the stream to verify overflow/drop accounting and a non-blocked CE/Lua thread.
+
+**Failure, cleanup, and lifecycle.** Zero, negative, unsupported interval and injected host creation failure leave no timer. After disposal, wait more than two intervals and prove no admission. Disable closes admission, neutralises callback, completes stream, and frees the timer before Lua teardown. Re-enable creates a new timer/scope; an old lease cannot stop or receive it. A target replacement must not cause a tick to use an old selection epoch.
+
+**Evidence.** Capture requested/observed intervals, tick/event/drop counts, timer IDs, callback-thread proof, PID/epochs, and final stop evidence.
+
+## Speed — Speed
+
+**Preconditions.** Record the host baseline speed and use a disposable local fixture. Inputs are finite and strictly positive. Speed is host-wide rather than a target allocation lease, so the gate explicitly restores baseline; Client must not silently initialise or reset it. Begin Unknown/Unavailable.
+
+**Success.** Read a copied baseline, set a known finite positive multiplier, read it back, and verify the fixture's timing change within a documented tolerance. Restore the exact recorded baseline as the test cleanup action.
+
+**Failure, cleanup, and lifecycle.** Reject zero, negative, NaN, and infinities before host mutation. A host error leaves the last valid value intact. At disable, re-enable, and target change, observe/record state without assuming per-process ownership, then restore the pre-test setting before finishing.
+
+**Evidence.** Capture baseline/request/read-back values, timing samples/tolerance, validation/host failures, host setting evidence, PID/epochs, and baseline-restoration proof.
+
+## Hashing — Hashing
+
+**Preconditions.** Test target-memory hashing and file hashing separately. For memory, select the local fixture and a bounded readable known-byte range. For files, use an authorised local fixture with a known digest. Begin Unknown/Unavailable.
+
+**Success.** Hash each input independently and compare copied digests with external known values. Define any same-content equivalence only under the documented algorithm/encoding. Neither operation mutates target memory or a file.
+
+**Failure, cleanup, and lifecycle.** Exercise unreadable/out-of-range memory, missing/unauthorised file, invalid range/algorithm, and selection change during memory hashing. Each returns a stable failure without a borrowed buffer, open file handle, or hashing the replacement PID. The service has no host lease, but disable/re-enable safely stop admission and a prior-epoch result is never reported as current.
+
+**Evidence.** Capture algorithm, bounded range metadata or canonical fixture path/hash, expected/actual digest, errors, resource checks, PID/epochs, and redacted no-content-logging proof.
+
+## DBVM observation, initialisation, and watches — Dbvm
+
+**Preconditions.** Use only a dedicated, authorised VM test environment whose hardware/OS support is understood by the operator. Observe DBVM state first: Client never initialises DBVM implicitly. Explicit initialisation requires a separate operator-approved rollback/VM-reset plan. Begin Unknown/Unavailable.
+
+**Success.** Prove state observation is side-effect free. Only when explicit initialisation is authorised, record pre/post state and install IDbvmWatchLease on a harmless fixture range. Verify copied events, bounded stream behaviour, and immediate non-blocking callback handling.
+
+**Failure, cleanup, and lifecycle.** Unsupported hardware, denied init, invalid range, and callback failure may not initialise DBVM or leave a watch active. Dispose watch and prove removal. If no safe in-session DBVM shutdown exists, document it, use the approved VM reset, and do not claim a Client deinitialisation guarantee. Disable closes admission, neutralises callbacks, completes streams, and removes watches before Lua detach. Re-enable creates fresh watches; selection change removes old watches before a new PID.
+
+**Evidence.** Capture observation/explicit-init decision, hardware/VM facts, watch IDs/ranges, stream/drop counts, lifecycle logs, reset proof where needed, and PID/epochs.
+
+## Value scanning — ValueScanning
+
+**Preconditions.** Select a fixture exposing known mutable values in a bounded documented region. SDK must have a production factory that creates and owns MemScan plus FoundList; Client must not hand-construct or adopt an unproven owner. Begin Unknown/Unavailable even if the public state machine exists.
+
+**Success.** Create one session, perform first scan, mutate the fixture, perform next scan, and read bounded copied result pages. Prove factory construction order is parent scan then child found-list. Confirm Client metadata exposes no LuaState, LuaRef, CEObject, Owned, borrowed Lua value, or raw pointer.
+
+**Failure, cleanup, and lifecycle.** Inject child creation failure after parent creation and prove parent rollback. Exercise invalid requests, cancellation before admission, empty/no-result scan, and failure during next scan. Dispose and prove child is destroyed before parent; disposal is idempotent and no owner remains after disable or selection change. Re-enable creates a new session; retained old page/session cannot scan or read new epoch data.
+
+**Evidence.** Capture request/result counts without sensitive values, parent/child create-destroy order, page bounds, cancellation/failure statuses, PID/epochs, CE scan UI/log evidence, and leak checks.
+
+## Recording the decision
+
+Attach the evidence bundle to the release issue or capability matrix, with exact host/SDK/Client versions and a pass/fail result for every phase. The review must affirm all four points:
+
+1. The managed plugin artifact ran in Cheat Engine 7.7 x64.
+2. Success, expected failure, cleanup, disable, re-enable, and target replacement passed.
+3. Ownership, epochs, callback admission, and Lua detachment were observed rather than inferred.
+4. The high-level boundary holds: normal Client code sees copied data and leases only, never SDK/Lua handles or raw pointers.
+
+Only four affirmative answers and the domain-specific evidence permit Available. Otherwise keep Unknown/Unavailable, retain the published contract if any, and exclude the feature from unguarded examples.
diff --git a/eng/Invoke-PackageSmoke.ps1 b/eng/Invoke-PackageSmoke.ps1
index 75a2d55..559bd1b 100644
--- a/eng/Invoke-PackageSmoke.ps1
+++ b/eng/Invoke-PackageSmoke.ps1
@@ -17,6 +17,11 @@ if (-not (Test-Path -LiteralPath $expectedPackage -PathType Leaf)) {
throw "Expected package '$expectedPackage' was not found. Run dotnet pack before the smoke test."
}
+$expectedHostingPackage = Join-Path $resolvedPackageSource "CheatEngine.Client.Hosting.$ClientVersion.nupkg"
+if (-not (Test-Path -LiteralPath $expectedHostingPackage -PathType Leaf)) {
+ throw "Expected package '$expectedHostingPackage' was not found. Run dotnet pack before the smoke test."
+}
+
$temporaryBase = [IO.Path]::GetFullPath([IO.Path]::GetTempPath())
$smokeDirectory = [IO.Path]::GetFullPath((Join-Path $temporaryBase ("CheatEngine.Client.PackageSmoke." + [Guid]::NewGuid().ToString('N'))))
if (-not $smokeDirectory.StartsWith($temporaryBase, [StringComparison]::OrdinalIgnoreCase)) {
@@ -26,9 +31,23 @@ if (-not $smokeDirectory.StartsWith($temporaryBase, [StringComparison]::OrdinalI
function Write-SmokeProject {
param(
[Parameter(Mandatory)] [string]$ProjectDirectory,
- [Parameter(Mandatory)] [bool]$IncludeSdkReference
+ [Parameter(Mandatory)] [bool]$IncludeClientReference,
+ [Parameter(Mandatory)] [bool]$IncludeSdkReference,
+ [bool]$IncludeHostingReference = $false,
+ [ValidateRange(0, 2)] [int]$PluginCount = 1,
+ [string]$LanguageVersion = '14.0',
+ [string]$PlatformTarget = 'x64',
+ [bool]$GenerateEntryPoint = $true,
+ [bool]$ManualBootstrap = $false
)
+ $clientReference = if ($IncludeClientReference) {
+ ' '
+ }
+ else {
+ ''
+ }
+
$sdkReference = if ($IncludeSdkReference) {
' '
}
@@ -36,36 +55,41 @@ function Write-SmokeProject {
''
}
+ $hostingReference = if ($IncludeHostingReference) {
+ ' '
+ }
+ else {
+ ''
+ }
+
$projectXml = @"
net10.0
- 14.0
+ $LanguageVersion
enable
enable
+ $PlatformTarget
true
+ $GenerateEntryPoint
+ $ManualBootstrap
false
true
obj/Generated
-
+$clientReference
+$hostingReference
$sdkReference
"@
Set-Content -LiteralPath (Join-Path $ProjectDirectory 'Smoke.Plugin.csproj') -Value $projectXml -Encoding utf8NoBOM
- $pluginSource = @'
-using CheatEngine.Client;
-using CheatEngine.Client.Hosting;
-using CheatEngine.Client.Memory;
-using CheatEngine.Client.Scanning;
-using CheatEngine.SDK.Annotations.Plugin;
-using CheatEngine.SDK.Engine.Values;
-
-[CheatEnginePlugin("Package smoke plugin")]
-public sealed class Plugin : CheatEngineClientPlugin
+ $pluginClasses = for ($index = 1; $index -le $PluginCount; $index++) {
+ @"
+[CheatEnginePlugin("Package smoke plugin $index")]
+public sealed class Plugin$index : CheatEngineClientPlugin
{
protected override void Configure(CheatEnginePluginBuilder builder)
{
@@ -77,13 +101,101 @@ public sealed class Plugin : CheatEngineClientPlugin
_ = client.Patterns.Aob("00").FirstOrNone();
}
}
+"@
+ }
+
+ if ($PluginCount -eq 0) {
+ $pluginClasses = @'
+public sealed class NotAPlugin;
'@
+ }
+
+ $manualBootstrapSource = if ($ManualBootstrap) {
+ @'
+namespace CESDK
+{
+ public static class CESDK
+ {
+ public static int CEPluginInitialize(IntPtr initialization, int version)
+ {
+ return 1;
+ }
+ }
+}
+'@
+ }
+ else {
+ ''
+ }
+
+ $pluginSource = @"
+using System;
+using CheatEngine.Client;
+using CheatEngine.Client.Hosting;
+using CheatEngine.Client.Memory;
+using CheatEngine.Client.Scanning;
+using CheatEngine.SDK.Annotations.Plugin;
+using CheatEngine.SDK.Engine.Values;
+
+$pluginClasses
+$manualBootstrapSource
+"@
Set-Content -LiteralPath (Join-Path $ProjectDirectory 'Plugin.cs') -Value $pluginSource -Encoding utf8NoBOM
}
+function Assert-ExpectedBuildFailure {
+ param(
+ [Parameter(Mandatory)] [string]$ProjectPath,
+ [Parameter(Mandatory)] [string]$ExpectedDiagnostic,
+ [string[]]$AdditionalArguments = @()
+ )
+
+ $output = & dotnet build $ProjectPath --configuration Release --no-restore @AdditionalArguments 2>&1 | Out-String
+ if ($LASTEXITCODE -eq 0) {
+ throw "The negative isolated plugin build unexpectedly succeeded; expected $ExpectedDiagnostic."
+ }
+ if ($output -notmatch $ExpectedDiagnostic) {
+ throw "The negative isolated plugin build failed, but did not report $ExpectedDiagnostic.`n$output"
+ }
+
+ # A diagnosed negative build is expected. Clear the native command status only after asserting its exact diagnostic.
+ $global:LASTEXITCODE = 0
+}
+
+function Assert-PackageEntries {
+ param(
+ [Parameter(Mandatory)] [string]$PackagePath,
+ [Parameter(Mandatory)] [string[]]$ExpectedEntries
+ )
+
+ Add-Type -AssemblyName System.IO.Compression.FileSystem
+ $archive = [IO.Compression.ZipFile]::OpenRead($PackagePath)
+ try {
+ $entries = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
+ foreach ($entry in $archive.Entries) {
+ [void]$entries.Add($entry.FullName)
+ }
+
+ foreach ($expectedEntry in $ExpectedEntries) {
+ if (-not $entries.Contains($expectedEntry)) {
+ throw "Package '$PackagePath' is missing '$expectedEntry'."
+ }
+ }
+ }
+ finally {
+ $archive.Dispose()
+ }
+}
+
try {
New-Item -ItemType Directory -Path $smokeDirectory | Out-Null
+ Assert-PackageEntries -PackagePath $expectedHostingPackage -ExpectedEntries @(
+ 'analyzers/dotnet/cs/CheatEngine.Client.SourceGenerators.Lua.dll',
+ 'buildTransitive/CheatEngine.Client.Hosting.props',
+ 'buildTransitive/CheatEngine.Client.Hosting.targets'
+ )
+
$configurationPath = Join-Path $smokeDirectory 'NuGet.Config'
$escapedSource = [Security.SecurityElement]::Escape($resolvedPackageSource)
$escapedPackageCache = [Security.SecurityElement]::Escape((Join-Path $smokeDirectory '.packages'))
@@ -104,20 +216,30 @@ try {
$positiveDirectory = Join-Path $smokeDirectory 'positive'
New-Item -ItemType Directory -Path $positiveDirectory | Out-Null
- Write-SmokeProject -ProjectDirectory $positiveDirectory -IncludeSdkReference $true
+ Write-SmokeProject -ProjectDirectory $positiveDirectory -IncludeClientReference $true -IncludeSdkReference $true
& dotnet restore (Join-Path $positiveDirectory 'Smoke.Plugin.csproj') --configfile $configurationPath
if ($LASTEXITCODE -ne 0) {
throw 'The positive isolated package restore failed.'
}
- & dotnet build (Join-Path $positiveDirectory 'Smoke.Plugin.csproj') --configuration Release --no-restore
+ $deploymentDirectory = Join-Path $smokeDirectory 'deployment'
+ & dotnet build (Join-Path $positiveDirectory 'Smoke.Plugin.csproj') --configuration Release --no-restore `
+ "-p:CheatEnginePluginOutputPath=$deploymentDirectory"
if ($LASTEXITCODE -ne 0) {
throw 'The positive isolated package build failed.'
}
+ # Reuse the destination so the deployment task must replace every staged file, not only create a fresh layout.
+ & dotnet build (Join-Path $positiveDirectory 'Smoke.Plugin.csproj') --configuration Release --no-restore `
+ "-p:CheatEnginePluginOutputPath=$deploymentDirectory"
+ if ($LASTEXITCODE -ne 0) {
+ throw 'The managed deployment could not atomically replace an existing plugin layout.'
+ }
+
$positiveOutput = Join-Path $positiveDirectory 'bin/Release/net10.0'
$requiredOutputFiles = @(
'Smoke.Plugin.dll',
+ 'Smoke.Plugin.deps.json',
'Smoke.Plugin.runtimeconfig.json',
'cheatengine-sdk-lua-bridge.dll',
'CheatEngine.SDK.dll',
@@ -134,6 +256,13 @@ try {
}
}
+ foreach ($requiredDeploymentFile in $requiredOutputFiles) {
+ $requiredDeploymentPath = Join-Path $deploymentDirectory $requiredDeploymentFile
+ if (-not (Test-Path -LiteralPath $requiredDeploymentPath -PathType Leaf)) {
+ throw "The prepared deployment is missing '$requiredDeploymentFile'."
+ }
+ }
+
$generatedEntryPoints = @(Get-ChildItem -LiteralPath (Join-Path $positiveDirectory 'obj') -Recurse -File |
Where-Object Name -eq 'CheatEngine.SDK.EntryPoint.g.cs')
if ($generatedEntryPoints.Count -ne 1) {
@@ -148,24 +277,107 @@ try {
$negativeDirectory = Join-Path $smokeDirectory 'negative'
New-Item -ItemType Directory -Path $negativeDirectory | Out-Null
- Write-SmokeProject -ProjectDirectory $negativeDirectory -IncludeSdkReference $false
+ Write-SmokeProject -ProjectDirectory $negativeDirectory -IncludeClientReference $true -IncludeSdkReference $false
& dotnet restore (Join-Path $negativeDirectory 'Smoke.Plugin.csproj') --configfile $configurationPath
if ($LASTEXITCODE -ne 0) {
throw 'The negative isolated package restore failed before CECLIENT001 could be evaluated.'
}
- $negativeOutput = & dotnet build (Join-Path $negativeDirectory 'Smoke.Plugin.csproj') --configuration Release --no-restore 2>&1 | Out-String
- if ($LASTEXITCODE -eq 0) {
- throw 'The negative isolated plugin build unexpectedly succeeded without a direct CheatEngine.SDK reference.'
+ Assert-ExpectedBuildFailure -ProjectPath (Join-Path $negativeDirectory 'Smoke.Plugin.csproj') -ExpectedDiagnostic 'CECLIENT001'
+
+ $missingClientDirectory = Join-Path $smokeDirectory 'missing-client'
+ New-Item -ItemType Directory -Path $missingClientDirectory | Out-Null
+ Write-SmokeProject -ProjectDirectory $missingClientDirectory -IncludeClientReference $false -IncludeSdkReference $true `
+ -IncludeHostingReference $true
+ & dotnet restore (Join-Path $missingClientDirectory 'Smoke.Plugin.csproj') --configfile $configurationPath
+ if ($LASTEXITCODE -ne 0) {
+ throw 'The missing-Client isolated package restore failed before CECLIENT002 could be evaluated.'
}
- if ($negativeOutput -notmatch 'CECLIENT001') {
- throw "The negative isolated plugin build failed, but did not report CECLIENT001.`n$negativeOutput"
+ Assert-ExpectedBuildFailure -ProjectPath (Join-Path $missingClientDirectory 'Smoke.Plugin.csproj') -ExpectedDiagnostic 'CECLIENT002'
+
+ $zeroPluginDirectory = Join-Path $smokeDirectory 'zero-plugin'
+ New-Item -ItemType Directory -Path $zeroPluginDirectory | Out-Null
+ Write-SmokeProject -ProjectDirectory $zeroPluginDirectory -IncludeClientReference $true -IncludeSdkReference $true -PluginCount 0
+ & dotnet restore (Join-Path $zeroPluginDirectory 'Smoke.Plugin.csproj') --configfile $configurationPath
+ if ($LASTEXITCODE -ne 0) {
+ throw 'The zero-plugin isolated package restore failed before CECLIENT003 could be evaluated.'
}
+ Assert-ExpectedBuildFailure -ProjectPath (Join-Path $zeroPluginDirectory 'Smoke.Plugin.csproj') -ExpectedDiagnostic 'CECLIENT003'
- # The expected negative build leaves PowerShell's native-command status non-zero. Clear it only after both
- # assertions prove that the failure was the intended CECLIENT001 guard.
- $global:LASTEXITCODE = 0
- Write-Host 'Package smoke test passed: direct SDK reference accepted and CECLIENT001 enforced for marked plugin projects.'
+ $multiplePluginDirectory = Join-Path $smokeDirectory 'multiple-plugin'
+ New-Item -ItemType Directory -Path $multiplePluginDirectory | Out-Null
+ Write-SmokeProject -ProjectDirectory $multiplePluginDirectory -IncludeClientReference $true -IncludeSdkReference $true `
+ -PluginCount 2 -GenerateEntryPoint $false -ManualBootstrap $true
+ & dotnet restore (Join-Path $multiplePluginDirectory 'Smoke.Plugin.csproj') --configfile $configurationPath
+ if ($LASTEXITCODE -ne 0) {
+ throw 'The multiple-plugin isolated package restore failed before CECLIENT004 could be evaluated.'
+ }
+ Assert-ExpectedBuildFailure -ProjectPath (Join-Path $multiplePluginDirectory 'Smoke.Plugin.csproj') -ExpectedDiagnostic 'CECLIENT004'
+
+ $manualBootstrapDirectory = Join-Path $smokeDirectory 'manual-bootstrap'
+ New-Item -ItemType Directory -Path $manualBootstrapDirectory | Out-Null
+ Write-SmokeProject -ProjectDirectory $manualBootstrapDirectory -IncludeClientReference $true -IncludeSdkReference $true `
+ -GenerateEntryPoint $false
+ & dotnet restore (Join-Path $manualBootstrapDirectory 'Smoke.Plugin.csproj') --configfile $configurationPath
+ if ($LASTEXITCODE -ne 0) {
+ throw 'The manual-bootstrap isolated package restore failed before CECLIENT008 could be evaluated.'
+ }
+ Assert-ExpectedBuildFailure -ProjectPath (Join-Path $manualBootstrapDirectory 'Smoke.Plugin.csproj') -ExpectedDiagnostic 'CECLIENT008'
+
+ $languageDirectory = Join-Path $smokeDirectory 'language'
+ New-Item -ItemType Directory -Path $languageDirectory | Out-Null
+ Write-SmokeProject -ProjectDirectory $languageDirectory -IncludeClientReference $true -IncludeSdkReference $true -LanguageVersion '13.0'
+ & dotnet restore (Join-Path $languageDirectory 'Smoke.Plugin.csproj') --configfile $configurationPath
+ if ($LASTEXITCODE -ne 0) {
+ throw 'The language-version isolated package restore failed before CECLIENT006 could be evaluated.'
+ }
+ Assert-ExpectedBuildFailure -ProjectPath (Join-Path $languageDirectory 'Smoke.Plugin.csproj') -ExpectedDiagnostic 'CECLIENT006'
+
+ $platformDirectory = Join-Path $smokeDirectory 'platform'
+ New-Item -ItemType Directory -Path $platformDirectory | Out-Null
+ Write-SmokeProject -ProjectDirectory $platformDirectory -IncludeClientReference $true -IncludeSdkReference $true -PlatformTarget 'x86'
+ & dotnet restore (Join-Path $platformDirectory 'Smoke.Plugin.csproj') --configfile $configurationPath
+ if ($LASTEXITCODE -ne 0) {
+ throw 'The platform isolated package restore failed before CECLIENT007 could be evaluated.'
+ }
+ Assert-ExpectedBuildFailure -ProjectPath (Join-Path $platformDirectory 'Smoke.Plugin.csproj') -ExpectedDiagnostic 'CECLIENT007'
+
+ $frameworkDirectory = Join-Path $smokeDirectory 'framework'
+ New-Item -ItemType Directory -Path $frameworkDirectory | Out-Null
+ Write-SmokeProject -ProjectDirectory $frameworkDirectory -IncludeClientReference $true -IncludeSdkReference $true
+ & dotnet restore (Join-Path $frameworkDirectory 'Smoke.Plugin.csproj') --configfile $configurationPath
+ if ($LASTEXITCODE -ne 0) {
+ throw 'The framework isolated package restore failed before CECLIENT005 could be evaluated.'
+ }
+ Assert-ExpectedBuildFailure -ProjectPath (Join-Path $frameworkDirectory 'Smoke.Plugin.csproj') -ExpectedDiagnostic 'CECLIENT005' `
+ -AdditionalArguments @('-p:TargetFramework=net9.0')
+
+ $anyCpuDirectory = Join-Path $smokeDirectory 'anycpu'
+ New-Item -ItemType Directory -Path $anyCpuDirectory | Out-Null
+ Write-SmokeProject -ProjectDirectory $anyCpuDirectory -IncludeClientReference $true -IncludeSdkReference $true -PlatformTarget 'AnyCPU'
+ & dotnet restore (Join-Path $anyCpuDirectory 'Smoke.Plugin.csproj') --configfile $configurationPath
+ if ($LASTEXITCODE -ne 0) {
+ throw 'The AnyCPU isolated package restore failed.'
+ }
+ & dotnet build (Join-Path $anyCpuDirectory 'Smoke.Plugin.csproj') --configuration Release --no-restore
+ if ($LASTEXITCODE -ne 0) {
+ throw 'The AnyCPU isolated plugin build failed.'
+ }
+
+ $manualBootstrapSuccessDirectory = Join-Path $smokeDirectory 'manual-bootstrap-success'
+ New-Item -ItemType Directory -Path $manualBootstrapSuccessDirectory | Out-Null
+ Write-SmokeProject -ProjectDirectory $manualBootstrapSuccessDirectory -IncludeClientReference $true -IncludeSdkReference $true `
+ -GenerateEntryPoint $false -ManualBootstrap $true
+ & dotnet restore (Join-Path $manualBootstrapSuccessDirectory 'Smoke.Plugin.csproj') --configfile $configurationPath
+ if ($LASTEXITCODE -ne 0) {
+ throw 'The manual-bootstrap-success isolated package restore failed.'
+ }
+ & dotnet build (Join-Path $manualBootstrapSuccessDirectory 'Smoke.Plugin.csproj') --configuration Release --no-restore
+ if ($LASTEXITCODE -ne 0) {
+ throw 'The explicit manual bootstrap isolated plugin build failed.'
+ }
+
+ Write-Host 'Package smoke test passed: direct package references, Client plugin profile diagnostics, generated SDK bootstrap, and managed deployment layout are verified.'
}
finally {
if (Test-Path -LiteralPath $smokeDirectory -PathType Container) {
diff --git a/eng/Tests.props b/eng/Tests.props
index b66e29e..f20f859 100644
--- a/eng/Tests.props
+++ b/eng/Tests.props
@@ -14,7 +14,6 @@
-
diff --git a/libs/CheatEngine.Client.Abstractions/Allocations/IAllocationClient.cs b/libs/CheatEngine.Client.Abstractions/Allocations/IAllocationClient.cs
new file mode 100644
index 0000000..3282469
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Allocations/IAllocationClient.cs
@@ -0,0 +1,21 @@
+using System.Diagnostics.CodeAnalysis;
+
+using CheatEngine.Client.Results;
+
+namespace CheatEngine.Client.Allocations;
+
+/// Creates explicitly owned target-memory allocations.
+public interface IAllocationClient
+{
+ /// Tries to allocate bounded target memory for the current selection.
+ public bool TryAllocate(
+ TargetAllocationRequest request,
+ [NotNullWhen(true)] out ITargetMemoryLease? lease,
+ out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Allocates bounded target memory or throws when the host rejects the request.
+ public ITargetMemoryLease Allocate(
+ TargetAllocationRequest request,
+ CancellationToken cancellationToken = default);
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Allocations/ITargetMemoryLease.cs b/libs/CheatEngine.Client.Abstractions/Allocations/ITargetMemoryLease.cs
new file mode 100644
index 0000000..82cd6eb
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Allocations/ITargetMemoryLease.cs
@@ -0,0 +1,35 @@
+using CheatEngine.SDK.Engine.Values;
+
+namespace CheatEngine.Client.Allocations;
+
+/// Owns one Client-created allocation in the selected target process.
+///
+/// The activation owner releases forgotten leases before disable. A selection-epoch change also invalidates and
+/// releases an allocation; implementations must not cache or pool target allocations.
+///
+public interface ITargetMemoryLease : IDisposable
+{
+ /// Gets the allocated target address.
+ public Address Address
+ {
+ get;
+ }
+
+ /// Gets the allocated byte count.
+ public long Size
+ {
+ get;
+ }
+
+ /// Gets the target-selection epoch captured when this allocation was created.
+ public long SelectionEpoch
+ {
+ get;
+ }
+
+ /// Gets whether the allocation has been released or invalidated.
+ public bool IsReleased
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Allocations/TargetAllocationAccess.cs b/libs/CheatEngine.Client.Abstractions/Allocations/TargetAllocationAccess.cs
new file mode 100644
index 0000000..71018c4
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Allocations/TargetAllocationAccess.cs
@@ -0,0 +1,11 @@
+namespace CheatEngine.Client.Allocations;
+
+/// Describes the access required by a Client-owned target-memory allocation.
+public enum TargetAllocationAccess
+{
+ /// Creates memory for copied parameter and data exchange.
+ ReadWrite,
+
+ /// Creates executable memory when a capability-gated operation has proven that requirement.
+ ExecuteReadWrite
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Allocations/TargetAllocationRequest.cs b/libs/CheatEngine.Client.Abstractions/Allocations/TargetAllocationRequest.cs
new file mode 100644
index 0000000..5f8ca12
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Allocations/TargetAllocationRequest.cs
@@ -0,0 +1,32 @@
+namespace CheatEngine.Client.Allocations;
+
+/// Describes one bounded allocation requested in the currently selected target.
+public readonly record struct TargetAllocationRequest
+{
+ /// Creates a target allocation request.
+ /// is not positive.
+ /// is not a defined value.
+ public TargetAllocationRequest(long size, TargetAllocationAccess access = TargetAllocationAccess.ReadWrite)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(size);
+ if (!Enum.IsDefined(access))
+ {
+ throw new ArgumentOutOfRangeException(nameof(access));
+ }
+
+ Size = size;
+ Access = access;
+ }
+
+ /// Gets the exact positive byte count requested from the target process.
+ public long Size
+ {
+ get;
+ }
+
+ /// Gets the requested target-memory access.
+ public TargetAllocationAccess Access
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Assembly/AssemblyInstructionRequest.cs b/libs/CheatEngine.Client.Abstractions/Assembly/AssemblyInstructionRequest.cs
new file mode 100644
index 0000000..69d430e
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Assembly/AssemblyInstructionRequest.cs
@@ -0,0 +1,28 @@
+using CheatEngine.SDK.Engine.Values;
+
+namespace CheatEngine.Client.Assembly;
+
+/// Describes a single instruction to assemble at one target address.
+public readonly record struct AssemblyInstructionRequest
+{
+ /// Creates an instruction-assembly request.
+ /// is blank.
+ public AssemblyInstructionRequest(Address address, string instruction)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(instruction);
+ Address = address;
+ Instruction = instruction;
+ }
+
+ /// Gets the address used as the instruction's assembly origin.
+ public Address Address
+ {
+ get;
+ }
+
+ /// Gets the assembly source for exactly one instruction.
+ public string Instruction
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Assembly/AssemblyInstructionSnapshot.cs b/libs/CheatEngine.Client.Abstractions/Assembly/AssemblyInstructionSnapshot.cs
new file mode 100644
index 0000000..09c3a97
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Assembly/AssemblyInstructionSnapshot.cs
@@ -0,0 +1,56 @@
+using System.Collections.Immutable;
+
+using CheatEngine.SDK.Engine.Values;
+
+namespace CheatEngine.Client.Assembly;
+
+/// A copied disassembly instruction and its exact target byte representation.
+public readonly record struct AssemblyInstructionSnapshot
+{
+ /// Creates a copied assembly-instruction snapshot.
+ /// is not positive.
+ /// is blank or is empty.
+ public AssemblyInstructionSnapshot(Address address, int length, string text, ReadOnlySpan bytes)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(length);
+ ArgumentException.ThrowIfNullOrWhiteSpace(text);
+ if (bytes.IsEmpty)
+ {
+ throw new ArgumentException("An instruction snapshot requires at least one byte.", nameof(bytes));
+ }
+
+ if (bytes.Length != length)
+ {
+ throw new ArgumentException("The byte count must match the instruction length.", nameof(bytes));
+ }
+
+ Address = address;
+ Length = length;
+ Text = text;
+ Bytes = ImmutableArray.Create(bytes);
+ }
+
+ /// Gets the address of the instruction.
+ public Address Address
+ {
+ get;
+ }
+
+ /// Gets the exact positive instruction length.
+ public int Length
+ {
+ get;
+ }
+
+ /// Gets the copied disassembly text.
+ public string Text
+ {
+ get;
+ }
+
+ /// Gets the immutable copy of the target instruction bytes.
+ public ImmutableArray Bytes
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Assembly/AutoAssemblerScript.cs b/libs/CheatEngine.Client.Abstractions/Assembly/AutoAssemblerScript.cs
new file mode 100644
index 0000000..f3136cd
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Assembly/AutoAssemblerScript.cs
@@ -0,0 +1,31 @@
+namespace CheatEngine.Client.Assembly;
+
+/// Describes an Auto Assembler script supplied to the capability-gated high-level API.
+public readonly record struct AutoAssemblerScript
+{
+ /// Creates an Auto Assembler script request.
+ /// is blank or is blank.
+ public AutoAssemblerScript(string source, string? name = null)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(source);
+ if (name is not null)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(name);
+ }
+
+ Source = source;
+ Name = name;
+ }
+
+ /// Gets the Auto Assembler source.
+ public string Source
+ {
+ get;
+ }
+
+ /// Gets the optional Client diagnostic name for the patch.
+ public string? Name
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Assembly/IAssemblyClient.cs b/libs/CheatEngine.Client.Abstractions/Assembly/IAssemblyClient.cs
new file mode 100644
index 0000000..08e1fde
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Assembly/IAssemblyClient.cs
@@ -0,0 +1,55 @@
+using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
+
+using CheatEngine.Client.Results;
+using CheatEngine.SDK.Engine.Values;
+
+namespace CheatEngine.Client.Assembly;
+
+/// Disassembles, assembles, comments, and applies Client-owned Auto Assembler patches.
+public interface IAssemblyClient
+{
+ /// Tries to copy one instruction from the target.
+ public bool TryDisassemble(Address address, out AssemblyInstructionSnapshot instruction,
+ out CheatEngineFailure failure, CancellationToken cancellationToken = default);
+
+ /// Copies one target instruction or throws when disassembly fails.
+ public AssemblyInstructionSnapshot Disassemble(Address address, CancellationToken cancellationToken = default);
+
+ /// Tries to get the exact size of the instruction at an address.
+ public bool TryGetInstructionSize(Address address, out int size, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Gets the instruction size or throws when Cheat Engine cannot decode it.
+ public int GetInstructionSize(Address address, CancellationToken cancellationToken = default);
+
+ /// Tries to resolve the start address of the preceding target instruction.
+ public bool TryGetPreviousInstruction(Address address, out Address previousAddress,
+ out CheatEngineFailure failure, CancellationToken cancellationToken = default);
+
+ /// Gets the preceding instruction address or throws when it cannot be resolved.
+ public Address GetPreviousInstruction(Address address, CancellationToken cancellationToken = default);
+
+ /// Tries to copy the optional comment associated with one target address.
+ public bool TryGetComment(Address address, [NotNullWhen(true)] out string? comment,
+ out CheatEngineFailure failure, CancellationToken cancellationToken = default);
+
+ /// Gets an address comment or throws when no comment is available.
+ public string GetComment(Address address, CancellationToken cancellationToken = default);
+
+ /// Tries to assemble exactly one instruction into copied bytes.
+ public bool TryAssemble(AssemblyInstructionRequest request, out ImmutableArray bytes,
+ out CheatEngineFailure failure, CancellationToken cancellationToken = default);
+
+ /// Assembles exactly one instruction into copied bytes or throws when assembly fails.
+ public ImmutableArray Assemble(AssemblyInstructionRequest request,
+ CancellationToken cancellationToken = default);
+
+ /// Tries to apply a Client-owned Auto Assembler script.
+ public bool TryApplyPatch(AutoAssemblerScript script, [NotNullWhen(true)] out IAutoAssemblerPatchLease? lease,
+ out CheatEngineFailure failure, CancellationToken cancellationToken = default);
+
+ /// Applies a Client-owned Auto Assembler script or throws when the host rejects it.
+ public IAutoAssemblerPatchLease ApplyPatch(AutoAssemblerScript script,
+ CancellationToken cancellationToken = default);
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Assembly/IAutoAssemblerPatchLease.cs b/libs/CheatEngine.Client.Abstractions/Assembly/IAutoAssemblerPatchLease.cs
new file mode 100644
index 0000000..5535d28
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Assembly/IAutoAssemblerPatchLease.cs
@@ -0,0 +1,23 @@
+namespace CheatEngine.Client.Assembly;
+
+/// Owns one Client-applied Auto Assembler patch for the current activation and target selection.
+public interface IAutoAssemblerPatchLease : IDisposable
+{
+ /// Gets the Client diagnostic name supplied when the patch was applied.
+ public string? Name
+ {
+ get;
+ }
+
+ /// Gets the target-selection epoch captured by this patch.
+ public long SelectionEpoch
+ {
+ get;
+ }
+
+ /// Gets whether the patch has already executed its disable cleanup or was invalidated.
+ public bool IsReleased
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Dbvm/DbvmInitializationRequest.cs b/libs/CheatEngine.Client.Abstractions/Dbvm/DbvmInitializationRequest.cs
new file mode 100644
index 0000000..feb9ab0
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Dbvm/DbvmInitializationRequest.cs
@@ -0,0 +1,17 @@
+namespace CheatEngine.Client.Dbvm;
+
+/// Describes an explicit DBVM initialization request.
+public readonly record struct DbvmInitializationRequest
+{
+ /// Creates a DBVM initialization request.
+ public DbvmInitializationRequest(bool useStealthMode = false)
+ {
+ UseStealthMode = useStealthMode;
+ }
+
+ /// Gets whether the caller explicitly requests the host's documented stealth mode.
+ public bool UseStealthMode
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Dbvm/DbvmState.cs b/libs/CheatEngine.Client.Abstractions/Dbvm/DbvmState.cs
new file mode 100644
index 0000000..3c57db8
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Dbvm/DbvmState.cs
@@ -0,0 +1,17 @@
+namespace CheatEngine.Client.Dbvm;
+
+/// Describes the observed DBVM lifecycle without triggering initialization.
+public enum DbvmState
+{
+ /// No host observation has established the DBVM state.
+ Unknown,
+
+ /// The active host cannot expose DBVM operations.
+ Unavailable,
+
+ /// DBVM is supported but has not been initialized.
+ NotInitialized,
+
+ /// DBVM has been initialized explicitly for the active host.
+ Initialized
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Dbvm/DbvmStatusSnapshot.cs b/libs/CheatEngine.Client.Abstractions/Dbvm/DbvmStatusSnapshot.cs
new file mode 100644
index 0000000..2a6c742
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Dbvm/DbvmStatusSnapshot.cs
@@ -0,0 +1,35 @@
+namespace CheatEngine.Client.Dbvm;
+
+/// Contains a copied DBVM status observation.
+public readonly record struct DbvmStatusSnapshot
+{
+ /// Creates a copied DBVM status observation.
+ /// is not a defined value.
+ public DbvmStatusSnapshot(DbvmState state, string? version = null)
+ {
+ if (!Enum.IsDefined(state))
+ {
+ throw new ArgumentOutOfRangeException(nameof(state));
+ }
+
+ if (version is not null && string.IsNullOrWhiteSpace(version))
+ {
+ throw new ArgumentException("A DBVM version must be null or non-empty.", nameof(version));
+ }
+
+ State = state;
+ Version = version;
+ }
+
+ /// Gets the observed state without causing initialization.
+ public DbvmState State
+ {
+ get;
+ }
+
+ /// Gets the optional copied DBVM version reported by the host.
+ public string? Version
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Dbvm/DbvmWatchEvent.cs b/libs/CheatEngine.Client.Abstractions/Dbvm/DbvmWatchEvent.cs
new file mode 100644
index 0000000..1f4e705
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Dbvm/DbvmWatchEvent.cs
@@ -0,0 +1,35 @@
+using CheatEngine.SDK.Engine.Values;
+
+namespace CheatEngine.Client.Dbvm;
+
+/// Contains a copied DBVM watch observation.
+public readonly record struct DbvmWatchEvent
+{
+ /// Creates a copied DBVM watch observation.
+ /// is not positive.
+ public DbvmWatchEvent(Address address, int length, DateTimeOffset occurredAt)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(length);
+ Address = address;
+ Length = length;
+ OccurredAt = occurredAt;
+ }
+
+ /// Gets the copied target address that triggered the watch.
+ public Address Address
+ {
+ get;
+ }
+
+ /// Gets the copied positive byte count associated with the event.
+ public int Length
+ {
+ get;
+ }
+
+ /// Gets the copied observation timestamp.
+ public DateTimeOffset OccurredAt
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Dbvm/DbvmWatchHandler.cs b/libs/CheatEngine.Client.Abstractions/Dbvm/DbvmWatchHandler.cs
new file mode 100644
index 0000000..9933c3b
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Dbvm/DbvmWatchHandler.cs
@@ -0,0 +1,4 @@
+namespace CheatEngine.Client.Dbvm;
+
+/// Handles a copied DBVM watch event synchronously without blocking the host callback thread.
+public delegate void DbvmWatchHandler(DbvmWatchEvent watchEvent);
diff --git a/libs/CheatEngine.Client.Abstractions/Dbvm/DbvmWatchRequest.cs b/libs/CheatEngine.Client.Abstractions/Dbvm/DbvmWatchRequest.cs
new file mode 100644
index 0000000..0cc79e0
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Dbvm/DbvmWatchRequest.cs
@@ -0,0 +1,28 @@
+using CheatEngine.SDK.Engine.Values;
+
+namespace CheatEngine.Client.Dbvm;
+
+/// Describes a bounded DBVM watch over one target-memory range.
+public readonly record struct DbvmWatchRequest
+{
+ /// Creates a bounded DBVM watch request.
+ /// is not positive.
+ public DbvmWatchRequest(Address address, int length)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(length);
+ Address = address;
+ Length = length;
+ }
+
+ /// Gets the first target address covered by the watch.
+ public Address Address
+ {
+ get;
+ }
+
+ /// Gets the exact positive watched byte count.
+ public int Length
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Dbvm/IDbvmClient.cs b/libs/CheatEngine.Client.Abstractions/Dbvm/IDbvmClient.cs
new file mode 100644
index 0000000..cd44597
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Dbvm/IDbvmClient.cs
@@ -0,0 +1,41 @@
+using System.Diagnostics.CodeAnalysis;
+
+using CheatEngine.Client.Events;
+using CheatEngine.Client.Results;
+
+namespace CheatEngine.Client.Dbvm;
+
+/// Observes DBVM state and exposes explicit initialization and watch operations.
+public interface IDbvmClient
+{
+ /// Tries to observe DBVM state without initializing DBVM.
+ public bool TryGetStatus(out DbvmStatusSnapshot status, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Observes DBVM state without initializing DBVM.
+ public DbvmStatusSnapshot GetStatus(CancellationToken cancellationToken = default);
+
+ /// Tries to initialize DBVM only after an explicit caller request.
+ public bool TryInitialize(DbvmInitializationRequest request, out DbvmStatusSnapshot status,
+ out CheatEngineFailure failure, CancellationToken cancellationToken = default);
+
+ /// Initializes DBVM only after an explicit caller request.
+ public DbvmStatusSnapshot Initialize(DbvmInitializationRequest request,
+ CancellationToken cancellationToken = default);
+
+ /// Tries to register a DBVM watch and its bounded copied event stream.
+ public bool TryRegisterWatch(
+ DbvmWatchRequest request,
+ DbvmWatchHandler handler,
+ EventStreamOptions streamOptions,
+ [NotNullWhen(true)] out IDbvmWatchLease? lease,
+ out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Registers a DBVM watch or throws when the capability is unavailable or registration fails.
+ public IDbvmWatchLease RegisterWatch(
+ DbvmWatchRequest request,
+ DbvmWatchHandler handler,
+ EventStreamOptions streamOptions,
+ CancellationToken cancellationToken = default);
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Dbvm/IDbvmWatchLease.cs b/libs/CheatEngine.Client.Abstractions/Dbvm/IDbvmWatchLease.cs
new file mode 100644
index 0000000..a8ad5ac
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Dbvm/IDbvmWatchLease.cs
@@ -0,0 +1,19 @@
+using CheatEngine.Client.Events;
+
+namespace CheatEngine.Client.Dbvm;
+
+/// Owns one Client DBVM watch and its bounded copied event stream.
+public interface IDbvmWatchLease : IEventStreamLease
+{
+ /// Gets the request used to create this watch.
+ public DbvmWatchRequest Request
+ {
+ get;
+ }
+
+ /// Gets the target-selection epoch captured when the watch was registered.
+ public long SelectionEpoch
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Debugger/BreakpointDisposition.cs b/libs/CheatEngine.Client.Abstractions/Debugger/BreakpointDisposition.cs
new file mode 100644
index 0000000..2bb23d3
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Debugger/BreakpointDisposition.cs
@@ -0,0 +1,11 @@
+namespace CheatEngine.Client.Debugger;
+
+/// Specifies the immediate continuation decision returned by a synchronous breakpoint handler.
+public enum BreakpointDisposition
+{
+ /// Continues target execution after the synchronous handler returns.
+ Continue,
+
+ /// Leaves execution broken for the host's debugger workflow.
+ Break
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Debugger/BreakpointEvent.cs b/libs/CheatEngine.Client.Abstractions/Debugger/BreakpointEvent.cs
new file mode 100644
index 0000000..0630b8a
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Debugger/BreakpointEvent.cs
@@ -0,0 +1,42 @@
+using System.Collections.Immutable;
+
+using CheatEngine.SDK.Engine.Values;
+
+namespace CheatEngine.Client.Debugger;
+
+/// Contains copied data delivered synchronously for one breakpoint hit.
+public readonly record struct BreakpointEvent
+{
+ /// Creates a copied breakpoint event.
+ /// is negative.
+ public BreakpointEvent(Address address, int threadId, ImmutableArray registers)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(threadId);
+ if (registers.IsDefault)
+ {
+ throw new ArgumentException("Register snapshots must be initialized.", nameof(registers));
+ }
+
+ Address = address;
+ ThreadId = threadId;
+ Registers = registers;
+ }
+
+ /// Gets the copied instruction address that triggered the breakpoint.
+ public Address Address
+ {
+ get;
+ }
+
+ /// Gets the non-negative target thread identifier.
+ public int ThreadId
+ {
+ get;
+ }
+
+ /// Gets immutable copied register observations.
+ public ImmutableArray Registers
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Debugger/BreakpointHandler.cs b/libs/CheatEngine.Client.Abstractions/Debugger/BreakpointHandler.cs
new file mode 100644
index 0000000..05ccfa3
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Debugger/BreakpointHandler.cs
@@ -0,0 +1,4 @@
+namespace CheatEngine.Client.Debugger;
+
+/// Handles one copied breakpoint event without awaiting on a Cheat Engine callback thread.
+public delegate BreakpointDisposition BreakpointHandler(BreakpointEvent breakpointEvent);
diff --git a/libs/CheatEngine.Client.Abstractions/Debugger/BreakpointKind.cs b/libs/CheatEngine.Client.Abstractions/Debugger/BreakpointKind.cs
new file mode 100644
index 0000000..806436e
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Debugger/BreakpointKind.cs
@@ -0,0 +1,14 @@
+namespace CheatEngine.Client.Debugger;
+
+/// Specifies the target-memory access that triggers a breakpoint.
+public enum BreakpointKind
+{
+ /// Breaks when execution reaches the target address.
+ Execute,
+
+ /// Breaks when the target address is written.
+ Write,
+
+ /// Breaks when the target address is read or written.
+ Access
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Debugger/BreakpointRegisterSnapshot.cs b/libs/CheatEngine.Client.Abstractions/Debugger/BreakpointRegisterSnapshot.cs
new file mode 100644
index 0000000..5d50206
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Debugger/BreakpointRegisterSnapshot.cs
@@ -0,0 +1,26 @@
+namespace CheatEngine.Client.Debugger;
+
+/// Contains one copied register observation associated with a breakpoint event.
+public readonly record struct BreakpointRegisterSnapshot
+{
+ /// Creates a copied register observation.
+ /// is blank.
+ public BreakpointRegisterSnapshot(string name, ulong value)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(name);
+ Name = name;
+ Value = value;
+ }
+
+ /// Gets the normalized register name.
+ public string Name
+ {
+ get;
+ }
+
+ /// Gets the copied register machine value.
+ public ulong Value
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Debugger/BreakpointRequest.cs b/libs/CheatEngine.Client.Abstractions/Debugger/BreakpointRequest.cs
new file mode 100644
index 0000000..e3890d3
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Debugger/BreakpointRequest.cs
@@ -0,0 +1,32 @@
+using CheatEngine.SDK.Engine.Values;
+
+namespace CheatEngine.Client.Debugger;
+
+/// Describes one Client-owned breakpoint.
+public readonly record struct BreakpointRequest
+{
+ /// Creates a breakpoint request.
+ /// is not a defined value.
+ public BreakpointRequest(Address address, BreakpointKind kind = BreakpointKind.Execute)
+ {
+ if (!Enum.IsDefined(kind))
+ {
+ throw new ArgumentOutOfRangeException(nameof(kind));
+ }
+
+ Address = address;
+ Kind = kind;
+ }
+
+ /// Gets the target address observed by the breakpoint.
+ public Address Address
+ {
+ get;
+ }
+
+ /// Gets the access kind that triggers the breakpoint.
+ public BreakpointKind Kind
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Debugger/IBreakpointLease.cs b/libs/CheatEngine.Client.Abstractions/Debugger/IBreakpointLease.cs
new file mode 100644
index 0000000..9a0e123
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Debugger/IBreakpointLease.cs
@@ -0,0 +1,19 @@
+using CheatEngine.Client.Events;
+
+namespace CheatEngine.Client.Debugger;
+
+/// Owns one Client breakpoint and its bounded copied event stream.
+public interface IBreakpointLease : IEventStreamLease
+{
+ /// Gets the request used to create this breakpoint.
+ public BreakpointRequest Request
+ {
+ get;
+ }
+
+ /// Gets the target-selection epoch captured when the breakpoint was registered.
+ public long SelectionEpoch
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Debugger/IDebuggerClient.cs b/libs/CheatEngine.Client.Abstractions/Debugger/IDebuggerClient.cs
new file mode 100644
index 0000000..53d2cc7
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Debugger/IDebuggerClient.cs
@@ -0,0 +1,26 @@
+using System.Diagnostics.CodeAnalysis;
+
+using CheatEngine.Client.Events;
+using CheatEngine.Client.Results;
+
+namespace CheatEngine.Client.Debugger;
+
+/// Registers Client-owned breakpoints with immediate handler decisions and bounded copied event streams.
+public interface IDebuggerClient
+{
+ /// Tries to register one breakpoint for the current activation and selection.
+ public bool TryRegisterBreakpoint(
+ BreakpointRequest request,
+ BreakpointHandler handler,
+ EventStreamOptions streamOptions,
+ [NotNullWhen(true)] out IBreakpointLease? lease,
+ out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Registers one breakpoint or throws when the capability is unavailable or registration fails.
+ public IBreakpointLease RegisterBreakpoint(
+ BreakpointRequest request,
+ BreakpointHandler handler,
+ EventStreamOptions streamOptions,
+ CancellationToken cancellationToken = default);
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Events/EventStreamOptions.cs b/libs/CheatEngine.Client.Abstractions/Events/EventStreamOptions.cs
new file mode 100644
index 0000000..c60a913
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Events/EventStreamOptions.cs
@@ -0,0 +1,34 @@
+namespace CheatEngine.Client.Events;
+
+/// Configures bounded, non-blocking materialization of host callback events.
+public readonly record struct EventStreamOptions
+{
+ /// Creates bounded stream options.
+ /// is not positive.
+ /// is not a defined value.
+ public EventStreamOptions(
+ int capacity,
+ EventStreamOverflowPolicy overflowPolicy = EventStreamOverflowPolicy.DropOldest)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(capacity);
+ if (!Enum.IsDefined(overflowPolicy))
+ {
+ throw new ArgumentOutOfRangeException(nameof(overflowPolicy));
+ }
+
+ Capacity = capacity;
+ OverflowPolicy = overflowPolicy;
+ }
+
+ /// Gets the mandatory maximum number of events buffered for a consumer.
+ public int Capacity
+ {
+ get;
+ }
+
+ /// Gets the policy applied when is reached.
+ public EventStreamOverflowPolicy OverflowPolicy
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Events/EventStreamOverflowPolicy.cs b/libs/CheatEngine.Client.Abstractions/Events/EventStreamOverflowPolicy.cs
new file mode 100644
index 0000000..37b8048
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Events/EventStreamOverflowPolicy.cs
@@ -0,0 +1,14 @@
+namespace CheatEngine.Client.Events;
+
+/// Specifies the bounded-admission policy for a copied Client event stream.
+public enum EventStreamOverflowPolicy
+{
+ /// Evicts the oldest buffered event before admitting the newest event.
+ DropOldest,
+
+ /// Rejects the newly raised event while retaining the buffered events.
+ DropNewest,
+
+ /// Terminates the subscription when its bounded buffer cannot admit an event.
+ FailSubscription
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Events/IEventStreamLease.cs b/libs/CheatEngine.Client.Abstractions/Events/IEventStreamLease.cs
new file mode 100644
index 0000000..c166cb5
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Events/IEventStreamLease.cs
@@ -0,0 +1,28 @@
+namespace CheatEngine.Client.Events;
+
+/// Owns a bounded stream of copied callback events for one Client activation.
+/// The copied event snapshot type.
+///
+/// Implementations must never wait for a stream consumer on a Cheat Engine or Lua callback thread. Disposing a
+/// lease closes admission, detaches its callback, completes , and releases host state.
+///
+public interface IEventStreamLease : IDisposable
+{
+ /// Gets the bounded stream of copied events.
+ public IAsyncEnumerable Events
+ {
+ get;
+ }
+
+ /// Gets the number of events that were not admitted to the stream.
+ public long DroppedEventCount
+ {
+ get;
+ }
+
+ /// Gets whether this activation-scoped subscription has released its host registration.
+ public bool IsReleased
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Hashing/FileHashRequest.cs b/libs/CheatEngine.Client.Abstractions/Hashing/FileHashRequest.cs
new file mode 100644
index 0000000..8f5173c
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Hashing/FileHashRequest.cs
@@ -0,0 +1,36 @@
+namespace CheatEngine.Client.Hashing;
+
+/// Describes a file hash request that is intentionally separate from target-memory hashing.
+public readonly record struct FileHashRequest
+{
+ /// Creates a file hash request.
+ /// is blank or relative, or the algorithm is undefined.
+ public FileHashRequest(string filePath, TargetHashAlgorithm algorithm = TargetHashAlgorithm.Sha256)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(filePath);
+ if (!Path.IsPathFullyQualified(filePath))
+ {
+ throw new ArgumentException("A file hash path must be absolute.", nameof(filePath));
+ }
+
+ if (!Enum.IsDefined(algorithm))
+ {
+ throw new ArgumentOutOfRangeException(nameof(algorithm));
+ }
+
+ FilePath = filePath;
+ Algorithm = algorithm;
+ }
+
+ /// Gets the absolute file path. The implementation verifies existence before hashing.
+ public string FilePath
+ {
+ get;
+ }
+
+ /// Gets the requested hash algorithm.
+ public TargetHashAlgorithm Algorithm
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Hashing/HashDigest.cs b/libs/CheatEngine.Client.Abstractions/Hashing/HashDigest.cs
new file mode 100644
index 0000000..6341fef
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Hashing/HashDigest.cs
@@ -0,0 +1,31 @@
+namespace CheatEngine.Client.Hashing;
+
+/// Contains a copied, normalized digest value.
+public readonly record struct HashDigest
+{
+ /// Creates a digest result.
+ /// is blank or the algorithm is undefined.
+ public HashDigest(TargetHashAlgorithm algorithm, string value)
+ {
+ if (!Enum.IsDefined(algorithm))
+ {
+ throw new ArgumentOutOfRangeException(nameof(algorithm));
+ }
+
+ ArgumentException.ThrowIfNullOrWhiteSpace(value);
+ Algorithm = algorithm;
+ Value = value;
+ }
+
+ /// Gets the algorithm that produced .
+ public TargetHashAlgorithm Algorithm
+ {
+ get;
+ }
+
+ /// Gets the copied textual digest.
+ public string Value
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Hashing/IHashingClient.cs b/libs/CheatEngine.Client.Abstractions/Hashing/IHashingClient.cs
new file mode 100644
index 0000000..f72ab52
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Hashing/IHashingClient.cs
@@ -0,0 +1,21 @@
+using CheatEngine.Client.Results;
+
+namespace CheatEngine.Client.Hashing;
+
+/// Hashes bounded target memory and files through explicitly separate operations.
+public interface IHashingClient
+{
+ /// Tries to hash an exact target-memory range.
+ public bool TryHashMemory(MemoryHashRequest request, out HashDigest digest, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Hashes an exact target-memory range or throws when unavailable.
+ public HashDigest HashMemory(MemoryHashRequest request, CancellationToken cancellationToken = default);
+
+ /// Tries to hash an existing file independently from the selected target.
+ public bool TryHashFile(FileHashRequest request, out HashDigest digest, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Hashes an existing file independently from the selected target.
+ public HashDigest HashFile(FileHashRequest request, CancellationToken cancellationToken = default);
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Hashing/MemoryHashRequest.cs b/libs/CheatEngine.Client.Abstractions/Hashing/MemoryHashRequest.cs
new file mode 100644
index 0000000..1119f64
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Hashing/MemoryHashRequest.cs
@@ -0,0 +1,40 @@
+using CheatEngine.SDK.Engine.Values;
+
+namespace CheatEngine.Client.Hashing;
+
+/// Describes a bounded target-memory hash request.
+public readonly record struct MemoryHashRequest
+{
+ /// Creates a target-memory hash request.
+ /// is not positive or the algorithm is undefined.
+ public MemoryHashRequest(Address address, int length, TargetHashAlgorithm algorithm = TargetHashAlgorithm.Sha256)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(length);
+ if (!Enum.IsDefined(algorithm))
+ {
+ throw new ArgumentOutOfRangeException(nameof(algorithm));
+ }
+
+ Address = address;
+ Length = length;
+ Algorithm = algorithm;
+ }
+
+ /// Gets the first target address included in the hash.
+ public Address Address
+ {
+ get;
+ }
+
+ /// Gets the exact positive number of target bytes included in the hash.
+ public int Length
+ {
+ get;
+ }
+
+ /// Gets the requested hash algorithm.
+ public TargetHashAlgorithm Algorithm
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Hashing/TargetHashAlgorithm.cs b/libs/CheatEngine.Client.Abstractions/Hashing/TargetHashAlgorithm.cs
new file mode 100644
index 0000000..eaf5b3d
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Hashing/TargetHashAlgorithm.cs
@@ -0,0 +1,14 @@
+namespace CheatEngine.Client.Hashing;
+
+/// Identifies a hash algorithm supported by the Client hashing contract.
+public enum TargetHashAlgorithm
+{
+ /// MD5.
+ Md5,
+
+ /// SHA-1.
+ Sha1,
+
+ /// SHA-256.
+ Sha256
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Hotkeys/HotkeyEvent.cs b/libs/CheatEngine.Client.Abstractions/Hotkeys/HotkeyEvent.cs
new file mode 100644
index 0000000..ee89eb5
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Hotkeys/HotkeyEvent.cs
@@ -0,0 +1,33 @@
+namespace CheatEngine.Client.Hotkeys;
+
+/// Contains a copied target hotkey activation.
+public readonly record struct HotkeyEvent
+{
+ /// Creates a copied hotkey activation.
+ /// is blank.
+ public HotkeyEvent(string name, HotkeyGesture gesture, DateTimeOffset occurredAt)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(name);
+ Name = name;
+ Gesture = gesture;
+ OccurredAt = occurredAt;
+ }
+
+ /// Gets the Client registration name.
+ public string Name
+ {
+ get;
+ }
+
+ /// Gets the copied gesture that was activated.
+ public HotkeyGesture Gesture
+ {
+ get;
+ }
+
+ /// Gets the copied activation timestamp.
+ public DateTimeOffset OccurredAt
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Hotkeys/HotkeyGesture.cs b/libs/CheatEngine.Client.Abstractions/Hotkeys/HotkeyGesture.cs
new file mode 100644
index 0000000..750f6ac
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Hotkeys/HotkeyGesture.cs
@@ -0,0 +1,39 @@
+namespace CheatEngine.Client.Hotkeys;
+
+/// Describes a platform-neutral virtual-key gesture.
+public readonly record struct HotkeyGesture
+{
+ /// Creates a hotkey gesture.
+ ///
+ /// is outside the byte-sized Windows
+ /// virtual-key range.
+ ///
+ public HotkeyGesture(int virtualKey, HotkeyModifiers modifiers = HotkeyModifiers.None)
+ {
+ if (virtualKey is < byte.MinValue or > byte.MaxValue)
+ {
+ throw new ArgumentOutOfRangeException(nameof(virtualKey));
+ }
+
+ if ((modifiers & ~(HotkeyModifiers.Alt | HotkeyModifiers.Control | HotkeyModifiers.Shift |
+ HotkeyModifiers.Windows)) != 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(modifiers));
+ }
+
+ VirtualKey = virtualKey;
+ Modifiers = modifiers;
+ }
+
+ /// Gets the platform virtual-key code.
+ public int VirtualKey
+ {
+ get;
+ }
+
+ /// Gets the requested modifier combination.
+ public HotkeyModifiers Modifiers
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Hotkeys/HotkeyHandler.cs b/libs/CheatEngine.Client.Abstractions/Hotkeys/HotkeyHandler.cs
new file mode 100644
index 0000000..411552f
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Hotkeys/HotkeyHandler.cs
@@ -0,0 +1,4 @@
+namespace CheatEngine.Client.Hotkeys;
+
+/// Handles a copied hotkey event synchronously without blocking the host callback thread.
+public delegate void HotkeyHandler(HotkeyEvent hotkeyEvent);
diff --git a/libs/CheatEngine.Client.Abstractions/Hotkeys/HotkeyModifiers.cs b/libs/CheatEngine.Client.Abstractions/Hotkeys/HotkeyModifiers.cs
new file mode 100644
index 0000000..c6e2197
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Hotkeys/HotkeyModifiers.cs
@@ -0,0 +1,21 @@
+namespace CheatEngine.Client.Hotkeys;
+
+/// Specifies modifier keys that participate in a registered target hotkey.
+[Flags]
+public enum HotkeyModifiers
+{
+ /// No modifier key.
+ None = 0,
+
+ /// The Alt modifier.
+ Alt = 1,
+
+ /// The Control modifier.
+ Control = 2,
+
+ /// The Shift modifier.
+ Shift = 4,
+
+ /// The Windows modifier.
+ Windows = 8
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Hotkeys/HotkeyRegistration.cs b/libs/CheatEngine.Client.Abstractions/Hotkeys/HotkeyRegistration.cs
new file mode 100644
index 0000000..300a709
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Hotkeys/HotkeyRegistration.cs
@@ -0,0 +1,26 @@
+namespace CheatEngine.Client.Hotkeys;
+
+/// Describes one Client-owned hotkey registration.
+public readonly record struct HotkeyRegistration
+{
+ /// Creates a hotkey registration.
+ /// is blank.
+ public HotkeyRegistration(string name, HotkeyGesture gesture)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(name);
+ Name = name;
+ Gesture = gesture;
+ }
+
+ /// Gets the Client-unique diagnostic name.
+ public string Name
+ {
+ get;
+ }
+
+ /// Gets the registered hotkey gesture.
+ public HotkeyGesture Gesture
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Hotkeys/IHotkeyClient.cs b/libs/CheatEngine.Client.Abstractions/Hotkeys/IHotkeyClient.cs
new file mode 100644
index 0000000..01962d7
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Hotkeys/IHotkeyClient.cs
@@ -0,0 +1,26 @@
+using System.Diagnostics.CodeAnalysis;
+
+using CheatEngine.Client.Events;
+using CheatEngine.Client.Results;
+
+namespace CheatEngine.Client.Hotkeys;
+
+/// Registers Client-owned hotkeys for the current activation.
+public interface IHotkeyClient
+{
+ /// Tries to register one hotkey and a bounded copied event stream.
+ public bool TryRegister(
+ HotkeyRegistration registration,
+ HotkeyHandler handler,
+ EventStreamOptions streamOptions,
+ [NotNullWhen(true)] out IHotkeyLease? lease,
+ out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Registers one hotkey or throws when the capability is unavailable or registration fails.
+ public IHotkeyLease Register(
+ HotkeyRegistration registration,
+ HotkeyHandler handler,
+ EventStreamOptions streamOptions,
+ CancellationToken cancellationToken = default);
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Hotkeys/IHotkeyLease.cs b/libs/CheatEngine.Client.Abstractions/Hotkeys/IHotkeyLease.cs
new file mode 100644
index 0000000..29b4268
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Hotkeys/IHotkeyLease.cs
@@ -0,0 +1,13 @@
+using CheatEngine.Client.Events;
+
+namespace CheatEngine.Client.Hotkeys;
+
+/// Owns one Client hotkey registration and its bounded copied event stream.
+public interface IHotkeyLease : IEventStreamLease
+{
+ /// Gets the registration owned by this lease.
+ public HotkeyRegistration Registration
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/ICheatEngineClient.cs b/libs/CheatEngine.Client.Abstractions/ICheatEngineClient.cs
index a5c1938..4b05b8e 100644
--- a/libs/CheatEngine.Client.Abstractions/ICheatEngineClient.cs
+++ b/libs/CheatEngine.Client.Abstractions/ICheatEngineClient.cs
@@ -1,11 +1,20 @@
+using CheatEngine.Client.Allocations;
+using CheatEngine.Client.Assembly;
+using CheatEngine.Client.Dbvm;
+using CheatEngine.Client.Debugger;
using CheatEngine.Client.Dispatching;
+using CheatEngine.Client.Hashing;
+using CheatEngine.Client.Hotkeys;
using CheatEngine.Client.Inspection;
using CheatEngine.Client.Lua;
using CheatEngine.Client.Memory;
using CheatEngine.Client.Processes;
+using CheatEngine.Client.RemoteExecution;
using CheatEngine.Client.Runtime;
using CheatEngine.Client.Scanning;
+using CheatEngine.Client.Speed;
using CheatEngine.Client.Tables;
+using CheatEngine.Client.Timers;
namespace CheatEngine.Client;
@@ -82,4 +91,58 @@ public ILuaClient Lua
{
get;
}
+
+ /// Gets owned target-memory allocation operations.
+ public IAllocationClient Allocations
+ {
+ get;
+ }
+
+ /// Gets copied assembly, disassembly, comment, and Auto Assembler patch operations.
+ public IAssemblyClient Assembly
+ {
+ get;
+ }
+
+ /// Gets bounded DLL injection and remote-call operations.
+ public IRemoteExecutionClient RemoteExecution
+ {
+ get;
+ }
+
+ /// Gets breakpoint and copied debugger-event operations.
+ public IDebuggerClient Debugger
+ {
+ get;
+ }
+
+ /// Gets activation-scoped hotkey operations.
+ public IHotkeyClient Hotkeys
+ {
+ get;
+ }
+
+ /// Gets activation-scoped timer operations.
+ public ITimerClient Timers
+ {
+ get;
+ }
+
+ /// Gets validated target-speed operations.
+ public ISpeedClient Speed
+ {
+ get;
+ }
+
+ /// Gets explicitly separated target-memory and file hashing operations.
+ public IHashingClient Hashing
+ {
+ get;
+ }
+
+ /// Gets observational and explicitly initialized DBVM operations.
+ public IDbvmClient Dbvm
+ {
+ get;
+ }
}
diff --git a/libs/CheatEngine.Client.Abstractions/Lua/CheatEngineLuaModuleAttribute.cs b/libs/CheatEngine.Client.Abstractions/Lua/CheatEngineLuaModuleAttribute.cs
new file mode 100644
index 0000000..add6b76
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Lua/CheatEngineLuaModuleAttribute.cs
@@ -0,0 +1,33 @@
+namespace CheatEngine.Client.Lua;
+
+///
+/// Declares a Client-owned Lua module whose registration adapter is emitted at compile time.
+///
+///
+///
+/// The annotated type is a non-static partial class. names the static partial
+/// SDK binding type that owns the generated RegisterLuaFunctions and UnregisterLuaFunctions
+/// methods. The Client generator reads this declaration at compile time; it never discovers modules through
+/// reflection at run time.
+///
+///
+/// The optional name is the stable identity reserved by for one activation. When it
+/// is omitted, the generator uses the annotated module type's simple name. Lua export identities are copied
+/// from the [LuaFunction] declarations on .
+///
+///
+[AttributeUsage(AttributeTargets.Class, Inherited = false)]
+public sealed class CheatEngineLuaModuleAttribute(Type bindingsType, string? name = null) : Attribute
+{
+ /// Gets the static SDK binding type that owns the generated Lua registration pair.
+ public Type BindingsType
+ {
+ get;
+ } = bindingsType ?? throw new ArgumentNullException(nameof(bindingsType));
+
+ /// Gets the optional stable module identity, or to use the module type name.
+ public string? Name
+ {
+ get;
+ } = name;
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Lua/CheatEngineLuaOperationAttribute.cs b/libs/CheatEngine.Client.Abstractions/Lua/CheatEngineLuaOperationAttribute.cs
new file mode 100644
index 0000000..0d88d85
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Lua/CheatEngineLuaOperationAttribute.cs
@@ -0,0 +1,39 @@
+namespace CheatEngine.Client.Lua;
+
+///
+/// Marks one SDK [LuaGlobal] declaration for generation of a copied, typed Client operation.
+///
+///
+///
+/// The Client generator emits a readonly operation value and a strongly typed factory beside the declaring
+/// binding type. The operation catches SDK Lua failures inside the Client boundary and returns
+/// instead of exposing a Lua state, reference,
+/// status or CE object.
+///
+///
+/// Supply a mapper type when the SDK result is not itself an approved copied scalar. The named type must
+/// implement for the SDK result and the
+/// desired Client DTO. The generator calls that mapper statically; no reflection is involved.
+///
+///
+[AttributeUsage(AttributeTargets.Method, Inherited = false)]
+public sealed class CheatEngineLuaOperationAttribute : Attribute
+{
+ /// Initializes scalar-result operation generation without a mapper.
+ public CheatEngineLuaOperationAttribute()
+ {
+ }
+
+ /// Initializes operation generation with one explicit static Client result mapper.
+ /// The type that implements the required static mapper contract.
+ public CheatEngineLuaOperationAttribute(Type mapperType)
+ {
+ MapperType = mapperType ?? throw new ArgumentNullException(nameof(mapperType));
+ }
+
+ /// Gets the explicit result mapper type, or for a copied scalar result.
+ public Type? MapperType
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Lua/IDescribedLuaModule.cs b/libs/CheatEngine.Client.Abstractions/Lua/IDescribedLuaModule.cs
new file mode 100644
index 0000000..1b5521c
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Lua/IDescribedLuaModule.cs
@@ -0,0 +1,18 @@
+namespace CheatEngine.Client.Lua;
+
+///
+/// Describes an explicit Lua module before the Client mutates Cheat Engine's global Lua environment.
+///
+///
+/// The descriptor is copied metadata only. It intentionally contains neither a Lua state nor any SDK ownership,
+/// reference, callback, or native handle. uses it to reserve the module and all export
+/// names atomically for an activation before invoking .
+///
+public interface IDescribedLuaModule : ILuaModule
+{
+ /// Gets this module's stable identity and exported Lua global names.
+ public LuaModuleDescriptor Descriptor
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Lua/ILuaClient.cs b/libs/CheatEngine.Client.Abstractions/Lua/ILuaClient.cs
index b6d6349..f3372e3 100644
--- a/libs/CheatEngine.Client.Abstractions/Lua/ILuaClient.cs
+++ b/libs/CheatEngine.Client.Abstractions/Lua/ILuaClient.cs
@@ -21,10 +21,55 @@ public bool TryRegisterModule(
/// Registers one explicitly supplied application Lua module or throws when registration fails.
public ILuaModuleLease RegisterModule(ILuaModule luaModule, CancellationToken cancellationToken = default);
+#pragma warning disable RS0026, RS0027 // The constrained overloads preserve the shipped overloads and require a token.
+
/// Tries to execute one typed operation on Cheat Engine's main thread.
public bool TryExecute(ILuaOperation operation, [MaybeNullWhen(false)] out TResult result,
out CheatEngineFailure failure, CancellationToken cancellationToken = default);
/// Executes one typed operation or throws when it fails.
public TResult Execute(ILuaOperation operation, CancellationToken cancellationToken = default);
+
+ ///
+ /// Tries to execute a value-type operation without forcing callers to convert it to an interface explicitly.
+ ///
+ /// The readonly value operation type.
+ /// The copied result type.
+ /// The operation to execute.
+ /// The copied result on success.
+ /// The mapped Client failure on failure.
+ /// Cancellation observed before dispatch admission.
+ /// when the operation completed successfully.
+ ///
+ /// This default implementation preserves source and binary compatibility for third-party implementations of
+ /// . Core supplies a constrained implementation that avoids boxing the generated
+ /// readonly record struct operation on its measured path.
+ ///
+ public bool TryExecute(
+ TOperation operation,
+ [MaybeNullWhen(false)] out TResult result,
+ out CheatEngineFailure failure,
+ CancellationToken cancellationToken)
+ where TOperation : struct, ILuaOperation
+ {
+ return TryExecute(operation, out result, out failure, cancellationToken);
+ }
+
+ /// Executes a value-type operation or throws when it fails.
+ /// The readonly value operation type.
+ /// The copied result type.
+ /// The operation to execute.
+ /// Cancellation observed before dispatch admission.
+ /// The copied result.
+ ///
+ /// This default implementation preserves compatibility for existing third-party implementations. Core replaces
+ /// it with constrained dispatch for generated value operations.
+ ///
+ public TResult Execute(TOperation operation, CancellationToken cancellationToken)
+ where TOperation : struct, ILuaOperation
+ {
+ return Execute(operation, cancellationToken);
+ }
+
+#pragma warning restore RS0026, RS0027
}
diff --git a/libs/CheatEngine.Client.Abstractions/Lua/ILuaModule.cs b/libs/CheatEngine.Client.Abstractions/Lua/ILuaModule.cs
index c50778d..ca04619 100644
--- a/libs/CheatEngine.Client.Abstractions/Lua/ILuaModule.cs
+++ b/libs/CheatEngine.Client.Abstractions/Lua/ILuaModule.cs
@@ -13,6 +13,11 @@ public interface ILuaModule
///
/// Throw when the generated SDK registration reports a non-success status so
/// can return the mapped failure.
+ ///
+ /// A manual module that implements only this interface remains a compatible advanced escape hatch. Because it
+ /// does not declare its identity or exports, it cannot participate in the Client's activation-wide global
+ /// collision guarantee. Implement for ordinary application modules.
+ ///
///
public void Register();
diff --git a/libs/CheatEngine.Client.Abstractions/Lua/ILuaResultMapper.cs b/libs/CheatEngine.Client.Abstractions/Lua/ILuaResultMapper.cs
new file mode 100644
index 0000000..6b185c5
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Lua/ILuaResultMapper.cs
@@ -0,0 +1,19 @@
+namespace CheatEngine.Client.Lua;
+
+///
+/// Projects one SDK-returned value into a copied Client result while a typed Lua operation is still active.
+///
+/// The SDK binding result type visible only inside generated operation code.
+/// The copied Client result type exposed by the operation.
+///
+/// Implementations must not retain their source value when it is backed by a Lua reference, CE object, owned
+/// wrapper, native pointer, span, or other activation-bound SDK resource. The generated operation invokes this
+/// member directly through static abstract interface dispatch; it never uses reflection.
+///
+public interface ILuaResultMapper
+{
+ /// Creates the Client-owned result before the Lua operation scope ends.
+ /// The value returned by the SDK binding.
+ /// A copied Client result.
+ public static abstract TResult Map(TSource source);
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Lua/LuaExportDescriptor.cs b/libs/CheatEngine.Client.Abstractions/Lua/LuaExportDescriptor.cs
new file mode 100644
index 0000000..3c7b5ac
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Lua/LuaExportDescriptor.cs
@@ -0,0 +1,19 @@
+namespace CheatEngine.Client.Lua;
+
+/// Copied metadata that identifies one Lua global exported by a Client module.
+public readonly record struct LuaExportDescriptor
+{
+ /// Initializes one validated Lua global descriptor.
+ /// The case-sensitive Lua global name.
+ public LuaExportDescriptor(string name)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(name);
+ Name = name;
+ }
+
+ /// Gets the case-sensitive Lua global name.
+ public string Name
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Lua/LuaModuleDescriptor.cs b/libs/CheatEngine.Client.Abstractions/Lua/LuaModuleDescriptor.cs
new file mode 100644
index 0000000..2d16ef6
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Lua/LuaModuleDescriptor.cs
@@ -0,0 +1,42 @@
+using System.Collections.Immutable;
+
+namespace CheatEngine.Client.Lua;
+
+/// Copied metadata that identifies one explicit Lua module and all global names it exports.
+public readonly record struct LuaModuleDescriptor
+{
+ /// Initializes one immutable module descriptor.
+ /// The stable module identity for the Client activation.
+ /// The Lua global names this module will register.
+ /// is blank or exports contain a blank or duplicate name.
+ public LuaModuleDescriptor(string name, ImmutableArray exports)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(name);
+ Name = name;
+ Exports = exports.IsDefault ? ImmutableArray.Empty : exports;
+
+ HashSet? names = Exports.IsEmpty ? null : new HashSet(StringComparer.Ordinal);
+ foreach (LuaExportDescriptor export in Exports)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(export.Name);
+ if (!names!.Add(export.Name))
+ {
+ throw new ArgumentException(
+ $"The Lua module '{name}' contains the export '{export.Name}' more than once.",
+ nameof(exports));
+ }
+ }
+ }
+
+ /// Gets the stable module identity for the Client activation.
+ public string Name
+ {
+ get;
+ }
+
+ /// Gets immutable copied descriptors of each Lua global this module exports.
+ public ImmutableArray Exports
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Memory/IMemoryClient.cs b/libs/CheatEngine.Client.Abstractions/Memory/IMemoryClient.cs
index 7e61307..d078d30 100644
--- a/libs/CheatEngine.Client.Abstractions/Memory/IMemoryClient.cs
+++ b/libs/CheatEngine.Client.Abstractions/Memory/IMemoryClient.cs
@@ -23,6 +23,22 @@ public bool TryWritePrimitive(Address address, T value, out CheatEngineFailur
/// Writes a built-in scalar or pointer type or throws when unsupported or rejected.
public void WritePrimitive(Address address, T value, CancellationToken cancellationToken = default);
+ /// Tries to read a bounded homogeneous batch of built-in scalars or pointers in one dispatch admission.
+ public bool TryReadPrimitiveBatch(MemoryPrimitiveBatchReadRequest request, out ImmutableArray values,
+ out CheatEngineFailure failure, CancellationToken cancellationToken = default);
+
+ /// Reads a bounded homogeneous batch of built-in scalars or pointers or throws on failure.
+ public ImmutableArray ReadPrimitiveBatch(MemoryPrimitiveBatchReadRequest request,
+ CancellationToken cancellationToken = default);
+
+ /// Tries to write a bounded homogeneous batch of built-in scalars or pointers in one dispatch admission.
+ public bool TryWritePrimitiveBatch(MemoryPrimitiveBatchWriteRequest request, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Writes a bounded homogeneous batch of built-in scalars or pointers or throws on failure.
+ public void WritePrimitiveBatch(MemoryPrimitiveBatchWriteRequest request,
+ CancellationToken cancellationToken = default);
+
/// Tries to copy an exact, caller-bounded byte range.
public bool TryReadBytes(MemoryBytesReadRequest request, out ImmutableArray bytes,
out CheatEngineFailure failure, CancellationToken cancellationToken = default);
diff --git a/libs/CheatEngine.Client.Abstractions/Memory/MemoryAddressValue.cs b/libs/CheatEngine.Client.Abstractions/Memory/MemoryAddressValue.cs
new file mode 100644
index 0000000..9d319b6
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Memory/MemoryAddressValue.cs
@@ -0,0 +1,29 @@
+using CheatEngine.SDK.Engine.Values;
+
+namespace CheatEngine.Client.Memory;
+
+/// Associates one copied managed value with its target address.
+/// The homogeneous scalar type written by a batch.
+public readonly struct MemoryAddressValue
+{
+ /// Creates one copied address/value pair.
+ /// The target address.
+ /// The managed value to write.
+ public MemoryAddressValue(Address address, T value)
+ {
+ Address = address;
+ Value = value;
+ }
+
+ /// Gets the target address.
+ public Address Address
+ {
+ get;
+ }
+
+ /// Gets the managed value to write.
+ public T Value
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Memory/MemoryBatchLimits.cs b/libs/CheatEngine.Client.Abstractions/Memory/MemoryBatchLimits.cs
new file mode 100644
index 0000000..1327380
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Memory/MemoryBatchLimits.cs
@@ -0,0 +1,12 @@
+namespace CheatEngine.Client.Memory;
+
+/// Defines the bounded sizes accepted by target-memory batch contracts.
+public static class MemoryBatchLimits
+{
+ /// Gets the largest number of homogeneous scalar operations accepted in one batch.
+ ///
+ /// The limit bounds request copying, result materialization and the amount of work admitted to one Cheat Engine
+ /// main-thread dispatch. It does not make the individual Lua globals atomic.
+ ///
+ public const int MaximumOperations = 1024;
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Memory/MemoryPrimitiveBatchReadRequest.cs b/libs/CheatEngine.Client.Abstractions/Memory/MemoryPrimitiveBatchReadRequest.cs
new file mode 100644
index 0000000..65fcb26
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Memory/MemoryPrimitiveBatchReadRequest.cs
@@ -0,0 +1,36 @@
+using System.Collections.Immutable;
+
+using CheatEngine.SDK.Engine.Values;
+
+namespace CheatEngine.Client.Memory;
+
+/// A copied, bounded request to read one homogeneous built-in scalar from multiple target addresses.
+/// The built-in scalar or target-aware pointer type to read.
+public readonly struct MemoryPrimitiveBatchReadRequest
+{
+ /// Creates a bounded batch by copying every target address.
+ /// The non-empty target addresses to read in order.
+ /// is empty.
+ /// exceeds the supported batch size.
+ public MemoryPrimitiveBatchReadRequest(ReadOnlySpan addresses)
+ {
+ if (addresses.IsEmpty)
+ {
+ throw new ArgumentException("A memory batch requires at least one address.", nameof(addresses));
+ }
+
+ if (addresses.Length > MemoryBatchLimits.MaximumOperations)
+ {
+ throw new ArgumentOutOfRangeException(nameof(addresses),
+ $"A memory batch is limited to {MemoryBatchLimits.MaximumOperations} operations.");
+ }
+
+ Addresses = ImmutableArray.Create(addresses.ToArray());
+ }
+
+ /// Gets the copied target addresses in result order.
+ public ImmutableArray Addresses
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Memory/MemoryPrimitiveBatchWriteRequest.cs b/libs/CheatEngine.Client.Abstractions/Memory/MemoryPrimitiveBatchWriteRequest.cs
new file mode 100644
index 0000000..5e8d6ec
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Memory/MemoryPrimitiveBatchWriteRequest.cs
@@ -0,0 +1,34 @@
+using System.Collections.Immutable;
+
+namespace CheatEngine.Client.Memory;
+
+/// A copied, bounded request to write one homogeneous built-in scalar to multiple target addresses.
+/// The built-in scalar or target-aware pointer type to write.
+public readonly struct MemoryPrimitiveBatchWriteRequest
+{
+ /// Creates a bounded batch by copying every address/value pair.
+ /// The non-empty target writes to perform in order.
+ /// is empty.
+ /// exceeds the supported batch size.
+ public MemoryPrimitiveBatchWriteRequest(ReadOnlySpan> values)
+ {
+ if (values.IsEmpty)
+ {
+ throw new ArgumentException("A memory batch requires at least one value.", nameof(values));
+ }
+
+ if (values.Length > MemoryBatchLimits.MaximumOperations)
+ {
+ throw new ArgumentOutOfRangeException(nameof(values),
+ $"A memory batch is limited to {MemoryBatchLimits.MaximumOperations} operations.");
+ }
+
+ Values = ImmutableArray.Create(values.ToArray());
+ }
+
+ /// Gets the copied address/value pairs in execution order.
+ public ImmutableArray> Values
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Memory/MemoryStringEncoding.cs b/libs/CheatEngine.Client.Abstractions/Memory/MemoryStringEncoding.cs
new file mode 100644
index 0000000..c3142d6
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Memory/MemoryStringEncoding.cs
@@ -0,0 +1,11 @@
+namespace CheatEngine.Client.Memory;
+
+/// Identifies the target representation used by a bounded string operation.
+public enum MemoryStringEncoding
+{
+ /// Uses the UTF-8 representation accepted by Cheat Engine's narrow string globals.
+ Utf8 = 0,
+
+ /// Uses the UTF-16 representation accepted by Cheat Engine's wide string globals.
+ Utf16 = 1
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Memory/MemoryStringReadRequest.cs b/libs/CheatEngine.Client.Abstractions/Memory/MemoryStringReadRequest.cs
index 4d9b66f..59b2361 100644
--- a/libs/CheatEngine.Client.Abstractions/Memory/MemoryStringReadRequest.cs
+++ b/libs/CheatEngine.Client.Abstractions/Memory/MemoryStringReadRequest.cs
@@ -31,4 +31,27 @@ public bool WideCharacter
{
get;
}
+
+ /// Gets the explicit UTF-8 or UTF-16 target representation.
+ public MemoryStringEncoding Encoding => WideCharacter ? MemoryStringEncoding.Utf16 : MemoryStringEncoding.Utf8;
+
+ /// Creates a bounded text read with an explicit target encoding.
+ /// The first target address.
+ /// The positive maximum length accepted by Cheat Engine.
+ /// The UTF-8 or UTF-16 target representation.
+ /// A request that preserves the supplied encoding choice.
+ public static MemoryStringReadRequest Create(Address address, int maximumLength, MemoryStringEncoding encoding)
+ {
+ return new MemoryStringReadRequest(address, maximumLength, ToWideCharacter(encoding));
+ }
+
+ private static bool ToWideCharacter(MemoryStringEncoding encoding)
+ {
+ return encoding switch
+ {
+ MemoryStringEncoding.Utf8 => false,
+ MemoryStringEncoding.Utf16 => true,
+ _ => throw new ArgumentOutOfRangeException(nameof(encoding))
+ };
+ }
}
diff --git a/libs/CheatEngine.Client.Abstractions/Memory/MemoryStringWriteRequest.cs b/libs/CheatEngine.Client.Abstractions/Memory/MemoryStringWriteRequest.cs
index c7240a9..6e3bdf5 100644
--- a/libs/CheatEngine.Client.Abstractions/Memory/MemoryStringWriteRequest.cs
+++ b/libs/CheatEngine.Client.Abstractions/Memory/MemoryStringWriteRequest.cs
@@ -11,6 +11,22 @@ public MemoryStringWriteRequest(Address address, string value, bool wideCharacte
ArgumentNullException.ThrowIfNull(value);
Address = address;
Value = value;
+ MaximumLength = 0;
+ WideCharacter = wideCharacter;
+ }
+
+ private MemoryStringWriteRequest(Address address, string value, int maximumLength, bool wideCharacter)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maximumLength);
+ if (GetEncodedLength(value, wideCharacter) > maximumLength)
+ {
+ throw new ArgumentException("The encoded text exceeds the explicit maximum length.", nameof(value));
+ }
+
+ Address = address;
+ Value = value;
+ MaximumLength = maximumLength;
WideCharacter = wideCharacter;
}
@@ -26,9 +42,45 @@ public string Value
get;
}
+ /// Gets the explicit encoded-length bound, or zero for the legacy unbounded constructor.
+ public int MaximumLength
+ {
+ get;
+ }
+
/// Gets whether Cheat Engine should write UTF-16 target text.
public bool WideCharacter
{
get;
}
+
+ /// Gets the explicit UTF-8 or UTF-16 target representation.
+ public MemoryStringEncoding Encoding => WideCharacter ? MemoryStringEncoding.Utf16 : MemoryStringEncoding.Utf8;
+
+ /// Creates a text write with an explicit maximum encoded length and target encoding.
+ /// The first target address.
+ /// The managed text to copy.
+ /// The positive maximum number of UTF-8 bytes or UTF-16 code units accepted.
+ /// The UTF-8 or UTF-16 target representation.
+ /// A request whose encoded text is bounded before it reaches Cheat Engine.
+ public static MemoryStringWriteRequest CreateBounded(Address address, string value, int maximumLength,
+ MemoryStringEncoding encoding)
+ {
+ return new MemoryStringWriteRequest(address, value, maximumLength, ToWideCharacter(encoding));
+ }
+
+ private static bool ToWideCharacter(MemoryStringEncoding encoding)
+ {
+ return encoding switch
+ {
+ MemoryStringEncoding.Utf8 => false,
+ MemoryStringEncoding.Utf16 => true,
+ _ => throw new ArgumentOutOfRangeException(nameof(encoding))
+ };
+ }
+
+ private static int GetEncodedLength(string value, bool wideCharacter)
+ {
+ return wideCharacter ? value.Length : System.Text.Encoding.UTF8.GetByteCount(value);
+ }
}
diff --git a/libs/CheatEngine.Client.Abstractions/Processes/IProcessClient.cs b/libs/CheatEngine.Client.Abstractions/Processes/IProcessClient.cs
index efa5018..f90e41c 100644
--- a/libs/CheatEngine.Client.Abstractions/Processes/IProcessClient.cs
+++ b/libs/CheatEngine.Client.Abstractions/Processes/IProcessClient.cs
@@ -6,6 +6,14 @@ namespace CheatEngine.Client.Processes;
/// Reads the process selected by the active Cheat Engine session.
public interface IProcessClient
{
+ /// Tries to enumerate copied local-process metadata within an explicit materialization bound.
+ public bool TryGetProcesses(ProcessEnumerationRequest request, out ProcessEnumerationResult result,
+ out CheatEngineFailure failure, CancellationToken cancellationToken = default);
+
+ /// Enumerates copied local-process metadata within an explicit materialization bound.
+ public ProcessEnumerationResult GetProcesses(ProcessEnumerationRequest request,
+ CancellationToken cancellationToken = default);
+
/// Tries to get a copied snapshot of the currently selected target process.
public bool TryGetCurrent(out ProcessSnapshot snapshot, out CheatEngineFailure failure,
CancellationToken cancellationToken = default);
@@ -37,4 +45,41 @@ public bool TryAttachExactName(string processName, out ProcessSnapshot snapshot,
/// Attaches to the single exact-name match or throws when none or several exist.
public ProcessSnapshot AttachExactName(string processName, CancellationToken cancellationToken = default);
+
+ /// Tries to attach Cheat Engine to the foreground process when the host exposes a validated binding.
+ public bool TryAttachForeground(out ProcessSnapshot snapshot, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Attaches Cheat Engine to the foreground process when the host exposes a validated binding.
+ public ProcessSnapshot AttachForeground(CancellationToken cancellationToken = default);
+
+ ///
+ /// Tries to create and select an explicit process when the host exposes a validated create-and-attach binding.
+ ///
+ public bool TryCreate(ProcessStartRequest request, out ProcessSnapshot snapshot, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Creates and selects an explicit process when the host exposes a validated create-and-attach binding.
+ public ProcessSnapshot Create(ProcessStartRequest request, CancellationToken cancellationToken = default);
+
+ /// Tries to pause the selected target when the host exposes a validated pause binding.
+ public bool TryPause(out ProcessSnapshot snapshot, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Pauses the selected target when the host exposes a validated pause binding.
+ public ProcessSnapshot Pause(CancellationToken cancellationToken = default);
+
+ /// Tries to resume the selected target when the host exposes a validated resume binding.
+ public bool TryResumeExecution(out ProcessSnapshot snapshot, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Resumes the selected target when the host exposes a validated resume binding.
+ public ProcessSnapshot ResumeExecution(CancellationToken cancellationToken = default);
+
+ /// Tries to observe the selected target's pause state when the host exposes a validated binding.
+ public bool TryGetPauseState(out ProcessPauseSnapshot snapshot, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Observes the selected target's pause state when the host exposes a validated binding.
+ public ProcessPauseSnapshot GetPauseState(CancellationToken cancellationToken = default);
}
diff --git a/libs/CheatEngine.Client.Abstractions/Processes/ProcessEnumerationRequest.cs b/libs/CheatEngine.Client.Abstractions/Processes/ProcessEnumerationRequest.cs
new file mode 100644
index 0000000..189c08e
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Processes/ProcessEnumerationRequest.cs
@@ -0,0 +1,30 @@
+namespace CheatEngine.Client.Processes;
+
+/// Defines a bounded, copied local-process enumeration.
+public readonly record struct ProcessEnumerationRequest
+{
+ /// Creates a bounded process enumeration request.
+ public ProcessEnumerationRequest(int maximumItems, string? nameContains = null)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maximumItems);
+ if (nameContains is { Length: 0 })
+ {
+ throw new ArgumentException("A process-name filter must be null or non-empty.", nameof(nameContains));
+ }
+
+ MaximumItems = maximumItems;
+ NameContains = nameContains;
+ }
+
+ /// Gets the maximum number of copied process records the caller permits.
+ public int MaximumItems
+ {
+ get;
+ }
+
+ /// Gets the optional case-insensitive substring applied to process names.
+ public string? NameContains
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Processes/ProcessEnumerationResult.cs b/libs/CheatEngine.Client.Abstractions/Processes/ProcessEnumerationResult.cs
new file mode 100644
index 0000000..5dc7a5f
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Processes/ProcessEnumerationResult.cs
@@ -0,0 +1,32 @@
+using System.Collections.Immutable;
+
+namespace CheatEngine.Client.Processes;
+
+/// A copied, bounded local-process enumeration result.
+public readonly record struct ProcessEnumerationResult
+{
+ /// Creates a copied process enumeration result.
+ public ProcessEnumerationResult(ImmutableArray processes, bool isTruncated)
+ {
+ Processes = processes.IsDefault ? ImmutableArray.Empty : processes;
+ if (isTruncated && Processes.IsEmpty)
+ {
+ throw new ArgumentException("A truncated process result must retain at least one copied process.",
+ nameof(processes));
+ }
+
+ IsTruncated = isTruncated;
+ }
+
+ /// Gets copied local-process metadata, ordered by process identifier.
+ public ImmutableArray Processes
+ {
+ get;
+ }
+
+ /// Gets whether matching local processes were omitted at the caller's explicit limit.
+ public bool IsTruncated
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Processes/ProcessInfoSnapshot.cs b/libs/CheatEngine.Client.Abstractions/Processes/ProcessInfoSnapshot.cs
new file mode 100644
index 0000000..b8cb42c
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Processes/ProcessInfoSnapshot.cs
@@ -0,0 +1,43 @@
+using CheatEngine.SDK.Engine.Inspection;
+
+namespace CheatEngine.Client.Processes;
+
+/// Copied local-process metadata that is independent of Cheat Engine's selected target.
+public readonly record struct ProcessInfoSnapshot
+{
+ /// Creates copied local-process metadata.
+ public ProcessInfoSnapshot(TargetProcessId id, string? name, string? executablePath)
+ {
+ if (name is { Length: 0 })
+ {
+ throw new ArgumentException("A process name must be null or non-empty.", nameof(name));
+ }
+
+ if (executablePath is { Length: 0 })
+ {
+ throw new ArgumentException("An executable path must be null or non-empty.", nameof(executablePath));
+ }
+
+ Id = id;
+ Name = name;
+ ExecutablePath = executablePath;
+ }
+
+ /// Gets the local process identifier.
+ public TargetProcessId Id
+ {
+ get;
+ }
+
+ /// Gets the display name when managed metadata could be read.
+ public string? Name
+ {
+ get;
+ }
+
+ /// Gets the executable path when managed metadata could be read.
+ public string? ExecutablePath
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Processes/ProcessPauseSnapshot.cs b/libs/CheatEngine.Client.Abstractions/Processes/ProcessPauseSnapshot.cs
new file mode 100644
index 0000000..a039637
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Processes/ProcessPauseSnapshot.cs
@@ -0,0 +1,34 @@
+using CheatEngine.SDK.Engine.Inspection;
+
+namespace CheatEngine.Client.Processes;
+
+/// A copied pause-state observation tied to one selected-target epoch.
+public readonly record struct ProcessPauseSnapshot
+{
+ /// Creates a target pause-state observation.
+ public ProcessPauseSnapshot(TargetProcessId processId, ProcessPauseState state, long selectionEpoch)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(selectionEpoch);
+ ProcessId = processId;
+ State = state;
+ SelectionEpoch = selectionEpoch;
+ }
+
+ /// Gets the selected process identifier observed with this state.
+ public TargetProcessId ProcessId
+ {
+ get;
+ }
+
+ /// Gets the observed pause state.
+ public ProcessPauseState State
+ {
+ get;
+ }
+
+ /// Gets the target-selection epoch associated with the observation.
+ public long SelectionEpoch
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Processes/ProcessPauseState.cs b/libs/CheatEngine.Client.Abstractions/Processes/ProcessPauseState.cs
new file mode 100644
index 0000000..ca611fc
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Processes/ProcessPauseState.cs
@@ -0,0 +1,14 @@
+namespace CheatEngine.Client.Processes;
+
+/// Describes whether a selected process is paused.
+public enum ProcessPauseState
+{
+ /// The host could not establish the state.
+ Unknown = 0,
+
+ /// The target is executing.
+ Running = 1,
+
+ /// The target is paused.
+ Paused = 2
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Processes/ProcessStartRequest.cs b/libs/CheatEngine.Client.Abstractions/Processes/ProcessStartRequest.cs
new file mode 100644
index 0000000..3dd5356
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Processes/ProcessStartRequest.cs
@@ -0,0 +1,43 @@
+namespace CheatEngine.Client.Processes;
+
+/// Describes an explicit executable launch that must also become the selected Cheat Engine target.
+public readonly record struct ProcessStartRequest
+{
+ /// Creates an explicit process-launch request.
+ public ProcessStartRequest(string executablePath, string? arguments = null, string? workingDirectory = null)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(executablePath);
+ if (!Path.IsPathFullyQualified(executablePath))
+ {
+ throw new ArgumentException("The executable path must be absolute.", nameof(executablePath));
+ }
+
+ if (workingDirectory is { } directory && !Path.IsPathFullyQualified(directory))
+ {
+ throw new ArgumentException("The working directory must be absolute when specified.",
+ nameof(workingDirectory));
+ }
+
+ ExecutablePath = executablePath;
+ Arguments = arguments;
+ WorkingDirectory = workingDirectory;
+ }
+
+ /// Gets the absolute executable path.
+ public string ExecutablePath
+ {
+ get;
+ }
+
+ /// Gets the optional command-line arguments.
+ public string? Arguments
+ {
+ get;
+ }
+
+ /// Gets the optional absolute working directory.
+ public string? WorkingDirectory
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/PublicAPI.Unshipped.txt b/libs/CheatEngine.Client.Abstractions/PublicAPI.Unshipped.txt
index 7dc5c58..5fc66a3 100644
--- a/libs/CheatEngine.Client.Abstractions/PublicAPI.Unshipped.txt
+++ b/libs/CheatEngine.Client.Abstractions/PublicAPI.Unshipped.txt
@@ -1 +1,529 @@
#nullable enable
+CheatEngine.Client.ICheatEngineClient.Allocations.get -> CheatEngine.Client.Allocations.IAllocationClient!
+CheatEngine.Client.ICheatEngineClient.Assembly.get -> CheatEngine.Client.Assembly.IAssemblyClient!
+CheatEngine.Client.ICheatEngineClient.Dbvm.get -> CheatEngine.Client.Dbvm.IDbvmClient!
+CheatEngine.Client.ICheatEngineClient.Debugger.get -> CheatEngine.Client.Debugger.IDebuggerClient!
+CheatEngine.Client.ICheatEngineClient.Hashing.get -> CheatEngine.Client.Hashing.IHashingClient!
+CheatEngine.Client.ICheatEngineClient.Hotkeys.get -> CheatEngine.Client.Hotkeys.IHotkeyClient!
+CheatEngine.Client.ICheatEngineClient.RemoteExecution.get -> CheatEngine.Client.RemoteExecution.IRemoteExecutionClient!
+CheatEngine.Client.ICheatEngineClient.Speed.get -> CheatEngine.Client.Speed.ISpeedClient!
+CheatEngine.Client.ICheatEngineClient.Timers.get -> CheatEngine.Client.Timers.ITimerClient!
+static CheatEngine.Client.Runtime.ClientCapabilityId.Allocations.get -> CheatEngine.Client.Runtime.ClientCapabilityId
+static CheatEngine.Client.Runtime.ClientCapabilityId.Assembly.get -> CheatEngine.Client.Runtime.ClientCapabilityId
+static CheatEngine.Client.Runtime.ClientCapabilityId.Dbvm.get -> CheatEngine.Client.Runtime.ClientCapabilityId
+static CheatEngine.Client.Runtime.ClientCapabilityId.Debugger.get -> CheatEngine.Client.Runtime.ClientCapabilityId
+static CheatEngine.Client.Runtime.ClientCapabilityId.Hashing.get -> CheatEngine.Client.Runtime.ClientCapabilityId
+static CheatEngine.Client.Runtime.ClientCapabilityId.Hotkeys.get -> CheatEngine.Client.Runtime.ClientCapabilityId
+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.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
+CheatEngine.Client.Processes.IProcessClient.GetProcesses(CheatEngine.Client.Processes.ProcessEnumerationRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Processes.ProcessEnumerationResult
+CheatEngine.Client.Processes.IProcessClient.Pause(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Processes.ProcessSnapshot
+CheatEngine.Client.Processes.IProcessClient.ResumeExecution(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Processes.ProcessSnapshot
+CheatEngine.Client.Processes.IProcessClient.TryAttachForeground(out CheatEngine.Client.Processes.ProcessSnapshot snapshot, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Processes.IProcessClient.TryCreate(CheatEngine.Client.Processes.ProcessStartRequest request, out CheatEngine.Client.Processes.ProcessSnapshot snapshot, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Processes.IProcessClient.TryGetPauseState(out CheatEngine.Client.Processes.ProcessPauseSnapshot snapshot, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Processes.IProcessClient.TryGetProcesses(CheatEngine.Client.Processes.ProcessEnumerationRequest request, out CheatEngine.Client.Processes.ProcessEnumerationResult result, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Processes.IProcessClient.TryPause(out CheatEngine.Client.Processes.ProcessSnapshot snapshot, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Processes.IProcessClient.TryResumeExecution(out CheatEngine.Client.Processes.ProcessSnapshot snapshot, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Processes.ProcessEnumerationRequest
+CheatEngine.Client.Processes.ProcessEnumerationRequest.Equals(CheatEngine.Client.Processes.ProcessEnumerationRequest other) -> bool
+CheatEngine.Client.Processes.ProcessEnumerationRequest.MaximumItems.get -> int
+CheatEngine.Client.Processes.ProcessEnumerationRequest.NameContains.get -> string?
+CheatEngine.Client.Processes.ProcessEnumerationRequest.ProcessEnumerationRequest() -> void
+CheatEngine.Client.Processes.ProcessEnumerationRequest.ProcessEnumerationRequest(int maximumItems, string? nameContains = null) -> void
+~override CheatEngine.Client.Processes.ProcessEnumerationRequest.Equals(object obj) -> bool
+override CheatEngine.Client.Processes.ProcessEnumerationRequest.GetHashCode() -> int
+~override CheatEngine.Client.Processes.ProcessEnumerationRequest.ToString() -> string
+static CheatEngine.Client.Processes.ProcessEnumerationRequest.operator !=(CheatEngine.Client.Processes.ProcessEnumerationRequest left, CheatEngine.Client.Processes.ProcessEnumerationRequest right) -> bool
+static CheatEngine.Client.Processes.ProcessEnumerationRequest.operator ==(CheatEngine.Client.Processes.ProcessEnumerationRequest left, CheatEngine.Client.Processes.ProcessEnumerationRequest right) -> bool
+CheatEngine.Client.Processes.ProcessEnumerationResult
+CheatEngine.Client.Processes.ProcessEnumerationResult.Equals(CheatEngine.Client.Processes.ProcessEnumerationResult other) -> bool
+CheatEngine.Client.Processes.ProcessEnumerationResult.IsTruncated.get -> bool
+CheatEngine.Client.Processes.ProcessEnumerationResult.ProcessEnumerationResult() -> void
+CheatEngine.Client.Processes.ProcessEnumerationResult.ProcessEnumerationResult(System.Collections.Immutable.ImmutableArray processes, bool isTruncated) -> void
+CheatEngine.Client.Processes.ProcessEnumerationResult.Processes.get -> System.Collections.Immutable.ImmutableArray
+~override CheatEngine.Client.Processes.ProcessEnumerationResult.Equals(object obj) -> bool
+override CheatEngine.Client.Processes.ProcessEnumerationResult.GetHashCode() -> int
+~override CheatEngine.Client.Processes.ProcessEnumerationResult.ToString() -> string
+static CheatEngine.Client.Processes.ProcessEnumerationResult.operator !=(CheatEngine.Client.Processes.ProcessEnumerationResult left, CheatEngine.Client.Processes.ProcessEnumerationResult right) -> bool
+static CheatEngine.Client.Processes.ProcessEnumerationResult.operator ==(CheatEngine.Client.Processes.ProcessEnumerationResult left, CheatEngine.Client.Processes.ProcessEnumerationResult right) -> bool
+CheatEngine.Client.Processes.ProcessInfoSnapshot
+CheatEngine.Client.Processes.ProcessInfoSnapshot.Equals(CheatEngine.Client.Processes.ProcessInfoSnapshot other) -> bool
+CheatEngine.Client.Processes.ProcessInfoSnapshot.ExecutablePath.get -> string?
+CheatEngine.Client.Processes.ProcessInfoSnapshot.Id.get -> CheatEngine.SDK.Engine.Inspection.TargetProcessId
+CheatEngine.Client.Processes.ProcessInfoSnapshot.Name.get -> string?
+CheatEngine.Client.Processes.ProcessInfoSnapshot.ProcessInfoSnapshot() -> void
+CheatEngine.Client.Processes.ProcessInfoSnapshot.ProcessInfoSnapshot(CheatEngine.SDK.Engine.Inspection.TargetProcessId id, string? name, string? executablePath) -> void
+~override CheatEngine.Client.Processes.ProcessInfoSnapshot.Equals(object obj) -> bool
+override CheatEngine.Client.Processes.ProcessInfoSnapshot.GetHashCode() -> int
+~override CheatEngine.Client.Processes.ProcessInfoSnapshot.ToString() -> string
+static CheatEngine.Client.Processes.ProcessInfoSnapshot.operator !=(CheatEngine.Client.Processes.ProcessInfoSnapshot left, CheatEngine.Client.Processes.ProcessInfoSnapshot right) -> bool
+static CheatEngine.Client.Processes.ProcessInfoSnapshot.operator ==(CheatEngine.Client.Processes.ProcessInfoSnapshot left, CheatEngine.Client.Processes.ProcessInfoSnapshot right) -> bool
+CheatEngine.Client.Processes.ProcessPauseSnapshot
+CheatEngine.Client.Processes.ProcessPauseSnapshot.Equals(CheatEngine.Client.Processes.ProcessPauseSnapshot other) -> bool
+CheatEngine.Client.Processes.ProcessPauseSnapshot.ProcessId.get -> CheatEngine.SDK.Engine.Inspection.TargetProcessId
+CheatEngine.Client.Processes.ProcessPauseSnapshot.ProcessPauseSnapshot() -> void
+CheatEngine.Client.Processes.ProcessPauseSnapshot.ProcessPauseSnapshot(CheatEngine.SDK.Engine.Inspection.TargetProcessId processId, CheatEngine.Client.Processes.ProcessPauseState state, long selectionEpoch) -> void
+CheatEngine.Client.Processes.ProcessPauseSnapshot.SelectionEpoch.get -> long
+CheatEngine.Client.Processes.ProcessPauseSnapshot.State.get -> CheatEngine.Client.Processes.ProcessPauseState
+~override CheatEngine.Client.Processes.ProcessPauseSnapshot.Equals(object obj) -> bool
+override CheatEngine.Client.Processes.ProcessPauseSnapshot.GetHashCode() -> int
+~override CheatEngine.Client.Processes.ProcessPauseSnapshot.ToString() -> string
+static CheatEngine.Client.Processes.ProcessPauseSnapshot.operator !=(CheatEngine.Client.Processes.ProcessPauseSnapshot left, CheatEngine.Client.Processes.ProcessPauseSnapshot right) -> bool
+static CheatEngine.Client.Processes.ProcessPauseSnapshot.operator ==(CheatEngine.Client.Processes.ProcessPauseSnapshot left, CheatEngine.Client.Processes.ProcessPauseSnapshot right) -> bool
+CheatEngine.Client.Processes.ProcessPauseState
+CheatEngine.Client.Processes.ProcessPauseState.Paused = 2 -> CheatEngine.Client.Processes.ProcessPauseState
+CheatEngine.Client.Processes.ProcessPauseState.Running = 1 -> CheatEngine.Client.Processes.ProcessPauseState
+CheatEngine.Client.Processes.ProcessPauseState.Unknown = 0 -> CheatEngine.Client.Processes.ProcessPauseState
+CheatEngine.Client.Processes.ProcessStartRequest
+CheatEngine.Client.Processes.ProcessStartRequest.Arguments.get -> string?
+CheatEngine.Client.Processes.ProcessStartRequest.Equals(CheatEngine.Client.Processes.ProcessStartRequest other) -> bool
+CheatEngine.Client.Processes.ProcessStartRequest.ExecutablePath.get -> string!
+CheatEngine.Client.Processes.ProcessStartRequest.ProcessStartRequest() -> void
+CheatEngine.Client.Processes.ProcessStartRequest.ProcessStartRequest(string! executablePath, string? arguments = null, string? workingDirectory = null) -> void
+CheatEngine.Client.Processes.ProcessStartRequest.WorkingDirectory.get -> string?
+~override CheatEngine.Client.Processes.ProcessStartRequest.Equals(object obj) -> bool
+override CheatEngine.Client.Processes.ProcessStartRequest.GetHashCode() -> int
+~override CheatEngine.Client.Processes.ProcessStartRequest.ToString() -> string
+static CheatEngine.Client.Processes.ProcessStartRequest.operator !=(CheatEngine.Client.Processes.ProcessStartRequest left, CheatEngine.Client.Processes.ProcessStartRequest right) -> bool
+static CheatEngine.Client.Processes.ProcessStartRequest.operator ==(CheatEngine.Client.Processes.ProcessStartRequest left, CheatEngine.Client.Processes.ProcessStartRequest right) -> bool
+CheatEngine.Client.Lua.CheatEngineLuaModuleAttribute
+CheatEngine.Client.Lua.CheatEngineLuaModuleAttribute.BindingsType.get -> System.Type!
+CheatEngine.Client.Lua.CheatEngineLuaModuleAttribute.CheatEngineLuaModuleAttribute(System.Type! bindingsType, string? name = null) -> void
+CheatEngine.Client.Lua.CheatEngineLuaModuleAttribute.Name.get -> string?
+CheatEngine.Client.Lua.CheatEngineLuaOperationAttribute
+CheatEngine.Client.Lua.CheatEngineLuaOperationAttribute.CheatEngineLuaOperationAttribute() -> void
+CheatEngine.Client.Lua.CheatEngineLuaOperationAttribute.CheatEngineLuaOperationAttribute(System.Type! mapperType) -> void
+CheatEngine.Client.Lua.CheatEngineLuaOperationAttribute.MapperType.get -> System.Type?
+CheatEngine.Client.Lua.IDescribedLuaModule
+CheatEngine.Client.Lua.IDescribedLuaModule.Descriptor.get -> CheatEngine.Client.Lua.LuaModuleDescriptor
+CheatEngine.Client.Lua.ILuaClient.Execute(TOperation operation, System.Threading.CancellationToken cancellationToken) -> TResult
+CheatEngine.Client.Lua.ILuaClient.TryExecute(TOperation operation, out TResult result, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken) -> bool
+CheatEngine.Client.Lua.ILuaResultMapper
+CheatEngine.Client.Lua.ILuaResultMapper.Map(TSource source) -> TResult
+CheatEngine.Client.Lua.LuaExportDescriptor
+CheatEngine.Client.Lua.LuaExportDescriptor.Equals(CheatEngine.Client.Lua.LuaExportDescriptor other) -> bool
+CheatEngine.Client.Lua.LuaExportDescriptor.LuaExportDescriptor() -> void
+CheatEngine.Client.Lua.LuaExportDescriptor.LuaExportDescriptor(string! name) -> void
+CheatEngine.Client.Lua.LuaExportDescriptor.Name.get -> string!
+CheatEngine.Client.Lua.LuaModuleDescriptor
+CheatEngine.Client.Lua.LuaModuleDescriptor.Equals(CheatEngine.Client.Lua.LuaModuleDescriptor other) -> bool
+CheatEngine.Client.Lua.LuaModuleDescriptor.Exports.get -> System.Collections.Immutable.ImmutableArray
+CheatEngine.Client.Lua.LuaModuleDescriptor.LuaModuleDescriptor() -> void
+CheatEngine.Client.Lua.LuaModuleDescriptor.LuaModuleDescriptor(string! name, System.Collections.Immutable.ImmutableArray exports) -> void
+CheatEngine.Client.Lua.LuaModuleDescriptor.Name.get -> string!
+~override CheatEngine.Client.Lua.LuaExportDescriptor.Equals(object obj) -> bool
+~override CheatEngine.Client.Lua.LuaExportDescriptor.ToString() -> string
+~override CheatEngine.Client.Lua.LuaModuleDescriptor.Equals(object obj) -> bool
+~override CheatEngine.Client.Lua.LuaModuleDescriptor.ToString() -> string
+override CheatEngine.Client.Lua.LuaExportDescriptor.GetHashCode() -> int
+override CheatEngine.Client.Lua.LuaModuleDescriptor.GetHashCode() -> int
+static CheatEngine.Client.Lua.LuaExportDescriptor.operator !=(CheatEngine.Client.Lua.LuaExportDescriptor left, CheatEngine.Client.Lua.LuaExportDescriptor right) -> bool
+static CheatEngine.Client.Lua.LuaExportDescriptor.operator ==(CheatEngine.Client.Lua.LuaExportDescriptor left, CheatEngine.Client.Lua.LuaExportDescriptor right) -> bool
+static CheatEngine.Client.Lua.LuaModuleDescriptor.operator !=(CheatEngine.Client.Lua.LuaModuleDescriptor left, CheatEngine.Client.Lua.LuaModuleDescriptor right) -> bool
+CheatEngine.Client.Allocations.IAllocationClient
+CheatEngine.Client.Allocations.IAllocationClient.Allocate(CheatEngine.Client.Allocations.TargetAllocationRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Allocations.ITargetMemoryLease!
+CheatEngine.Client.Allocations.IAllocationClient.TryAllocate(CheatEngine.Client.Allocations.TargetAllocationRequest request, out CheatEngine.Client.Allocations.ITargetMemoryLease? lease, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Allocations.ITargetMemoryLease
+CheatEngine.Client.Allocations.ITargetMemoryLease.Address.get -> CheatEngine.SDK.Engine.Values.Address
+CheatEngine.Client.Allocations.ITargetMemoryLease.IsReleased.get -> bool
+CheatEngine.Client.Allocations.ITargetMemoryLease.SelectionEpoch.get -> long
+CheatEngine.Client.Allocations.ITargetMemoryLease.Size.get -> long
+CheatEngine.Client.Allocations.TargetAllocationAccess
+CheatEngine.Client.Allocations.TargetAllocationAccess.ExecuteReadWrite = 1 -> CheatEngine.Client.Allocations.TargetAllocationAccess
+CheatEngine.Client.Allocations.TargetAllocationAccess.ReadWrite = 0 -> CheatEngine.Client.Allocations.TargetAllocationAccess
+CheatEngine.Client.Allocations.TargetAllocationRequest
+CheatEngine.Client.Allocations.TargetAllocationRequest.Access.get -> CheatEngine.Client.Allocations.TargetAllocationAccess
+CheatEngine.Client.Allocations.TargetAllocationRequest.Equals(CheatEngine.Client.Allocations.TargetAllocationRequest other) -> bool
+CheatEngine.Client.Allocations.TargetAllocationRequest.Size.get -> long
+CheatEngine.Client.Allocations.TargetAllocationRequest.TargetAllocationRequest() -> void
+CheatEngine.Client.Allocations.TargetAllocationRequest.TargetAllocationRequest(long size, CheatEngine.Client.Allocations.TargetAllocationAccess access = CheatEngine.Client.Allocations.TargetAllocationAccess.ReadWrite) -> void
+CheatEngine.Client.Assembly.AssemblyInstructionRequest
+CheatEngine.Client.Assembly.AssemblyInstructionRequest.Address.get -> CheatEngine.SDK.Engine.Values.Address
+CheatEngine.Client.Assembly.AssemblyInstructionRequest.AssemblyInstructionRequest() -> void
+CheatEngine.Client.Assembly.AssemblyInstructionRequest.AssemblyInstructionRequest(CheatEngine.SDK.Engine.Values.Address address, string! instruction) -> void
+CheatEngine.Client.Assembly.AssemblyInstructionRequest.Equals(CheatEngine.Client.Assembly.AssemblyInstructionRequest other) -> bool
+CheatEngine.Client.Assembly.AssemblyInstructionRequest.Instruction.get -> string!
+CheatEngine.Client.Assembly.AssemblyInstructionSnapshot
+CheatEngine.Client.Assembly.AssemblyInstructionSnapshot.Address.get -> CheatEngine.SDK.Engine.Values.Address
+CheatEngine.Client.Assembly.AssemblyInstructionSnapshot.AssemblyInstructionSnapshot() -> void
+CheatEngine.Client.Assembly.AssemblyInstructionSnapshot.AssemblyInstructionSnapshot(CheatEngine.SDK.Engine.Values.Address address, int length, string! text, System.ReadOnlySpan bytes) -> void
+CheatEngine.Client.Assembly.AssemblyInstructionSnapshot.Bytes.get -> System.Collections.Immutable.ImmutableArray
+CheatEngine.Client.Assembly.AssemblyInstructionSnapshot.Equals(CheatEngine.Client.Assembly.AssemblyInstructionSnapshot other) -> bool
+CheatEngine.Client.Assembly.AssemblyInstructionSnapshot.Length.get -> int
+CheatEngine.Client.Assembly.AssemblyInstructionSnapshot.Text.get -> string!
+CheatEngine.Client.Assembly.AutoAssemblerScript
+CheatEngine.Client.Assembly.AutoAssemblerScript.AutoAssemblerScript() -> void
+CheatEngine.Client.Assembly.AutoAssemblerScript.AutoAssemblerScript(string! source, string? name = null) -> void
+CheatEngine.Client.Assembly.AutoAssemblerScript.Equals(CheatEngine.Client.Assembly.AutoAssemblerScript other) -> bool
+CheatEngine.Client.Assembly.AutoAssemblerScript.Name.get -> string?
+CheatEngine.Client.Assembly.AutoAssemblerScript.Source.get -> string!
+CheatEngine.Client.Assembly.IAssemblyClient
+CheatEngine.Client.Assembly.IAssemblyClient.ApplyPatch(CheatEngine.Client.Assembly.AutoAssemblerScript script, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Assembly.IAutoAssemblerPatchLease!
+CheatEngine.Client.Assembly.IAssemblyClient.Assemble(CheatEngine.Client.Assembly.AssemblyInstructionRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Collections.Immutable.ImmutableArray
+CheatEngine.Client.Assembly.IAssemblyClient.Disassemble(CheatEngine.SDK.Engine.Values.Address address, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Assembly.AssemblyInstructionSnapshot
+CheatEngine.Client.Assembly.IAssemblyClient.GetComment(CheatEngine.SDK.Engine.Values.Address address, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> string!
+CheatEngine.Client.Assembly.IAssemblyClient.GetInstructionSize(CheatEngine.SDK.Engine.Values.Address address, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> int
+CheatEngine.Client.Assembly.IAssemblyClient.GetPreviousInstruction(CheatEngine.SDK.Engine.Values.Address address, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.SDK.Engine.Values.Address
+CheatEngine.Client.Assembly.IAssemblyClient.TryApplyPatch(CheatEngine.Client.Assembly.AutoAssemblerScript script, out CheatEngine.Client.Assembly.IAutoAssemblerPatchLease? lease, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Assembly.IAssemblyClient.TryAssemble(CheatEngine.Client.Assembly.AssemblyInstructionRequest request, out System.Collections.Immutable.ImmutableArray bytes, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Assembly.IAssemblyClient.TryDisassemble(CheatEngine.SDK.Engine.Values.Address address, out CheatEngine.Client.Assembly.AssemblyInstructionSnapshot instruction, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Assembly.IAssemblyClient.TryGetComment(CheatEngine.SDK.Engine.Values.Address address, out string? comment, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Assembly.IAssemblyClient.TryGetInstructionSize(CheatEngine.SDK.Engine.Values.Address address, out int size, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Assembly.IAssemblyClient.TryGetPreviousInstruction(CheatEngine.SDK.Engine.Values.Address address, out CheatEngine.SDK.Engine.Values.Address previousAddress, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Assembly.IAutoAssemblerPatchLease
+CheatEngine.Client.Assembly.IAutoAssemblerPatchLease.IsReleased.get -> bool
+CheatEngine.Client.Assembly.IAutoAssemblerPatchLease.Name.get -> string?
+CheatEngine.Client.Assembly.IAutoAssemblerPatchLease.SelectionEpoch.get -> long
+CheatEngine.Client.Dbvm.DbvmInitializationRequest
+CheatEngine.Client.Dbvm.DbvmInitializationRequest.DbvmInitializationRequest() -> void
+CheatEngine.Client.Dbvm.DbvmInitializationRequest.DbvmInitializationRequest(bool useStealthMode = false) -> void
+CheatEngine.Client.Dbvm.DbvmInitializationRequest.Equals(CheatEngine.Client.Dbvm.DbvmInitializationRequest other) -> bool
+CheatEngine.Client.Dbvm.DbvmInitializationRequest.UseStealthMode.get -> bool
+CheatEngine.Client.Dbvm.DbvmState
+CheatEngine.Client.Dbvm.DbvmState.Initialized = 3 -> CheatEngine.Client.Dbvm.DbvmState
+CheatEngine.Client.Dbvm.DbvmState.NotInitialized = 2 -> CheatEngine.Client.Dbvm.DbvmState
+CheatEngine.Client.Dbvm.DbvmState.Unavailable = 1 -> CheatEngine.Client.Dbvm.DbvmState
+CheatEngine.Client.Dbvm.DbvmState.Unknown = 0 -> CheatEngine.Client.Dbvm.DbvmState
+CheatEngine.Client.Dbvm.DbvmStatusSnapshot
+CheatEngine.Client.Dbvm.DbvmStatusSnapshot.DbvmStatusSnapshot() -> void
+CheatEngine.Client.Dbvm.DbvmStatusSnapshot.DbvmStatusSnapshot(CheatEngine.Client.Dbvm.DbvmState state, string? version = null) -> void
+CheatEngine.Client.Dbvm.DbvmStatusSnapshot.Equals(CheatEngine.Client.Dbvm.DbvmStatusSnapshot other) -> bool
+CheatEngine.Client.Dbvm.DbvmStatusSnapshot.State.get -> CheatEngine.Client.Dbvm.DbvmState
+CheatEngine.Client.Dbvm.DbvmStatusSnapshot.Version.get -> string?
+CheatEngine.Client.Dbvm.DbvmWatchEvent
+CheatEngine.Client.Dbvm.DbvmWatchEvent.Address.get -> CheatEngine.SDK.Engine.Values.Address
+CheatEngine.Client.Dbvm.DbvmWatchEvent.DbvmWatchEvent() -> void
+CheatEngine.Client.Dbvm.DbvmWatchEvent.DbvmWatchEvent(CheatEngine.SDK.Engine.Values.Address address, int length, System.DateTimeOffset occurredAt) -> void
+CheatEngine.Client.Dbvm.DbvmWatchEvent.Equals(CheatEngine.Client.Dbvm.DbvmWatchEvent other) -> bool
+CheatEngine.Client.Dbvm.DbvmWatchEvent.Length.get -> int
+CheatEngine.Client.Dbvm.DbvmWatchEvent.OccurredAt.get -> System.DateTimeOffset
+CheatEngine.Client.Dbvm.DbvmWatchHandler
+CheatEngine.Client.Dbvm.DbvmWatchRequest
+CheatEngine.Client.Dbvm.DbvmWatchRequest.Address.get -> CheatEngine.SDK.Engine.Values.Address
+CheatEngine.Client.Dbvm.DbvmWatchRequest.DbvmWatchRequest() -> void
+CheatEngine.Client.Dbvm.DbvmWatchRequest.DbvmWatchRequest(CheatEngine.SDK.Engine.Values.Address address, int length) -> void
+CheatEngine.Client.Dbvm.DbvmWatchRequest.Equals(CheatEngine.Client.Dbvm.DbvmWatchRequest other) -> bool
+CheatEngine.Client.Dbvm.DbvmWatchRequest.Length.get -> int
+CheatEngine.Client.Dbvm.IDbvmClient
+CheatEngine.Client.Dbvm.IDbvmClient.GetStatus(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Dbvm.DbvmStatusSnapshot
+CheatEngine.Client.Dbvm.IDbvmClient.Initialize(CheatEngine.Client.Dbvm.DbvmInitializationRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Dbvm.DbvmStatusSnapshot
+CheatEngine.Client.Dbvm.IDbvmClient.RegisterWatch(CheatEngine.Client.Dbvm.DbvmWatchRequest request, CheatEngine.Client.Dbvm.DbvmWatchHandler! handler, CheatEngine.Client.Events.EventStreamOptions streamOptions, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Dbvm.IDbvmWatchLease!
+CheatEngine.Client.Dbvm.IDbvmClient.TryGetStatus(out CheatEngine.Client.Dbvm.DbvmStatusSnapshot status, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Dbvm.IDbvmClient.TryInitialize(CheatEngine.Client.Dbvm.DbvmInitializationRequest request, out CheatEngine.Client.Dbvm.DbvmStatusSnapshot status, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Dbvm.IDbvmClient.TryRegisterWatch(CheatEngine.Client.Dbvm.DbvmWatchRequest request, CheatEngine.Client.Dbvm.DbvmWatchHandler! handler, CheatEngine.Client.Events.EventStreamOptions streamOptions, out CheatEngine.Client.Dbvm.IDbvmWatchLease? lease, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Dbvm.IDbvmWatchLease
+CheatEngine.Client.Dbvm.IDbvmWatchLease.Request.get -> CheatEngine.Client.Dbvm.DbvmWatchRequest
+CheatEngine.Client.Dbvm.IDbvmWatchLease.SelectionEpoch.get -> long
+CheatEngine.Client.Debugger.BreakpointDisposition
+CheatEngine.Client.Debugger.BreakpointDisposition.Break = 1 -> CheatEngine.Client.Debugger.BreakpointDisposition
+CheatEngine.Client.Debugger.BreakpointDisposition.Continue = 0 -> CheatEngine.Client.Debugger.BreakpointDisposition
+CheatEngine.Client.Debugger.BreakpointEvent
+CheatEngine.Client.Debugger.BreakpointEvent.Address.get -> CheatEngine.SDK.Engine.Values.Address
+CheatEngine.Client.Debugger.BreakpointEvent.BreakpointEvent() -> void
+CheatEngine.Client.Debugger.BreakpointEvent.BreakpointEvent(CheatEngine.SDK.Engine.Values.Address address, int threadId, System.Collections.Immutable.ImmutableArray registers) -> void
+CheatEngine.Client.Debugger.BreakpointEvent.Equals(CheatEngine.Client.Debugger.BreakpointEvent other) -> bool
+CheatEngine.Client.Debugger.BreakpointEvent.Registers.get -> System.Collections.Immutable.ImmutableArray
+CheatEngine.Client.Debugger.BreakpointEvent.ThreadId.get -> int
+CheatEngine.Client.Debugger.BreakpointHandler
+CheatEngine.Client.Debugger.BreakpointKind
+CheatEngine.Client.Debugger.BreakpointKind.Access = 2 -> CheatEngine.Client.Debugger.BreakpointKind
+CheatEngine.Client.Debugger.BreakpointKind.Execute = 0 -> CheatEngine.Client.Debugger.BreakpointKind
+CheatEngine.Client.Debugger.BreakpointKind.Write = 1 -> CheatEngine.Client.Debugger.BreakpointKind
+CheatEngine.Client.Debugger.BreakpointRegisterSnapshot
+CheatEngine.Client.Debugger.BreakpointRegisterSnapshot.BreakpointRegisterSnapshot() -> void
+CheatEngine.Client.Debugger.BreakpointRegisterSnapshot.BreakpointRegisterSnapshot(string! name, ulong value) -> void
+CheatEngine.Client.Debugger.BreakpointRegisterSnapshot.Equals(CheatEngine.Client.Debugger.BreakpointRegisterSnapshot other) -> bool
+CheatEngine.Client.Debugger.BreakpointRegisterSnapshot.Name.get -> string!
+CheatEngine.Client.Debugger.BreakpointRegisterSnapshot.Value.get -> ulong
+CheatEngine.Client.Debugger.BreakpointRequest
+CheatEngine.Client.Debugger.BreakpointRequest.Address.get -> CheatEngine.SDK.Engine.Values.Address
+CheatEngine.Client.Debugger.BreakpointRequest.BreakpointRequest() -> void
+CheatEngine.Client.Debugger.BreakpointRequest.BreakpointRequest(CheatEngine.SDK.Engine.Values.Address address, CheatEngine.Client.Debugger.BreakpointKind kind = CheatEngine.Client.Debugger.BreakpointKind.Execute) -> void
+CheatEngine.Client.Debugger.BreakpointRequest.Equals(CheatEngine.Client.Debugger.BreakpointRequest other) -> bool
+CheatEngine.Client.Debugger.BreakpointRequest.Kind.get -> CheatEngine.Client.Debugger.BreakpointKind
+CheatEngine.Client.Debugger.IBreakpointLease
+CheatEngine.Client.Debugger.IBreakpointLease.Request.get -> CheatEngine.Client.Debugger.BreakpointRequest
+CheatEngine.Client.Debugger.IBreakpointLease.SelectionEpoch.get -> long
+CheatEngine.Client.Debugger.IDebuggerClient
+CheatEngine.Client.Debugger.IDebuggerClient.RegisterBreakpoint(CheatEngine.Client.Debugger.BreakpointRequest request, CheatEngine.Client.Debugger.BreakpointHandler! handler, CheatEngine.Client.Events.EventStreamOptions streamOptions, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Debugger.IBreakpointLease!
+CheatEngine.Client.Debugger.IDebuggerClient.TryRegisterBreakpoint(CheatEngine.Client.Debugger.BreakpointRequest request, CheatEngine.Client.Debugger.BreakpointHandler! handler, CheatEngine.Client.Events.EventStreamOptions streamOptions, out CheatEngine.Client.Debugger.IBreakpointLease? lease, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Events.EventStreamOptions
+CheatEngine.Client.Events.EventStreamOptions.Capacity.get -> int
+CheatEngine.Client.Events.EventStreamOptions.Equals(CheatEngine.Client.Events.EventStreamOptions other) -> bool
+CheatEngine.Client.Events.EventStreamOptions.EventStreamOptions() -> void
+CheatEngine.Client.Events.EventStreamOptions.EventStreamOptions(int capacity, CheatEngine.Client.Events.EventStreamOverflowPolicy overflowPolicy = CheatEngine.Client.Events.EventStreamOverflowPolicy.DropOldest) -> void
+CheatEngine.Client.Events.EventStreamOptions.OverflowPolicy.get -> CheatEngine.Client.Events.EventStreamOverflowPolicy
+CheatEngine.Client.Events.EventStreamOverflowPolicy
+CheatEngine.Client.Events.EventStreamOverflowPolicy.DropNewest = 1 -> CheatEngine.Client.Events.EventStreamOverflowPolicy
+CheatEngine.Client.Events.EventStreamOverflowPolicy.DropOldest = 0 -> CheatEngine.Client.Events.EventStreamOverflowPolicy
+CheatEngine.Client.Events.EventStreamOverflowPolicy.FailSubscription = 2 -> CheatEngine.Client.Events.EventStreamOverflowPolicy
+CheatEngine.Client.Events.IEventStreamLease
+CheatEngine.Client.Events.IEventStreamLease.DroppedEventCount.get -> long
+CheatEngine.Client.Events.IEventStreamLease.Events.get -> System.Collections.Generic.IAsyncEnumerable!
+CheatEngine.Client.Events.IEventStreamLease.IsReleased.get -> bool
+CheatEngine.Client.Hashing.FileHashRequest
+CheatEngine.Client.Hashing.FileHashRequest.Algorithm.get -> CheatEngine.Client.Hashing.TargetHashAlgorithm
+CheatEngine.Client.Hashing.FileHashRequest.Equals(CheatEngine.Client.Hashing.FileHashRequest other) -> bool
+CheatEngine.Client.Hashing.FileHashRequest.FileHashRequest() -> void
+CheatEngine.Client.Hashing.FileHashRequest.FileHashRequest(string! filePath, CheatEngine.Client.Hashing.TargetHashAlgorithm algorithm = CheatEngine.Client.Hashing.TargetHashAlgorithm.Sha256) -> void
+CheatEngine.Client.Hashing.FileHashRequest.FilePath.get -> string!
+CheatEngine.Client.Hashing.HashDigest
+CheatEngine.Client.Hashing.HashDigest.Algorithm.get -> CheatEngine.Client.Hashing.TargetHashAlgorithm
+CheatEngine.Client.Hashing.HashDigest.Equals(CheatEngine.Client.Hashing.HashDigest other) -> bool
+CheatEngine.Client.Hashing.HashDigest.HashDigest() -> void
+CheatEngine.Client.Hashing.HashDigest.HashDigest(CheatEngine.Client.Hashing.TargetHashAlgorithm algorithm, string! value) -> void
+CheatEngine.Client.Hashing.HashDigest.Value.get -> string!
+CheatEngine.Client.Hashing.IHashingClient
+CheatEngine.Client.Hashing.IHashingClient.HashFile(CheatEngine.Client.Hashing.FileHashRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Hashing.HashDigest
+CheatEngine.Client.Hashing.IHashingClient.HashMemory(CheatEngine.Client.Hashing.MemoryHashRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Hashing.HashDigest
+CheatEngine.Client.Hashing.IHashingClient.TryHashFile(CheatEngine.Client.Hashing.FileHashRequest request, out CheatEngine.Client.Hashing.HashDigest digest, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Hashing.IHashingClient.TryHashMemory(CheatEngine.Client.Hashing.MemoryHashRequest request, out CheatEngine.Client.Hashing.HashDigest digest, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Hashing.MemoryHashRequest
+CheatEngine.Client.Hashing.MemoryHashRequest.Address.get -> CheatEngine.SDK.Engine.Values.Address
+CheatEngine.Client.Hashing.MemoryHashRequest.Algorithm.get -> CheatEngine.Client.Hashing.TargetHashAlgorithm
+CheatEngine.Client.Hashing.MemoryHashRequest.Equals(CheatEngine.Client.Hashing.MemoryHashRequest other) -> bool
+CheatEngine.Client.Hashing.MemoryHashRequest.Length.get -> int
+CheatEngine.Client.Hashing.MemoryHashRequest.MemoryHashRequest() -> void
+CheatEngine.Client.Hashing.MemoryHashRequest.MemoryHashRequest(CheatEngine.SDK.Engine.Values.Address address, int length, CheatEngine.Client.Hashing.TargetHashAlgorithm algorithm = CheatEngine.Client.Hashing.TargetHashAlgorithm.Sha256) -> void
+CheatEngine.Client.Hashing.TargetHashAlgorithm
+CheatEngine.Client.Hashing.TargetHashAlgorithm.Md5 = 0 -> CheatEngine.Client.Hashing.TargetHashAlgorithm
+CheatEngine.Client.Hashing.TargetHashAlgorithm.Sha1 = 1 -> CheatEngine.Client.Hashing.TargetHashAlgorithm
+CheatEngine.Client.Hashing.TargetHashAlgorithm.Sha256 = 2 -> CheatEngine.Client.Hashing.TargetHashAlgorithm
+CheatEngine.Client.Hotkeys.HotkeyEvent
+CheatEngine.Client.Hotkeys.HotkeyEvent.Equals(CheatEngine.Client.Hotkeys.HotkeyEvent other) -> bool
+CheatEngine.Client.Hotkeys.HotkeyEvent.Gesture.get -> CheatEngine.Client.Hotkeys.HotkeyGesture
+CheatEngine.Client.Hotkeys.HotkeyEvent.HotkeyEvent() -> void
+CheatEngine.Client.Hotkeys.HotkeyEvent.HotkeyEvent(string! name, CheatEngine.Client.Hotkeys.HotkeyGesture gesture, System.DateTimeOffset occurredAt) -> void
+CheatEngine.Client.Hotkeys.HotkeyEvent.Name.get -> string!
+CheatEngine.Client.Hotkeys.HotkeyEvent.OccurredAt.get -> System.DateTimeOffset
+CheatEngine.Client.Hotkeys.HotkeyGesture
+CheatEngine.Client.Hotkeys.HotkeyGesture.Equals(CheatEngine.Client.Hotkeys.HotkeyGesture other) -> bool
+CheatEngine.Client.Hotkeys.HotkeyGesture.HotkeyGesture() -> void
+CheatEngine.Client.Hotkeys.HotkeyGesture.HotkeyGesture(int virtualKey, CheatEngine.Client.Hotkeys.HotkeyModifiers modifiers = CheatEngine.Client.Hotkeys.HotkeyModifiers.None) -> void
+CheatEngine.Client.Hotkeys.HotkeyGesture.Modifiers.get -> CheatEngine.Client.Hotkeys.HotkeyModifiers
+CheatEngine.Client.Hotkeys.HotkeyGesture.VirtualKey.get -> int
+CheatEngine.Client.Hotkeys.HotkeyHandler
+CheatEngine.Client.Hotkeys.HotkeyModifiers
+CheatEngine.Client.Hotkeys.HotkeyModifiers.Alt = 1 -> CheatEngine.Client.Hotkeys.HotkeyModifiers
+CheatEngine.Client.Hotkeys.HotkeyModifiers.Control = 2 -> CheatEngine.Client.Hotkeys.HotkeyModifiers
+CheatEngine.Client.Hotkeys.HotkeyModifiers.None = 0 -> CheatEngine.Client.Hotkeys.HotkeyModifiers
+CheatEngine.Client.Hotkeys.HotkeyModifiers.Shift = 4 -> CheatEngine.Client.Hotkeys.HotkeyModifiers
+CheatEngine.Client.Hotkeys.HotkeyModifiers.Windows = 8 -> CheatEngine.Client.Hotkeys.HotkeyModifiers
+CheatEngine.Client.Hotkeys.HotkeyRegistration
+CheatEngine.Client.Hotkeys.HotkeyRegistration.Equals(CheatEngine.Client.Hotkeys.HotkeyRegistration other) -> bool
+CheatEngine.Client.Hotkeys.HotkeyRegistration.Gesture.get -> CheatEngine.Client.Hotkeys.HotkeyGesture
+CheatEngine.Client.Hotkeys.HotkeyRegistration.HotkeyRegistration() -> void
+CheatEngine.Client.Hotkeys.HotkeyRegistration.HotkeyRegistration(string! name, CheatEngine.Client.Hotkeys.HotkeyGesture gesture) -> void
+CheatEngine.Client.Hotkeys.HotkeyRegistration.Name.get -> string!
+CheatEngine.Client.Hotkeys.IHotkeyClient
+CheatEngine.Client.Hotkeys.IHotkeyClient.Register(CheatEngine.Client.Hotkeys.HotkeyRegistration registration, CheatEngine.Client.Hotkeys.HotkeyHandler! handler, CheatEngine.Client.Events.EventStreamOptions streamOptions, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Hotkeys.IHotkeyLease!
+CheatEngine.Client.Hotkeys.IHotkeyClient.TryRegister(CheatEngine.Client.Hotkeys.HotkeyRegistration registration, CheatEngine.Client.Hotkeys.HotkeyHandler! handler, CheatEngine.Client.Events.EventStreamOptions streamOptions, out CheatEngine.Client.Hotkeys.IHotkeyLease? lease, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Hotkeys.IHotkeyLease
+CheatEngine.Client.Hotkeys.IHotkeyLease.Registration.get -> CheatEngine.Client.Hotkeys.HotkeyRegistration
+CheatEngine.Client.RemoteExecution.IRemoteExecutionClient
+CheatEngine.Client.RemoteExecution.IRemoteExecutionClient.InjectLibrary(CheatEngine.Client.RemoteExecution.RemoteDllInjectionRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> void
+CheatEngine.Client.RemoteExecution.IRemoteExecutionClient.Invoke(CheatEngine.Client.RemoteExecution.RemoteCallRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.RemoteExecution.RemoteCallResult
+CheatEngine.Client.RemoteExecution.IRemoteExecutionClient.TryInjectLibrary(CheatEngine.Client.RemoteExecution.RemoteDllInjectionRequest request, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.RemoteExecution.IRemoteExecutionClient.TryInvoke(CheatEngine.Client.RemoteExecution.RemoteCallRequest request, out CheatEngine.Client.RemoteExecution.RemoteCallResult result, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.RemoteExecution.RemoteCallRequest
+CheatEngine.Client.RemoteExecution.RemoteCallRequest.EntryPoint.get -> CheatEngine.SDK.Engine.Values.Address
+CheatEngine.Client.RemoteExecution.RemoteCallRequest.Equals(CheatEngine.Client.RemoteExecution.RemoteCallRequest other) -> bool
+CheatEngine.Client.RemoteExecution.RemoteCallRequest.Parameters.get -> System.Collections.Immutable.ImmutableArray
+CheatEngine.Client.RemoteExecution.RemoteCallRequest.RemoteCallRequest() -> void
+CheatEngine.Client.RemoteExecution.RemoteCallRequest.RemoteCallRequest(CheatEngine.SDK.Engine.Values.Address entryPoint, System.ReadOnlySpan parameters, System.TimeSpan timeout) -> void
+CheatEngine.Client.RemoteExecution.RemoteCallRequest.Timeout.get -> System.TimeSpan
+CheatEngine.Client.RemoteExecution.RemoteCallResult
+CheatEngine.Client.RemoteExecution.RemoteCallResult.Equals(CheatEngine.Client.RemoteExecution.RemoteCallResult other) -> bool
+CheatEngine.Client.RemoteExecution.RemoteCallResult.Output.get -> System.Collections.Immutable.ImmutableArray
+CheatEngine.Client.RemoteExecution.RemoteCallResult.RemoteCallResult() -> void
+CheatEngine.Client.RemoteExecution.RemoteCallResult.RemoteCallResult(ulong returnValue, System.ReadOnlySpan output) -> void
+CheatEngine.Client.RemoteExecution.RemoteCallResult.ReturnValue.get -> ulong
+CheatEngine.Client.RemoteExecution.RemoteDllInjectionRequest
+CheatEngine.Client.RemoteExecution.RemoteDllInjectionRequest.Equals(CheatEngine.Client.RemoteExecution.RemoteDllInjectionRequest other) -> bool
+CheatEngine.Client.RemoteExecution.RemoteDllInjectionRequest.LibraryPath.get -> string!
+CheatEngine.Client.RemoteExecution.RemoteDllInjectionRequest.RemoteDllInjectionRequest() -> void
+CheatEngine.Client.RemoteExecution.RemoteDllInjectionRequest.RemoteDllInjectionRequest(string! libraryPath) -> void
+CheatEngine.Client.Speed.ISpeedClient
+CheatEngine.Client.Speed.ISpeedClient.GetMultiplier(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Speed.SpeedMultiplier
+CheatEngine.Client.Speed.ISpeedClient.SetMultiplier(CheatEngine.Client.Speed.SpeedMultiplier multiplier, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> void
+CheatEngine.Client.Speed.ISpeedClient.TryGetMultiplier(out CheatEngine.Client.Speed.SpeedMultiplier multiplier, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Speed.ISpeedClient.TrySetMultiplier(CheatEngine.Client.Speed.SpeedMultiplier multiplier, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Speed.SpeedMultiplier
+CheatEngine.Client.Speed.SpeedMultiplier.Equals(CheatEngine.Client.Speed.SpeedMultiplier other) -> bool
+CheatEngine.Client.Speed.SpeedMultiplier.SpeedMultiplier() -> void
+CheatEngine.Client.Speed.SpeedMultiplier.SpeedMultiplier(double value) -> void
+CheatEngine.Client.Speed.SpeedMultiplier.Value.get -> double
+CheatEngine.Client.Timers.ITimerClient
+CheatEngine.Client.Timers.ITimerClient.Register(CheatEngine.Client.Timers.TimerRequest request, CheatEngine.Client.Timers.TimerHandler! handler, CheatEngine.Client.Events.EventStreamOptions streamOptions, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Timers.ITimerLease!
+CheatEngine.Client.Timers.ITimerClient.TryRegister(CheatEngine.Client.Timers.TimerRequest request, CheatEngine.Client.Timers.TimerHandler! handler, CheatEngine.Client.Events.EventStreamOptions streamOptions, out CheatEngine.Client.Timers.ITimerLease? lease, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Timers.ITimerLease
+CheatEngine.Client.Timers.ITimerLease.Request.get -> CheatEngine.Client.Timers.TimerRequest
+CheatEngine.Client.Timers.TimerHandler
+CheatEngine.Client.Timers.TimerRequest
+CheatEngine.Client.Timers.TimerRequest.Equals(CheatEngine.Client.Timers.TimerRequest other) -> bool
+CheatEngine.Client.Timers.TimerRequest.Interval.get -> System.TimeSpan
+CheatEngine.Client.Timers.TimerRequest.TimerRequest() -> void
+CheatEngine.Client.Timers.TimerRequest.TimerRequest(System.TimeSpan interval) -> void
+CheatEngine.Client.Timers.TimerTick
+CheatEngine.Client.Timers.TimerTick.Equals(CheatEngine.Client.Timers.TimerTick other) -> bool
+CheatEngine.Client.Timers.TimerTick.OccurredAt.get -> System.DateTimeOffset
+CheatEngine.Client.Timers.TimerTick.Sequence.get -> long
+CheatEngine.Client.Timers.TimerTick.TimerTick() -> void
+CheatEngine.Client.Timers.TimerTick.TimerTick(long sequence, System.DateTimeOffset occurredAt) -> void
+~override CheatEngine.Client.Allocations.TargetAllocationRequest.Equals(object obj) -> bool
+~override CheatEngine.Client.Allocations.TargetAllocationRequest.ToString() -> string
+~override CheatEngine.Client.Assembly.AssemblyInstructionRequest.Equals(object obj) -> bool
+~override CheatEngine.Client.Assembly.AssemblyInstructionRequest.ToString() -> string
+~override CheatEngine.Client.Assembly.AssemblyInstructionSnapshot.Equals(object obj) -> bool
+~override CheatEngine.Client.Assembly.AssemblyInstructionSnapshot.ToString() -> string
+~override CheatEngine.Client.Assembly.AutoAssemblerScript.Equals(object obj) -> bool
+~override CheatEngine.Client.Assembly.AutoAssemblerScript.ToString() -> string
+~override CheatEngine.Client.Dbvm.DbvmInitializationRequest.Equals(object obj) -> bool
+~override CheatEngine.Client.Dbvm.DbvmInitializationRequest.ToString() -> string
+~override CheatEngine.Client.Dbvm.DbvmStatusSnapshot.Equals(object obj) -> bool
+~override CheatEngine.Client.Dbvm.DbvmStatusSnapshot.ToString() -> string
+~override CheatEngine.Client.Dbvm.DbvmWatchEvent.Equals(object obj) -> bool
+~override CheatEngine.Client.Dbvm.DbvmWatchEvent.ToString() -> string
+~override CheatEngine.Client.Dbvm.DbvmWatchRequest.Equals(object obj) -> bool
+~override CheatEngine.Client.Dbvm.DbvmWatchRequest.ToString() -> string
+~override CheatEngine.Client.Debugger.BreakpointEvent.Equals(object obj) -> bool
+~override CheatEngine.Client.Debugger.BreakpointEvent.ToString() -> string
+~override CheatEngine.Client.Debugger.BreakpointRegisterSnapshot.Equals(object obj) -> bool
+~override CheatEngine.Client.Debugger.BreakpointRegisterSnapshot.ToString() -> string
+~override CheatEngine.Client.Debugger.BreakpointRequest.Equals(object obj) -> bool
+~override CheatEngine.Client.Debugger.BreakpointRequest.ToString() -> string
+~override CheatEngine.Client.Events.EventStreamOptions.Equals(object obj) -> bool
+~override CheatEngine.Client.Events.EventStreamOptions.ToString() -> string
+~override CheatEngine.Client.Hashing.FileHashRequest.Equals(object obj) -> bool
+~override CheatEngine.Client.Hashing.FileHashRequest.ToString() -> string
+~override CheatEngine.Client.Hashing.HashDigest.Equals(object obj) -> bool
+~override CheatEngine.Client.Hashing.HashDigest.ToString() -> string
+~override CheatEngine.Client.Hashing.MemoryHashRequest.Equals(object obj) -> bool
+~override CheatEngine.Client.Hashing.MemoryHashRequest.ToString() -> string
+~override CheatEngine.Client.Hotkeys.HotkeyEvent.Equals(object obj) -> bool
+~override CheatEngine.Client.Hotkeys.HotkeyEvent.ToString() -> string
+~override CheatEngine.Client.Hotkeys.HotkeyGesture.Equals(object obj) -> bool
+~override CheatEngine.Client.Hotkeys.HotkeyGesture.ToString() -> string
+~override CheatEngine.Client.Hotkeys.HotkeyRegistration.Equals(object obj) -> bool
+~override CheatEngine.Client.Hotkeys.HotkeyRegistration.ToString() -> string
+~override CheatEngine.Client.RemoteExecution.RemoteCallRequest.Equals(object obj) -> bool
+~override CheatEngine.Client.RemoteExecution.RemoteCallRequest.ToString() -> string
+~override CheatEngine.Client.RemoteExecution.RemoteCallResult.Equals(object obj) -> bool
+~override CheatEngine.Client.RemoteExecution.RemoteCallResult.ToString() -> string
+~override CheatEngine.Client.RemoteExecution.RemoteDllInjectionRequest.Equals(object obj) -> bool
+~override CheatEngine.Client.RemoteExecution.RemoteDllInjectionRequest.ToString() -> string
+~override CheatEngine.Client.Speed.SpeedMultiplier.Equals(object obj) -> bool
+~override CheatEngine.Client.Speed.SpeedMultiplier.ToString() -> string
+~override CheatEngine.Client.Timers.TimerRequest.Equals(object obj) -> bool
+~override CheatEngine.Client.Timers.TimerRequest.ToString() -> string
+~override CheatEngine.Client.Timers.TimerTick.Equals(object obj) -> bool
+~override CheatEngine.Client.Timers.TimerTick.ToString() -> string
+override CheatEngine.Client.Allocations.TargetAllocationRequest.GetHashCode() -> int
+override CheatEngine.Client.Assembly.AssemblyInstructionRequest.GetHashCode() -> int
+override CheatEngine.Client.Assembly.AssemblyInstructionSnapshot.GetHashCode() -> int
+override CheatEngine.Client.Assembly.AutoAssemblerScript.GetHashCode() -> int
+override CheatEngine.Client.Dbvm.DbvmInitializationRequest.GetHashCode() -> int
+override CheatEngine.Client.Dbvm.DbvmStatusSnapshot.GetHashCode() -> int
+override CheatEngine.Client.Dbvm.DbvmWatchEvent.GetHashCode() -> int
+override CheatEngine.Client.Dbvm.DbvmWatchRequest.GetHashCode() -> int
+override CheatEngine.Client.Debugger.BreakpointEvent.GetHashCode() -> int
+override CheatEngine.Client.Debugger.BreakpointRegisterSnapshot.GetHashCode() -> int
+override CheatEngine.Client.Debugger.BreakpointRequest.GetHashCode() -> int
+override CheatEngine.Client.Events.EventStreamOptions.GetHashCode() -> int
+override CheatEngine.Client.Hashing.FileHashRequest.GetHashCode() -> int
+override CheatEngine.Client.Hashing.HashDigest.GetHashCode() -> int
+override CheatEngine.Client.Hashing.MemoryHashRequest.GetHashCode() -> int
+override CheatEngine.Client.Hotkeys.HotkeyEvent.GetHashCode() -> int
+override CheatEngine.Client.Hotkeys.HotkeyGesture.GetHashCode() -> int
+override CheatEngine.Client.Hotkeys.HotkeyRegistration.GetHashCode() -> int
+override CheatEngine.Client.RemoteExecution.RemoteCallRequest.GetHashCode() -> int
+override CheatEngine.Client.RemoteExecution.RemoteCallResult.GetHashCode() -> int
+override CheatEngine.Client.RemoteExecution.RemoteDllInjectionRequest.GetHashCode() -> int
+override CheatEngine.Client.Speed.SpeedMultiplier.GetHashCode() -> int
+override CheatEngine.Client.Timers.TimerRequest.GetHashCode() -> int
+override CheatEngine.Client.Timers.TimerTick.GetHashCode() -> int
+static CheatEngine.Client.Allocations.TargetAllocationRequest.operator !=(CheatEngine.Client.Allocations.TargetAllocationRequest left, CheatEngine.Client.Allocations.TargetAllocationRequest right) -> bool
+static CheatEngine.Client.Allocations.TargetAllocationRequest.operator ==(CheatEngine.Client.Allocations.TargetAllocationRequest left, CheatEngine.Client.Allocations.TargetAllocationRequest right) -> bool
+static CheatEngine.Client.Assembly.AssemblyInstructionRequest.operator !=(CheatEngine.Client.Assembly.AssemblyInstructionRequest left, CheatEngine.Client.Assembly.AssemblyInstructionRequest right) -> bool
+static CheatEngine.Client.Assembly.AssemblyInstructionRequest.operator ==(CheatEngine.Client.Assembly.AssemblyInstructionRequest left, CheatEngine.Client.Assembly.AssemblyInstructionRequest right) -> bool
+static CheatEngine.Client.Assembly.AssemblyInstructionSnapshot.operator !=(CheatEngine.Client.Assembly.AssemblyInstructionSnapshot left, CheatEngine.Client.Assembly.AssemblyInstructionSnapshot right) -> bool
+static CheatEngine.Client.Assembly.AssemblyInstructionSnapshot.operator ==(CheatEngine.Client.Assembly.AssemblyInstructionSnapshot left, CheatEngine.Client.Assembly.AssemblyInstructionSnapshot right) -> bool
+static CheatEngine.Client.Assembly.AutoAssemblerScript.operator !=(CheatEngine.Client.Assembly.AutoAssemblerScript left, CheatEngine.Client.Assembly.AutoAssemblerScript right) -> bool
+static CheatEngine.Client.Assembly.AutoAssemblerScript.operator ==(CheatEngine.Client.Assembly.AutoAssemblerScript left, CheatEngine.Client.Assembly.AutoAssemblerScript right) -> bool
+static CheatEngine.Client.Dbvm.DbvmInitializationRequest.operator !=(CheatEngine.Client.Dbvm.DbvmInitializationRequest left, CheatEngine.Client.Dbvm.DbvmInitializationRequest right) -> bool
+static CheatEngine.Client.Dbvm.DbvmInitializationRequest.operator ==(CheatEngine.Client.Dbvm.DbvmInitializationRequest left, CheatEngine.Client.Dbvm.DbvmInitializationRequest right) -> bool
+static CheatEngine.Client.Dbvm.DbvmStatusSnapshot.operator !=(CheatEngine.Client.Dbvm.DbvmStatusSnapshot left, CheatEngine.Client.Dbvm.DbvmStatusSnapshot right) -> bool
+static CheatEngine.Client.Dbvm.DbvmStatusSnapshot.operator ==(CheatEngine.Client.Dbvm.DbvmStatusSnapshot left, CheatEngine.Client.Dbvm.DbvmStatusSnapshot right) -> bool
+static CheatEngine.Client.Dbvm.DbvmWatchEvent.operator !=(CheatEngine.Client.Dbvm.DbvmWatchEvent left, CheatEngine.Client.Dbvm.DbvmWatchEvent right) -> bool
+static CheatEngine.Client.Dbvm.DbvmWatchEvent.operator ==(CheatEngine.Client.Dbvm.DbvmWatchEvent left, CheatEngine.Client.Dbvm.DbvmWatchEvent right) -> bool
+static CheatEngine.Client.Dbvm.DbvmWatchRequest.operator !=(CheatEngine.Client.Dbvm.DbvmWatchRequest left, CheatEngine.Client.Dbvm.DbvmWatchRequest right) -> bool
+static CheatEngine.Client.Dbvm.DbvmWatchRequest.operator ==(CheatEngine.Client.Dbvm.DbvmWatchRequest left, CheatEngine.Client.Dbvm.DbvmWatchRequest right) -> bool
+static CheatEngine.Client.Debugger.BreakpointEvent.operator !=(CheatEngine.Client.Debugger.BreakpointEvent left, CheatEngine.Client.Debugger.BreakpointEvent right) -> bool
+static CheatEngine.Client.Debugger.BreakpointEvent.operator ==(CheatEngine.Client.Debugger.BreakpointEvent left, CheatEngine.Client.Debugger.BreakpointEvent right) -> bool
+static CheatEngine.Client.Debugger.BreakpointRegisterSnapshot.operator !=(CheatEngine.Client.Debugger.BreakpointRegisterSnapshot left, CheatEngine.Client.Debugger.BreakpointRegisterSnapshot right) -> bool
+static CheatEngine.Client.Debugger.BreakpointRegisterSnapshot.operator ==(CheatEngine.Client.Debugger.BreakpointRegisterSnapshot left, CheatEngine.Client.Debugger.BreakpointRegisterSnapshot right) -> bool
+static CheatEngine.Client.Debugger.BreakpointRequest.operator !=(CheatEngine.Client.Debugger.BreakpointRequest left, CheatEngine.Client.Debugger.BreakpointRequest right) -> bool
+static CheatEngine.Client.Debugger.BreakpointRequest.operator ==(CheatEngine.Client.Debugger.BreakpointRequest left, CheatEngine.Client.Debugger.BreakpointRequest right) -> bool
+static CheatEngine.Client.Events.EventStreamOptions.operator !=(CheatEngine.Client.Events.EventStreamOptions left, CheatEngine.Client.Events.EventStreamOptions right) -> bool
+static CheatEngine.Client.Events.EventStreamOptions.operator ==(CheatEngine.Client.Events.EventStreamOptions left, CheatEngine.Client.Events.EventStreamOptions right) -> bool
+static CheatEngine.Client.Hashing.FileHashRequest.operator !=(CheatEngine.Client.Hashing.FileHashRequest left, CheatEngine.Client.Hashing.FileHashRequest right) -> bool
+static CheatEngine.Client.Hashing.FileHashRequest.operator ==(CheatEngine.Client.Hashing.FileHashRequest left, CheatEngine.Client.Hashing.FileHashRequest right) -> bool
+static CheatEngine.Client.Hashing.HashDigest.operator !=(CheatEngine.Client.Hashing.HashDigest left, CheatEngine.Client.Hashing.HashDigest right) -> bool
+static CheatEngine.Client.Hashing.HashDigest.operator ==(CheatEngine.Client.Hashing.HashDigest left, CheatEngine.Client.Hashing.HashDigest right) -> bool
+static CheatEngine.Client.Hashing.MemoryHashRequest.operator !=(CheatEngine.Client.Hashing.MemoryHashRequest left, CheatEngine.Client.Hashing.MemoryHashRequest right) -> bool
+static CheatEngine.Client.Hashing.MemoryHashRequest.operator ==(CheatEngine.Client.Hashing.MemoryHashRequest left, CheatEngine.Client.Hashing.MemoryHashRequest right) -> bool
+static CheatEngine.Client.Hotkeys.HotkeyEvent.operator !=(CheatEngine.Client.Hotkeys.HotkeyEvent left, CheatEngine.Client.Hotkeys.HotkeyEvent right) -> bool
+static CheatEngine.Client.Hotkeys.HotkeyEvent.operator ==(CheatEngine.Client.Hotkeys.HotkeyEvent left, CheatEngine.Client.Hotkeys.HotkeyEvent right) -> bool
+static CheatEngine.Client.Hotkeys.HotkeyGesture.operator !=(CheatEngine.Client.Hotkeys.HotkeyGesture left, CheatEngine.Client.Hotkeys.HotkeyGesture right) -> bool
+static CheatEngine.Client.Hotkeys.HotkeyGesture.operator ==(CheatEngine.Client.Hotkeys.HotkeyGesture left, CheatEngine.Client.Hotkeys.HotkeyGesture right) -> bool
+static CheatEngine.Client.Hotkeys.HotkeyRegistration.operator !=(CheatEngine.Client.Hotkeys.HotkeyRegistration left, CheatEngine.Client.Hotkeys.HotkeyRegistration right) -> bool
+static CheatEngine.Client.Hotkeys.HotkeyRegistration.operator ==(CheatEngine.Client.Hotkeys.HotkeyRegistration left, CheatEngine.Client.Hotkeys.HotkeyRegistration right) -> bool
+static CheatEngine.Client.RemoteExecution.RemoteCallRequest.operator !=(CheatEngine.Client.RemoteExecution.RemoteCallRequest left, CheatEngine.Client.RemoteExecution.RemoteCallRequest right) -> bool
+static CheatEngine.Client.RemoteExecution.RemoteCallRequest.operator ==(CheatEngine.Client.RemoteExecution.RemoteCallRequest left, CheatEngine.Client.RemoteExecution.RemoteCallRequest right) -> bool
+static CheatEngine.Client.RemoteExecution.RemoteCallResult.operator !=(CheatEngine.Client.RemoteExecution.RemoteCallResult left, CheatEngine.Client.RemoteExecution.RemoteCallResult right) -> bool
+static CheatEngine.Client.RemoteExecution.RemoteCallResult.operator ==(CheatEngine.Client.RemoteExecution.RemoteCallResult left, CheatEngine.Client.RemoteExecution.RemoteCallResult right) -> bool
+static CheatEngine.Client.RemoteExecution.RemoteDllInjectionRequest.operator !=(CheatEngine.Client.RemoteExecution.RemoteDllInjectionRequest left, CheatEngine.Client.RemoteExecution.RemoteDllInjectionRequest right) -> bool
+static CheatEngine.Client.RemoteExecution.RemoteDllInjectionRequest.operator ==(CheatEngine.Client.RemoteExecution.RemoteDllInjectionRequest left, CheatEngine.Client.RemoteExecution.RemoteDllInjectionRequest right) -> bool
+static CheatEngine.Client.Speed.SpeedMultiplier.operator !=(CheatEngine.Client.Speed.SpeedMultiplier left, CheatEngine.Client.Speed.SpeedMultiplier right) -> bool
+static CheatEngine.Client.Speed.SpeedMultiplier.operator ==(CheatEngine.Client.Speed.SpeedMultiplier left, CheatEngine.Client.Speed.SpeedMultiplier right) -> bool
+static CheatEngine.Client.Timers.TimerRequest.operator !=(CheatEngine.Client.Timers.TimerRequest left, CheatEngine.Client.Timers.TimerRequest right) -> bool
+static CheatEngine.Client.Timers.TimerRequest.operator ==(CheatEngine.Client.Timers.TimerRequest left, CheatEngine.Client.Timers.TimerRequest right) -> bool
+static CheatEngine.Client.Timers.TimerTick.operator !=(CheatEngine.Client.Timers.TimerTick left, CheatEngine.Client.Timers.TimerTick right) -> bool
+static CheatEngine.Client.Timers.TimerTick.operator ==(CheatEngine.Client.Timers.TimerTick left, CheatEngine.Client.Timers.TimerTick right) -> bool
+virtual CheatEngine.Client.Dbvm.DbvmWatchHandler.Invoke(CheatEngine.Client.Dbvm.DbvmWatchEvent watchEvent) -> void
+virtual CheatEngine.Client.Debugger.BreakpointHandler.Invoke(CheatEngine.Client.Debugger.BreakpointEvent breakpointEvent) -> CheatEngine.Client.Debugger.BreakpointDisposition
+virtual CheatEngine.Client.Hotkeys.HotkeyHandler.Invoke(CheatEngine.Client.Hotkeys.HotkeyEvent hotkeyEvent) -> void
+virtual CheatEngine.Client.Timers.TimerHandler.Invoke(CheatEngine.Client.Timers.TimerTick tick) -> void
+static CheatEngine.Client.Lua.LuaModuleDescriptor.operator ==(CheatEngine.Client.Lua.LuaModuleDescriptor left, CheatEngine.Client.Lua.LuaModuleDescriptor right) -> bool
+CheatEngine.Client.Memory.IMemoryClient.ReadPrimitiveBatch(CheatEngine.Client.Memory.MemoryPrimitiveBatchReadRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Collections.Immutable.ImmutableArray
+CheatEngine.Client.Memory.IMemoryClient.TryReadPrimitiveBatch(CheatEngine.Client.Memory.MemoryPrimitiveBatchReadRequest request, out System.Collections.Immutable.ImmutableArray values, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Memory.IMemoryClient.TryWritePrimitiveBatch(CheatEngine.Client.Memory.MemoryPrimitiveBatchWriteRequest request, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Memory.IMemoryClient.WritePrimitiveBatch(CheatEngine.Client.Memory.MemoryPrimitiveBatchWriteRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> void
+CheatEngine.Client.Memory.MemoryAddressValue
+CheatEngine.Client.Memory.MemoryAddressValue.Address.get -> CheatEngine.SDK.Engine.Values.Address
+CheatEngine.Client.Memory.MemoryAddressValue.MemoryAddressValue(CheatEngine.SDK.Engine.Values.Address address, T value) -> void
+CheatEngine.Client.Memory.MemoryAddressValue.MemoryAddressValue() -> void
+CheatEngine.Client.Memory.MemoryAddressValue.Value.get -> T
+CheatEngine.Client.Memory.MemoryBatchLimits
+const CheatEngine.Client.Memory.MemoryBatchLimits.MaximumOperations = 1024 -> int
+CheatEngine.Client.Memory.MemoryPrimitiveBatchReadRequest
+CheatEngine.Client.Memory.MemoryPrimitiveBatchReadRequest.Addresses.get -> System.Collections.Immutable.ImmutableArray
+CheatEngine.Client.Memory.MemoryPrimitiveBatchReadRequest.MemoryPrimitiveBatchReadRequest(System.ReadOnlySpan addresses) -> void
+CheatEngine.Client.Memory.MemoryPrimitiveBatchReadRequest.MemoryPrimitiveBatchReadRequest() -> void
+CheatEngine.Client.Memory.MemoryPrimitiveBatchWriteRequest
+CheatEngine.Client.Memory.MemoryPrimitiveBatchWriteRequest.MemoryPrimitiveBatchWriteRequest(System.ReadOnlySpan> values) -> void
+CheatEngine.Client.Memory.MemoryPrimitiveBatchWriteRequest.MemoryPrimitiveBatchWriteRequest() -> void
+CheatEngine.Client.Memory.MemoryPrimitiveBatchWriteRequest.Values.get -> System.Collections.Immutable.ImmutableArray>
+CheatEngine.Client.Memory.MemoryStringEncoding
+CheatEngine.Client.Memory.MemoryStringEncoding.Utf16 = 1 -> CheatEngine.Client.Memory.MemoryStringEncoding
+CheatEngine.Client.Memory.MemoryStringEncoding.Utf8 = 0 -> CheatEngine.Client.Memory.MemoryStringEncoding
+static CheatEngine.Client.Memory.MemoryStringReadRequest.Create(CheatEngine.SDK.Engine.Values.Address address, int maximumLength, CheatEngine.Client.Memory.MemoryStringEncoding encoding) -> CheatEngine.Client.Memory.MemoryStringReadRequest
+CheatEngine.Client.Memory.MemoryStringReadRequest.Encoding.get -> CheatEngine.Client.Memory.MemoryStringEncoding
+static CheatEngine.Client.Memory.MemoryStringWriteRequest.CreateBounded(CheatEngine.SDK.Engine.Values.Address address, string! value, int maximumLength, CheatEngine.Client.Memory.MemoryStringEncoding encoding) -> CheatEngine.Client.Memory.MemoryStringWriteRequest
+CheatEngine.Client.Memory.MemoryStringWriteRequest.Encoding.get -> CheatEngine.Client.Memory.MemoryStringEncoding
+CheatEngine.Client.Memory.MemoryStringWriteRequest.MaximumLength.get -> int
diff --git a/libs/CheatEngine.Client.Abstractions/README.md b/libs/CheatEngine.Client.Abstractions/README.md
index 499cbcc..d344bae 100644
--- a/libs/CheatEngine.Client.Abstractions/README.md
+++ b/libs/CheatEngine.Client.Abstractions/README.md
@@ -39,8 +39,9 @@ CheatEngine.Client.Abstractions
`CheatEngineClientException`-derived exceptions only from the convenience methods.
- Keeps CE-owned objects out of the public surface: no `LuaState`, `LuaRef`, `CEObject`,
`Owned`, or raw native handle escapes this package.
-- Requires bounded copies for scans, table snapshots, strings, byte reads, pointer chains, and
- inspection collections, avoiding unbounded materialization and leaked SDK ownership.
+- Requires bounded copies for scans, table snapshots, strings, byte reads, finite pointer chains,
+ homogeneous primitive batches, and inspection collections, avoiding unbounded materialization and
+ leaked SDK ownership.
- Makes lifecycle constraints visible: `ICheatEngineClient.Epoch`, `Stopping`, leases, and scan
sessions are activation-scoped; stale resources report `CheatEngineActivationExpiredException`.
@@ -58,6 +59,11 @@ namespaces only:
| `.Scanning` | AOB contracts and the value-scan session contract |
| `.Tables` | copied Address List records and explicitly trusted table I/O requests |
| `.Lua` / `.Modules` | typed protected Lua operations, explicit modules, and leases |
+| `.Allocations` / `.Assembly` | selection-bound allocation and reversible patch leases |
+| `.RemoteExecution` | bounded DLL injection and remote-call requests |
+| `.Debugger` / `.Hotkeys` | synchronous copied callbacks and bounded stream projections |
+| `.Timers` / `.Speed` | activation-scoped timers and validated speed observations/mutations |
+| `.Hashing` / `.Dbvm` | separate memory/file hashes and explicit DBVM observation/control |
| `.Results` | classified expected failures and lifecycle exceptions |
No public consumer should use `CheatEngine.Client.Abstractions` as a namespace.
@@ -69,10 +75,10 @@ and explicitly disposable value-scan sessions. A contract is not an availability
must inspect `ICheatEngineRuntime` capability observations or handle `CapabilityUnavailable`.
In particular, the value-scan contract and state model are published, but the Core implementation
-does **not** currently create a live `MemScan`/`FoundList` session. CheatEngine.SDK 1.0.0 has no
-public ownership factory for the required objects; Client enablement remains blocked by the
-Cheat Engine 7.7 x64 ownership and reactivation live gate. Do not treat `IValueScanner` as an
-available v1 runtime feature.
+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
+enablement remains blocked by the Cheat Engine 7.7 x64 ownership and reactivation live gate. Do
+not treat `IValueScanner` as available until that gate promotes its capability.
`IUnsafeLuaClient` is intentionally separate from `ILuaClient` and is not registered by default.
It is for explicitly trusted source only and still never exposes a raw Lua state.
diff --git a/libs/CheatEngine.Client.Abstractions/RemoteExecution/IRemoteExecutionClient.cs b/libs/CheatEngine.Client.Abstractions/RemoteExecution/IRemoteExecutionClient.cs
new file mode 100644
index 0000000..fac13de
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/RemoteExecution/IRemoteExecutionClient.cs
@@ -0,0 +1,21 @@
+using CheatEngine.Client.Results;
+
+namespace CheatEngine.Client.RemoteExecution;
+
+/// Injects existing DLLs and executes bounded target calls without exposing target allocation handles.
+public interface IRemoteExecutionClient
+{
+ /// Tries to inject an existing absolute DLL path into the selected target process.
+ public bool TryInjectLibrary(RemoteDllInjectionRequest request, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Injects an existing absolute DLL path or throws when the host rejects it.
+ public void InjectLibrary(RemoteDllInjectionRequest request, CancellationToken cancellationToken = default);
+
+ /// Tries to call a target function with a positive timeout and copied parameter result data.
+ public bool TryInvoke(RemoteCallRequest request, out RemoteCallResult result, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Calls a target function or throws when remote execution fails.
+ public RemoteCallResult Invoke(RemoteCallRequest request, CancellationToken cancellationToken = default);
+}
diff --git a/libs/CheatEngine.Client.Abstractions/RemoteExecution/RemoteCallRequest.cs b/libs/CheatEngine.Client.Abstractions/RemoteExecution/RemoteCallRequest.cs
new file mode 100644
index 0000000..64d3b71
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/RemoteExecution/RemoteCallRequest.cs
@@ -0,0 +1,41 @@
+using System.Collections.Immutable;
+
+using CheatEngine.SDK.Engine.Values;
+
+namespace CheatEngine.Client.RemoteExecution;
+
+/// Describes one bounded remote call and its copied parameter payload.
+public readonly record struct RemoteCallRequest
+{
+ /// Creates a remote-call request.
+ /// is not strictly positive.
+ public RemoteCallRequest(Address entryPoint, ReadOnlySpan parameters, TimeSpan timeout)
+ {
+ if (timeout <= TimeSpan.Zero)
+ {
+ throw new ArgumentOutOfRangeException(nameof(timeout), "A remote-call timeout must be strictly positive.");
+ }
+
+ EntryPoint = entryPoint;
+ Parameters = ImmutableArray.Create(parameters);
+ Timeout = timeout;
+ }
+
+ /// Gets the target function entry-point address.
+ public Address EntryPoint
+ {
+ get;
+ }
+
+ /// Gets an immutable copy of the call parameter bytes.
+ public ImmutableArray Parameters
+ {
+ get;
+ }
+
+ /// Gets the strictly positive call timeout.
+ public TimeSpan Timeout
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/RemoteExecution/RemoteCallResult.cs b/libs/CheatEngine.Client.Abstractions/RemoteExecution/RemoteCallResult.cs
new file mode 100644
index 0000000..71697bf
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/RemoteExecution/RemoteCallResult.cs
@@ -0,0 +1,26 @@
+using System.Collections.Immutable;
+
+namespace CheatEngine.Client.RemoteExecution;
+
+/// Contains the copied result returned by a completed remote call.
+public readonly record struct RemoteCallResult
+{
+ /// Creates a copied remote-call result.
+ public RemoteCallResult(ulong returnValue, ReadOnlySpan output)
+ {
+ ReturnValue = returnValue;
+ Output = ImmutableArray.Create(output);
+ }
+
+ /// Gets the target ABI return value represented as an unsigned machine value.
+ public ulong ReturnValue
+ {
+ get;
+ }
+
+ /// Gets an immutable copy of any requested output parameter bytes.
+ public ImmutableArray Output
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/RemoteExecution/RemoteDllInjectionRequest.cs b/libs/CheatEngine.Client.Abstractions/RemoteExecution/RemoteDllInjectionRequest.cs
new file mode 100644
index 0000000..6f37967
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/RemoteExecution/RemoteDllInjectionRequest.cs
@@ -0,0 +1,29 @@
+namespace CheatEngine.Client.RemoteExecution;
+
+/// Describes a DLL whose existing absolute path is injected into the selected target.
+public readonly record struct RemoteDllInjectionRequest
+{
+ /// Creates a remote DLL-injection request.
+ /// is blank, relative, or not a DLL path.
+ public RemoteDllInjectionRequest(string libraryPath)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(libraryPath);
+ if (!Path.IsPathFullyQualified(libraryPath))
+ {
+ throw new ArgumentException("A DLL injection path must be absolute.", nameof(libraryPath));
+ }
+
+ if (!string.Equals(Path.GetExtension(libraryPath), ".dll", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new ArgumentException("A DLL injection path must have a .dll extension.", nameof(libraryPath));
+ }
+
+ LibraryPath = libraryPath;
+ }
+
+ /// Gets the absolute DLL path. The implementation verifies that it exists before injection.
+ public string LibraryPath
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityId.cs b/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityId.cs
index 93c4614..1c7ec58 100644
--- a/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityId.cs
+++ b/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityId.cs
@@ -42,6 +42,33 @@ public ClientCapabilityId(string value)
/// Gets the Client capability for explicitly opted-in arbitrary Lua source execution.
public static ClientCapabilityId UnsafeLuaExecution => new("Client.UnsafeLuaExecution");
+ /// Gets the Client capability for owned target-memory allocations.
+ public static ClientCapabilityId Allocations => new("Client.Allocations");
+
+ /// Gets the Client capability for assembly, disassembly, comments, and Auto Assembler patches.
+ public static ClientCapabilityId Assembly => new("Client.Assembly");
+
+ /// Gets the Client capability for bounded remote execution and DLL injection.
+ public static ClientCapabilityId RemoteExecution => new("Client.RemoteExecution");
+
+ /// Gets the Client capability for debugger breakpoints and copied debugger events.
+ public static ClientCapabilityId Debugger => new("Client.Debugger");
+
+ /// Gets the Client capability for activation-scoped hotkeys.
+ public static ClientCapabilityId Hotkeys => new("Client.Hotkeys");
+
+ /// Gets the Client capability for activation-scoped timers.
+ public static ClientCapabilityId Timers => new("Client.Timers");
+
+ /// Gets the Client capability for observing and changing target speed.
+ public static ClientCapabilityId Speed => new("Client.Speed");
+
+ /// Gets the Client capability for target-memory and file hashing.
+ public static ClientCapabilityId Hashing => new("Client.Hashing");
+
+ /// Gets the Client capability for explicitly initialized DBVM operations and watches.
+ public static ClientCapabilityId Dbvm => new("Client.Dbvm");
+
///
public bool Equals(ClientCapabilityId other)
{
diff --git a/libs/CheatEngine.Client.Abstractions/Speed/ISpeedClient.cs b/libs/CheatEngine.Client.Abstractions/Speed/ISpeedClient.cs
new file mode 100644
index 0000000..292bc7e
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Speed/ISpeedClient.cs
@@ -0,0 +1,21 @@
+using CheatEngine.Client.Results;
+
+namespace CheatEngine.Client.Speed;
+
+/// Reads and updates the selected target's validated speed multiplier.
+public interface ISpeedClient
+{
+ /// Tries to get the selected target's current speed multiplier.
+ public bool TryGetMultiplier(out SpeedMultiplier multiplier, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Gets the selected target's current speed multiplier or throws when unavailable.
+ public SpeedMultiplier GetMultiplier(CancellationToken cancellationToken = default);
+
+ /// Tries to set a finite, strictly positive speed multiplier.
+ public bool TrySetMultiplier(SpeedMultiplier multiplier, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Sets a finite, strictly positive speed multiplier or throws when unavailable.
+ public void SetMultiplier(SpeedMultiplier multiplier, CancellationToken cancellationToken = default);
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Speed/SpeedMultiplier.cs b/libs/CheatEngine.Client.Abstractions/Speed/SpeedMultiplier.cs
new file mode 100644
index 0000000..a8a63d1
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Speed/SpeedMultiplier.cs
@@ -0,0 +1,24 @@
+namespace CheatEngine.Client.Speed;
+
+/// Represents a finite, strictly positive Client speed multiplier.
+public readonly record struct SpeedMultiplier
+{
+ /// Creates a speed multiplier.
+ /// is non-finite or not positive.
+ public SpeedMultiplier(double value)
+ {
+ if (!double.IsFinite(value) || value <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value),
+ "A speed multiplier must be finite and strictly positive.");
+ }
+
+ Value = value;
+ }
+
+ /// Gets the finite, strictly positive multiplier value.
+ public double Value
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Timers/ITimerClient.cs b/libs/CheatEngine.Client.Abstractions/Timers/ITimerClient.cs
new file mode 100644
index 0000000..11045c5
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Timers/ITimerClient.cs
@@ -0,0 +1,26 @@
+using System.Diagnostics.CodeAnalysis;
+
+using CheatEngine.Client.Events;
+using CheatEngine.Client.Results;
+
+namespace CheatEngine.Client.Timers;
+
+/// Registers Client-owned recurring timers for the current activation.
+public interface ITimerClient
+{
+ /// Tries to register a timer and its bounded copied tick stream.
+ public bool TryRegister(
+ TimerRequest request,
+ TimerHandler handler,
+ EventStreamOptions streamOptions,
+ [NotNullWhen(true)] out ITimerLease? lease,
+ out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Registers a timer or throws when the capability is unavailable or registration fails.
+ public ITimerLease Register(
+ TimerRequest request,
+ TimerHandler handler,
+ EventStreamOptions streamOptions,
+ CancellationToken cancellationToken = default);
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Timers/ITimerLease.cs b/libs/CheatEngine.Client.Abstractions/Timers/ITimerLease.cs
new file mode 100644
index 0000000..bb70118
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Timers/ITimerLease.cs
@@ -0,0 +1,13 @@
+using CheatEngine.Client.Events;
+
+namespace CheatEngine.Client.Timers;
+
+/// Owns one Client timer registration and its bounded copied tick stream.
+public interface ITimerLease : IEventStreamLease
+{
+ /// Gets the request used to create this timer.
+ public TimerRequest Request
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Timers/TimerHandler.cs b/libs/CheatEngine.Client.Abstractions/Timers/TimerHandler.cs
new file mode 100644
index 0000000..df79112
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Timers/TimerHandler.cs
@@ -0,0 +1,4 @@
+namespace CheatEngine.Client.Timers;
+
+/// Handles one copied timer tick synchronously without blocking the host callback thread.
+public delegate void TimerHandler(TimerTick tick);
diff --git a/libs/CheatEngine.Client.Abstractions/Timers/TimerRequest.cs b/libs/CheatEngine.Client.Abstractions/Timers/TimerRequest.cs
new file mode 100644
index 0000000..d2cc5a6
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Timers/TimerRequest.cs
@@ -0,0 +1,23 @@
+namespace CheatEngine.Client.Timers;
+
+/// Describes a recurring Client timer with an explicit positive interval.
+public readonly record struct TimerRequest
+{
+ /// Creates a recurring timer request.
+ /// is not strictly positive.
+ public TimerRequest(TimeSpan interval)
+ {
+ if (interval <= TimeSpan.Zero)
+ {
+ throw new ArgumentOutOfRangeException(nameof(interval), "A timer interval must be strictly positive.");
+ }
+
+ Interval = interval;
+ }
+
+ /// Gets the strictly positive timer interval.
+ public TimeSpan Interval
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Timers/TimerTick.cs b/libs/CheatEngine.Client.Abstractions/Timers/TimerTick.cs
new file mode 100644
index 0000000..022cd8b
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Timers/TimerTick.cs
@@ -0,0 +1,26 @@
+namespace CheatEngine.Client.Timers;
+
+/// Contains a copied timer tick.
+public readonly record struct TimerTick
+{
+ /// Creates a copied timer tick.
+ /// is negative.
+ public TimerTick(long sequence, DateTimeOffset occurredAt)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(sequence);
+ Sequence = sequence;
+ OccurredAt = occurredAt;
+ }
+
+ /// Gets the non-negative sequence number within this timer registration.
+ public long Sequence
+ {
+ get;
+ }
+
+ /// Gets the copied tick timestamp.
+ public DateTimeOffset OccurredAt
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Core/CheatEngineClient.cs b/libs/CheatEngine.Client.Core/CheatEngineClient.cs
index 4ce4fc2..d7aebc0 100644
--- a/libs/CheatEngine.Client.Core/CheatEngineClient.cs
+++ b/libs/CheatEngine.Client.Core/CheatEngineClient.cs
@@ -1,12 +1,21 @@
+using CheatEngine.Client.Allocations;
+using CheatEngine.Client.Assembly;
using CheatEngine.Client.Core.Infrastructure;
+using CheatEngine.Client.Dbvm;
+using CheatEngine.Client.Debugger;
using CheatEngine.Client.Dispatching;
+using CheatEngine.Client.Hashing;
+using CheatEngine.Client.Hotkeys;
using CheatEngine.Client.Inspection;
using CheatEngine.Client.Lua;
using CheatEngine.Client.Memory;
using CheatEngine.Client.Processes;
+using CheatEngine.Client.RemoteExecution;
using CheatEngine.Client.Runtime;
using CheatEngine.Client.Scanning;
+using CheatEngine.Client.Speed;
using CheatEngine.Client.Tables;
+using CheatEngine.Client.Timers;
namespace CheatEngine.Client.Core;
@@ -32,6 +41,15 @@ internal CheatEngineClient(
Inspection = domainServices.Inspection ?? throw new ArgumentNullException(nameof(domainServices));
Tables = domainServices.Tables ?? throw new ArgumentNullException(nameof(domainServices));
Lua = domainServices.Lua ?? throw new ArgumentNullException(nameof(domainServices));
+ Allocations = domainServices.Allocations ?? throw new ArgumentNullException(nameof(domainServices));
+ Assembly = domainServices.Assembly ?? throw new ArgumentNullException(nameof(domainServices));
+ RemoteExecution = domainServices.RemoteExecution ?? throw new ArgumentNullException(nameof(domainServices));
+ Debugger = domainServices.Debugger ?? throw new ArgumentNullException(nameof(domainServices));
+ Hotkeys = domainServices.Hotkeys ?? throw new ArgumentNullException(nameof(domainServices));
+ Timers = domainServices.Timers ?? throw new ArgumentNullException(nameof(domainServices));
+ Speed = domainServices.Speed ?? throw new ArgumentNullException(nameof(domainServices));
+ Hashing = domainServices.Hashing ?? throw new ArgumentNullException(nameof(domainServices));
+ Dbvm = domainServices.Dbvm ?? throw new ArgumentNullException(nameof(domainServices));
}
public long Epoch => _lifetime.Epoch;
@@ -81,4 +99,49 @@ public ILuaClient Lua
{
get;
}
+
+ public IAllocationClient Allocations
+ {
+ get;
+ }
+
+ public IAssemblyClient Assembly
+ {
+ get;
+ }
+
+ public IRemoteExecutionClient RemoteExecution
+ {
+ get;
+ }
+
+ public IDebuggerClient Debugger
+ {
+ get;
+ }
+
+ public IHotkeyClient Hotkeys
+ {
+ get;
+ }
+
+ public ITimerClient Timers
+ {
+ get;
+ }
+
+ public ISpeedClient Speed
+ {
+ get;
+ }
+
+ public IHashingClient Hashing
+ {
+ get;
+ }
+
+ public IDbvmClient Dbvm
+ {
+ get;
+ }
}
diff --git a/libs/CheatEngine.Client.Core/CheatEngineClientDomainServices.cs b/libs/CheatEngine.Client.Core/CheatEngineClientDomainServices.cs
index 7ce2156..c7358f9 100644
--- a/libs/CheatEngine.Client.Core/CheatEngineClientDomainServices.cs
+++ b/libs/CheatEngine.Client.Core/CheatEngineClientDomainServices.cs
@@ -1,9 +1,18 @@
-using CheatEngine.Client.Inspection;
+using CheatEngine.Client.Allocations;
+using CheatEngine.Client.Assembly;
+using CheatEngine.Client.Dbvm;
+using CheatEngine.Client.Debugger;
+using CheatEngine.Client.Hashing;
+using CheatEngine.Client.Hotkeys;
+using CheatEngine.Client.Inspection;
using CheatEngine.Client.Lua;
using CheatEngine.Client.Memory;
using CheatEngine.Client.Processes;
+using CheatEngine.Client.RemoteExecution;
using CheatEngine.Client.Scanning;
+using CheatEngine.Client.Speed;
using CheatEngine.Client.Tables;
+using CheatEngine.Client.Timers;
namespace CheatEngine.Client.Core;
@@ -15,4 +24,13 @@ internal sealed record CheatEngineClientDomainServices(
IValueScanner Scans,
IInspectionClient Inspection,
ITableClient Tables,
- ILuaClient Lua);
+ ILuaClient Lua,
+ IAllocationClient Allocations,
+ IAssemblyClient Assembly,
+ IRemoteExecutionClient RemoteExecution,
+ IDebuggerClient Debugger,
+ IHotkeyClient Hotkeys,
+ ITimerClient Timers,
+ ISpeedClient Speed,
+ IHashingClient Hashing,
+ IDbvmClient Dbvm);
diff --git a/libs/CheatEngine.Client.Core/Dispatching/IMainThreadInvoker.cs b/libs/CheatEngine.Client.Core/Dispatching/IMainThreadInvoker.cs
index db33040..710fa52 100644
--- a/libs/CheatEngine.Client.Core/Dispatching/IMainThreadInvoker.cs
+++ b/libs/CheatEngine.Client.Core/Dispatching/IMainThreadInvoker.cs
@@ -5,4 +5,16 @@ internal interface IMainThreadInvoker
public Exception? Invoke(Action callback);
public MainThreadInvocationResult Invoke(Func callback);
+
+ public Exception? Invoke(Action callback, TState state)
+ {
+ ArgumentNullException.ThrowIfNull(callback);
+ return Invoke(() => callback(state));
+ }
+
+ public MainThreadInvocationResult Invoke(Func callback, TState state)
+ {
+ ArgumentNullException.ThrowIfNull(callback);
+ return Invoke(() => callback(state));
+ }
}
diff --git a/libs/CheatEngine.Client.Core/Dispatching/IStatefulCheatEngineDispatcher.cs b/libs/CheatEngine.Client.Core/Dispatching/IStatefulCheatEngineDispatcher.cs
new file mode 100644
index 0000000..1827b20
--- /dev/null
+++ b/libs/CheatEngine.Client.Core/Dispatching/IStatefulCheatEngineDispatcher.cs
@@ -0,0 +1,17 @@
+using System.Diagnostics.CodeAnalysis;
+
+using CheatEngine.Client.Results;
+
+namespace CheatEngine.Client.Core.Dispatching;
+
+/// Internal fast path that carries explicit state into the SDK main-thread dispatcher.
+///
+/// The public dispatcher contract deliberately remains small and source-compatible for third-party
+/// implementations. Core services retain the closure-based public fallback only for non-SDK dispatchers.
+///
+internal interface IStatefulCheatEngineDispatcher
+{
+ public bool TryInvoke(TState state, Func callback,
+ [MaybeNullWhen(false)] out TResult result, out CheatEngineFailure failure,
+ CancellationToken cancellationToken);
+}
diff --git a/libs/CheatEngine.Client.Core/Dispatching/SdkMainThreadDispatcher.cs b/libs/CheatEngine.Client.Core/Dispatching/SdkMainThreadDispatcher.cs
index b4395f5..eb0a510 100644
--- a/libs/CheatEngine.Client.Core/Dispatching/SdkMainThreadDispatcher.cs
+++ b/libs/CheatEngine.Client.Core/Dispatching/SdkMainThreadDispatcher.cs
@@ -10,7 +10,7 @@
namespace CheatEngine.Client.Core.Dispatching;
/// Adapts the SDK's synchronous main-thread dispatcher without retaining Lua state.
-internal sealed class SdkMainThreadDispatcher : ICheatEngineDispatcher
+internal sealed class SdkMainThreadDispatcher : ICheatEngineDispatcher, IStatefulCheatEngineDispatcher
{
private const string _invokeOperation = "Dispatcher.Invoke";
@@ -132,6 +132,93 @@ public T Invoke(Func callback, CancellationToken cancellationToken = defau
return ThrowFailure(failure);
}
+ public bool TryInvoke(TState state, Func callback,
+ [MaybeNullWhen(false)] out TResult result, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(callback);
+ _lifetime.ThrowIfDispatchAllowed(_invokeOperation);
+ if (cancellationToken.IsCancellationRequested)
+ {
+ result = default;
+ failure = CoreFailureFactory.Cancelled(_invokeOperation);
+ return false;
+ }
+
+ MainThreadInvocationResult callbackResult;
+ try
+ {
+ callbackResult = _mainThread.Invoke(callback, state);
+ }
+ catch (CheatEngineActivationExpiredException)
+ {
+ result = default;
+ throw;
+ }
+ catch (Exception exception) when (!_lifetime.IsActivationCurrent)
+ {
+ result = default;
+ throw new CheatEngineActivationExpiredException(_invokeOperation,
+ "The Cheat Engine plugin lifecycle changed while dispatching work.", exception);
+ }
+ catch (Exception exception)
+ {
+ result = default;
+ failure = CoreFailureFactory.FromException(_invokeOperation, exception);
+ return false;
+ }
+
+ if (callbackResult.Exception is not null)
+ {
+ result = default;
+ RethrowCallback(callbackResult.Exception);
+ }
+
+ result = callbackResult.Result;
+ failure = default;
+ return true;
+ }
+
+ public bool TryInvoke(TState state, Action callback, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(callback);
+ _lifetime.ThrowIfDispatchAllowed(_invokeOperation);
+ if (cancellationToken.IsCancellationRequested)
+ {
+ failure = CoreFailureFactory.Cancelled(_invokeOperation);
+ return false;
+ }
+
+ Exception? callbackException;
+ try
+ {
+ callbackException = _mainThread.Invoke(callback, state);
+ }
+ catch (CheatEngineActivationExpiredException)
+ {
+ throw;
+ }
+ catch (Exception exception) when (!_lifetime.IsActivationCurrent)
+ {
+ throw new CheatEngineActivationExpiredException(_invokeOperation,
+ "The Cheat Engine plugin lifecycle changed while dispatching work.", exception);
+ }
+ catch (Exception exception)
+ {
+ failure = CoreFailureFactory.FromException(_invokeOperation, exception);
+ return false;
+ }
+
+ if (callbackException is not null)
+ {
+ RethrowCallback(callbackException);
+ }
+
+ failure = default;
+ return true;
+ }
+
private static T ThrowFailure(CheatEngineFailure failure)
{
failure.Throw();
@@ -169,6 +256,59 @@ private static MainThreadInvocationResult InvokeCallback(Func callback)
}
}
+ private static Exception? InvokeCallback(Action callback, TState state)
+ {
+ try
+ {
+ callback(state);
+ return null;
+ }
+ catch (Exception exception)
+ {
+ return exception;
+ }
+ }
+
+ private static MainThreadInvocationResult InvokeCallback(
+ Func callback,
+ TState state)
+ {
+ try
+ {
+ return new MainThreadInvocationResult(callback(state), null);
+ }
+ catch (Exception exception)
+ {
+ return new MainThreadInvocationResult(default!, exception);
+ }
+ }
+
+ private static Exception? InvokeCallback(CallbackState invocation)
+ {
+ try
+ {
+ invocation.Callback(invocation.State);
+ return null;
+ }
+ catch (Exception exception)
+ {
+ return exception;
+ }
+ }
+
+ private static MainThreadInvocationResult InvokeCallback(
+ CallbackState invocation)
+ {
+ try
+ {
+ return new MainThreadInvocationResult(invocation.Callback(invocation.State), null);
+ }
+ catch (Exception exception)
+ {
+ return new MainThreadInvocationResult(default!, exception);
+ }
+ }
+
private sealed class SdkMainThreadInvoker : IMainThreadInvoker
{
internal static SdkMainThreadInvoker Instance
@@ -185,5 +325,23 @@ public MainThreadInvocationResult Invoke(Func callback)
{
return MainThread.Invoke(static function => InvokeCallback(function), callback);
}
+
+ public Exception? Invoke(Action callback, TState state)
+ {
+ return MainThread.Invoke(static invocation => InvokeCallback(invocation),
+ new CallbackState(callback, state));
+ }
+
+ public MainThreadInvocationResult Invoke(Func callback,
+ TState state)
+ {
+ return MainThread.Invoke(
+ static invocation => InvokeCallback(invocation),
+ new CallbackState(callback, state));
+ }
}
+
+ private readonly record struct CallbackState(Action Callback, TState State);
+
+ private readonly record struct CallbackState(Func Callback, TState State);
}
diff --git a/libs/CheatEngine.Client.Core/Domains/Allocations/UnavailableAllocationClient.cs b/libs/CheatEngine.Client.Core/Domains/Allocations/UnavailableAllocationClient.cs
new file mode 100644
index 0000000..5626467
--- /dev/null
+++ b/libs/CheatEngine.Client.Core/Domains/Allocations/UnavailableAllocationClient.cs
@@ -0,0 +1,26 @@
+using System.Diagnostics.CodeAnalysis;
+
+using CheatEngine.Client.Allocations;
+using CheatEngine.Client.Core.Domains.Events;
+using CheatEngine.Client.Results;
+
+namespace CheatEngine.Client.Core.Domains.Allocations;
+
+/// Preserves the allocation contract while its SDK ownership factory awaits live-host validation.
+internal sealed class UnavailableAllocationClient : IAllocationClient
+{
+ public bool TryAllocate(TargetAllocationRequest request, [NotNullWhen(true)] out ITargetMemoryLease? lease,
+ out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default)
+ {
+ lease = null;
+ failure = UnavailableCapabilityFailure.Create("Target allocations", "Allocations.Allocate", cancellationToken);
+ return false;
+ }
+
+ public ITargetMemoryLease Allocate(TargetAllocationRequest request, CancellationToken cancellationToken = default)
+ {
+ _ = TryAllocate(request, out _, out CheatEngineFailure failure, cancellationToken);
+ return UnavailableCapabilityFailure.Throw(failure);
+ }
+}
diff --git a/libs/CheatEngine.Client.Core/Domains/Assembly/UnavailableAssemblyClient.cs b/libs/CheatEngine.Client.Core/Domains/Assembly/UnavailableAssemblyClient.cs
new file mode 100644
index 0000000..94daf0a
--- /dev/null
+++ b/libs/CheatEngine.Client.Core/Domains/Assembly/UnavailableAssemblyClient.cs
@@ -0,0 +1,107 @@
+using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
+
+using CheatEngine.Client.Assembly;
+using CheatEngine.Client.Core.Domains.Events;
+using CheatEngine.Client.Results;
+using CheatEngine.SDK.Engine.Values;
+
+namespace CheatEngine.Client.Core.Domains.Assembly;
+
+/// Preserves the assembly and patch surface until Auto Assembler ownership passes its live-host gate.
+internal sealed class UnavailableAssemblyClient : IAssemblyClient
+{
+ public bool TryDisassemble(Address address, out AssemblyInstructionSnapshot instruction,
+ out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default)
+ {
+ instruction = default;
+ failure = CreateFailure("Assembly.Disassemble", cancellationToken);
+ return false;
+ }
+
+ public AssemblyInstructionSnapshot Disassemble(Address address, CancellationToken cancellationToken = default)
+ {
+ _ = TryDisassemble(address, out _, out CheatEngineFailure failure, cancellationToken);
+ return UnavailableCapabilityFailure.Throw(failure);
+ }
+
+ public bool TryGetInstructionSize(Address address, out int size, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default)
+ {
+ size = default;
+ failure = CreateFailure("Assembly.GetInstructionSize", cancellationToken);
+ return false;
+ }
+
+ public int GetInstructionSize(Address address, CancellationToken cancellationToken = default)
+ {
+ _ = TryGetInstructionSize(address, out _, out CheatEngineFailure failure, cancellationToken);
+ return UnavailableCapabilityFailure.Throw(failure);
+ }
+
+ public bool TryGetPreviousInstruction(Address address, out Address previousAddress, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default)
+ {
+ previousAddress = default;
+ failure = CreateFailure("Assembly.GetPreviousInstruction", cancellationToken);
+ return false;
+ }
+
+ public Address GetPreviousInstruction(Address address, CancellationToken cancellationToken = default)
+ {
+ _ = TryGetPreviousInstruction(address, out _, out CheatEngineFailure failure, cancellationToken);
+ return UnavailableCapabilityFailure.Throw(failure);
+ }
+
+ public bool TryGetComment(Address address, [NotNullWhen(true)] out string? comment, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default)
+ {
+ comment = null;
+ failure = CreateFailure("Assembly.GetComment", cancellationToken);
+ return false;
+ }
+
+ public string GetComment(Address address, CancellationToken cancellationToken = default)
+ {
+ _ = TryGetComment(address, out _, out CheatEngineFailure failure, cancellationToken);
+ return UnavailableCapabilityFailure.Throw(failure);
+ }
+
+ public bool TryAssemble(AssemblyInstructionRequest request, out ImmutableArray bytes,
+ out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default)
+ {
+ bytes = default;
+ failure = CreateFailure("Assembly.Assemble", cancellationToken);
+ return false;
+ }
+
+ public ImmutableArray Assemble(AssemblyInstructionRequest request,
+ CancellationToken cancellationToken = default)
+ {
+ _ = TryAssemble(request, out _, out CheatEngineFailure failure, cancellationToken);
+ return UnavailableCapabilityFailure.Throw>(failure);
+ }
+
+ public bool TryApplyPatch(AutoAssemblerScript script, [NotNullWhen(true)] out IAutoAssemblerPatchLease? lease,
+ out CheatEngineFailure failure, CancellationToken cancellationToken = default)
+ {
+ lease = null;
+ failure = CreateFailure("Assembly.ApplyPatch", cancellationToken);
+ return false;
+ }
+
+ public IAutoAssemblerPatchLease ApplyPatch(AutoAssemblerScript script,
+ CancellationToken cancellationToken = default)
+ {
+ _ = TryApplyPatch(script, out _, out CheatEngineFailure failure, cancellationToken);
+ return UnavailableCapabilityFailure.Throw(failure);
+ }
+
+ private static CheatEngineFailure CreateFailure(string operation, CancellationToken cancellationToken)
+ {
+ return UnavailableCapabilityFailure.Create("Assembly, disassembly, and Auto Assembler patches", operation,
+ cancellationToken);
+ }
+}
diff --git a/libs/CheatEngine.Client.Core/Domains/Dbvm/UnavailableDbvmClient.cs b/libs/CheatEngine.Client.Core/Domains/Dbvm/UnavailableDbvmClient.cs
new file mode 100644
index 0000000..801de60
--- /dev/null
+++ b/libs/CheatEngine.Client.Core/Domains/Dbvm/UnavailableDbvmClient.cs
@@ -0,0 +1,65 @@
+using System.Diagnostics.CodeAnalysis;
+
+using CheatEngine.Client.Core.Domains.Events;
+using CheatEngine.Client.Dbvm;
+using CheatEngine.Client.Events;
+using CheatEngine.Client.Results;
+
+namespace CheatEngine.Client.Core.Domains.Dbvm;
+
+/// Observes no inferred DBVM state and never initializes DBVM before its explicit live-host gate passes.
+internal sealed class UnavailableDbvmClient : IDbvmClient
+{
+ public bool TryGetStatus(out DbvmStatusSnapshot status, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default)
+ {
+ status = default;
+ failure = CreateFailure("Dbvm.GetStatus", cancellationToken);
+ return false;
+ }
+
+ public DbvmStatusSnapshot GetStatus(CancellationToken cancellationToken = default)
+ {
+ _ = TryGetStatus(out _, out CheatEngineFailure failure, cancellationToken);
+ return UnavailableCapabilityFailure.Throw(failure);
+ }
+
+ public bool TryInitialize(DbvmInitializationRequest request, out DbvmStatusSnapshot status,
+ out CheatEngineFailure failure, CancellationToken cancellationToken = default)
+ {
+ status = default;
+ failure = CreateFailure("Dbvm.Initialize", cancellationToken);
+ return false;
+ }
+
+ public DbvmStatusSnapshot Initialize(DbvmInitializationRequest request,
+ CancellationToken cancellationToken = default)
+ {
+ _ = TryInitialize(request, out _, out CheatEngineFailure failure, cancellationToken);
+ return UnavailableCapabilityFailure.Throw(failure);
+ }
+
+ public bool TryRegisterWatch(DbvmWatchRequest request, DbvmWatchHandler handler, EventStreamOptions streamOptions,
+ [NotNullWhen(true)] out IDbvmWatchLease? lease, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(handler);
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(streamOptions.Capacity);
+ lease = null;
+ failure = CreateFailure("Dbvm.RegisterWatch", cancellationToken);
+ return false;
+ }
+
+ public IDbvmWatchLease RegisterWatch(DbvmWatchRequest request, DbvmWatchHandler handler,
+ EventStreamOptions streamOptions, CancellationToken cancellationToken = default)
+ {
+ _ = TryRegisterWatch(request, handler, streamOptions, out _, out CheatEngineFailure failure, cancellationToken);
+ return UnavailableCapabilityFailure.Throw(failure);
+ }
+
+ private static CheatEngineFailure CreateFailure(string operation, CancellationToken cancellationToken)
+ {
+ return UnavailableCapabilityFailure.Create("DBVM observation, explicit initialization, and watches", operation,
+ cancellationToken);
+ }
+}
diff --git a/libs/CheatEngine.Client.Core/Domains/Debugger/UnavailableDebuggerClient.cs b/libs/CheatEngine.Client.Core/Domains/Debugger/UnavailableDebuggerClient.cs
new file mode 100644
index 0000000..9e00343
--- /dev/null
+++ b/libs/CheatEngine.Client.Core/Domains/Debugger/UnavailableDebuggerClient.cs
@@ -0,0 +1,33 @@
+using System.Diagnostics.CodeAnalysis;
+
+using CheatEngine.Client.Core.Domains.Events;
+using CheatEngine.Client.Debugger;
+using CheatEngine.Client.Events;
+using CheatEngine.Client.Results;
+
+namespace CheatEngine.Client.Core.Domains.Debugger;
+
+/// Preserves copied breakpoint semantics until debugger callback ownership and reactivation pass a live gate.
+internal sealed class UnavailableDebuggerClient : IDebuggerClient
+{
+ public bool TryRegisterBreakpoint(BreakpointRequest request, BreakpointHandler handler,
+ EventStreamOptions streamOptions,
+ [NotNullWhen(true)] out IBreakpointLease? lease, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(handler);
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(streamOptions.Capacity);
+ lease = null;
+ failure = UnavailableCapabilityFailure.Create("Debugger breakpoints", "Debugger.RegisterBreakpoint",
+ cancellationToken);
+ return false;
+ }
+
+ public IBreakpointLease RegisterBreakpoint(BreakpointRequest request, BreakpointHandler handler,
+ EventStreamOptions streamOptions, CancellationToken cancellationToken = default)
+ {
+ _ = TryRegisterBreakpoint(request, handler, streamOptions, out _, out CheatEngineFailure failure,
+ cancellationToken);
+ return UnavailableCapabilityFailure.Throw(failure);
+ }
+}
diff --git a/libs/CheatEngine.Client.Core/Domains/Events/BoundedEventStream.cs b/libs/CheatEngine.Client.Core/Domains/Events/BoundedEventStream.cs
new file mode 100644
index 0000000..06fb809
--- /dev/null
+++ b/libs/CheatEngine.Client.Core/Domains/Events/BoundedEventStream.cs
@@ -0,0 +1,478 @@
+using System.Runtime.CompilerServices;
+
+using CheatEngine.Client.Events;
+
+namespace CheatEngine.Client.Core.Domains.Events;
+
+///
+/// Provides a bounded, non-blocking handoff from a Cheat Engine callback to an asynchronous consumer. Callback
+/// admission is synchronous and never waits for a reader; reader continuations always run asynchronously.
+///
+/// The copied event type.
+internal sealed class BoundedEventStream : IAsyncEnumerable, IDisposable
+{
+ private readonly T[] _buffer;
+ private readonly object _gate = new();
+ private readonly EventStreamOverflowPolicy _overflowPolicy;
+ private readonly LinkedList _pendingReads = [];
+ private bool _completed;
+ private Exception? _completionError;
+ private int _count;
+ private bool _disposed;
+ private bool _isAdmissionOpen = true;
+ private long _lostCount;
+ private int _readIndex;
+ private int _writeIndex;
+
+ /// Initializes a stream with the supplied bounded-buffer policy.
+ internal BoundedEventStream(EventStreamOptions options)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(options.Capacity);
+ if (!Enum.IsDefined(options.OverflowPolicy))
+ {
+ throw new ArgumentOutOfRangeException(nameof(options));
+ }
+
+ _buffer = new T[options.Capacity];
+ _overflowPolicy = options.OverflowPolicy;
+ }
+
+ /// Gets the number of events intentionally not delivered because the bounded buffer was full.
+ internal long LostCount
+ {
+ get
+ {
+ lock (_gate)
+ {
+ return _lostCount;
+ }
+ }
+ }
+
+ /// Gets whether callbacks can still publish into the stream.
+ internal bool IsAdmissionOpen
+ {
+ get
+ {
+ lock (_gate)
+ {
+ return _isAdmissionOpen;
+ }
+ }
+ }
+
+ /// Gets whether no further events will be admitted.
+ internal bool IsCompleted
+ {
+ get
+ {
+ lock (_gate)
+ {
+ return _completed;
+ }
+ }
+ }
+
+ /// Returns an asynchronous enumerator over copied events.
+ public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default)
+ {
+ return new Enumerator(this, cancellationToken);
+ }
+
+ /// Closes admission and discards buffered events during deterministic subscription teardown.
+ public void Dispose()
+ {
+ List? pendingReads = null;
+ lock (_gate)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+ _isAdmissionOpen = false;
+ _completed = true;
+ ClearBuffer();
+ pendingReads = DetachAllPendingReads();
+ }
+
+ CompletePendingReads(pendingReads, ReadResult.End);
+ }
+
+ ///
+ /// Attempts to publish a copied callback event without waiting for an asynchronous consumer. A
+ ///
+ /// result means admission has closed or the selected overflow policy terminated the subscription.
+ ///
+ internal bool TryPublish(T value)
+ {
+ PendingRead? pendingRead = null;
+ lock (_gate)
+ {
+ if (!_isAdmissionOpen)
+ {
+ return false;
+ }
+
+ pendingRead = DetachNextPendingRead();
+ if (pendingRead is null)
+ {
+ if (_count < _buffer.Length)
+ {
+ Enqueue(value);
+ return true;
+ }
+
+ switch (_overflowPolicy)
+ {
+ case EventStreamOverflowPolicy.DropOldest:
+ _buffer[_writeIndex] = value;
+ _writeIndex = NextIndex(_writeIndex);
+ _readIndex = _writeIndex;
+ _lostCount++;
+ return true;
+
+ case EventStreamOverflowPolicy.DropNewest:
+ _lostCount++;
+ return true;
+
+ case EventStreamOverflowPolicy.FailSubscription:
+ _lostCount++;
+ CompleteCore(new InvalidOperationException(
+ "The bounded callback stream overflowed and the subscription was closed."), true);
+ return false;
+
+ default:
+ throw new InvalidOperationException(
+ "The bounded callback stream has an unknown overflow policy.");
+ }
+ }
+ }
+
+ pendingRead.Complete(new ReadResult(value));
+ return true;
+ }
+
+ /// Closes admission and lets readers drain the copied events already accepted by the stream.
+ internal void Complete()
+ {
+ lock (_gate)
+ {
+ CompleteCore(null, false);
+ }
+ }
+
+ /// Closes callback admission without completing existing readers until the host callback is neutralized.
+ internal void CloseAdmission()
+ {
+ lock (_gate)
+ {
+ _isAdmissionOpen = false;
+ }
+ }
+
+ /// Closes admission and completes all consumers with the supplied failure.
+ internal void Complete(Exception error)
+ {
+ ArgumentNullException.ThrowIfNull(error);
+ lock (_gate)
+ {
+ CompleteCore(error, true);
+ }
+ }
+
+ private ValueTask ReadAsync(CancellationToken cancellationToken)
+ {
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return ValueTask.FromCanceled(cancellationToken);
+ }
+
+ PendingRead? pendingRead = null;
+ lock (_gate)
+ {
+ if (_count != 0)
+ {
+ return ValueTask.FromResult(new ReadResult(Dequeue()));
+ }
+
+ if (_completionError is not null)
+ {
+ return ValueTask.FromException(_completionError);
+ }
+
+ if (_completed)
+ {
+ return ValueTask.FromResult(ReadResult.End);
+ }
+
+ pendingRead = new PendingRead(this, cancellationToken);
+ pendingRead.Node = _pendingReads.AddLast(pendingRead);
+ }
+
+ pendingRead.RegisterCancellation();
+ return new ValueTask(pendingRead.Task);
+ }
+
+ private void CancelPendingRead(PendingRead pendingRead, CancellationToken cancellationToken)
+ {
+ lock (_gate)
+ {
+ if (pendingRead.Node is null || !pendingRead.TryBeginCompletion())
+ {
+ return;
+ }
+
+ _pendingReads.Remove(pendingRead.Node);
+ pendingRead.Node = null;
+ }
+
+ pendingRead.Cancel(cancellationToken);
+ }
+
+ private PendingRead? DetachNextPendingRead()
+ {
+ while (_pendingReads.First is LinkedListNode node)
+ {
+ PendingRead pendingRead = node.Value;
+ _pendingReads.Remove(node);
+ pendingRead.Node = null;
+ if (pendingRead.TryBeginCompletion())
+ {
+ return pendingRead;
+ }
+ }
+
+ return null;
+ }
+
+ private List? DetachAllPendingReads()
+ {
+ if (_pendingReads.Count == 0)
+ {
+ return null;
+ }
+
+ List detached = new(_pendingReads.Count);
+ while (_pendingReads.First is LinkedListNode node)
+ {
+ PendingRead pendingRead = node.Value;
+ _pendingReads.RemoveFirst();
+ pendingRead.Node = null;
+ if (pendingRead.TryBeginCompletion())
+ {
+ detached.Add(pendingRead);
+ }
+ }
+
+ return detached;
+ }
+
+ private void CompleteCore(Exception? error, bool clearBufferedEvents)
+ {
+ if (_completed)
+ {
+ return;
+ }
+
+ _isAdmissionOpen = false;
+ _completed = true;
+ _completionError = error;
+ if (clearBufferedEvents)
+ {
+ ClearBuffer();
+ }
+
+ List? pendingReads = DetachAllPendingReads();
+ if (pendingReads is null)
+ {
+ return;
+ }
+
+ if (error is null)
+ {
+ CompletePendingReads(pendingReads, ReadResult.End);
+ return;
+ }
+
+ CompletePendingReads(pendingReads, error);
+ }
+
+ private void Enqueue(T value)
+ {
+ _buffer[_writeIndex] = value;
+ _writeIndex = NextIndex(_writeIndex);
+ _count++;
+ }
+
+ private T Dequeue()
+ {
+ T value = _buffer[_readIndex];
+ _buffer[_readIndex] = default!;
+ _readIndex = NextIndex(_readIndex);
+ _count--;
+ return value;
+ }
+
+ private void ClearBuffer()
+ {
+ if (RuntimeHelpers.IsReferenceOrContainsReferences())
+ {
+ Array.Clear(_buffer);
+ }
+
+ _readIndex = 0;
+ _writeIndex = 0;
+ _count = 0;
+ }
+
+ private int NextIndex(int index)
+ {
+ return index == _buffer.Length - 1 ? 0 : index + 1;
+ }
+
+ private static void CompletePendingReads(IEnumerable? pendingReads, ReadResult result)
+ {
+ if (pendingReads is null)
+ {
+ return;
+ }
+
+ foreach (PendingRead pendingRead in pendingReads)
+ {
+ pendingRead.Complete(result);
+ }
+ }
+
+ private static void CompletePendingReads(IEnumerable pendingReads, Exception error)
+ {
+ foreach (PendingRead pendingRead in pendingReads)
+ {
+ pendingRead.Fail(error);
+ }
+ }
+
+ private readonly record struct ReadResult(bool HasValue, T? Value)
+ {
+ internal ReadResult(T value)
+ : this(true, value)
+ {
+ }
+
+ internal static ReadResult End => new(false, default);
+ }
+
+ private sealed class PendingRead(BoundedEventStream owner, CancellationToken cancellationToken)
+ {
+ private readonly CancellationToken _cancellationToken = cancellationToken;
+ private readonly BoundedEventStream _owner = owner;
+
+ private readonly TaskCompletionSource _source = new(
+ TaskCreationOptions.RunContinuationsAsynchronously);
+
+ private int _completionStarted;
+ private CancellationTokenRegistration _registration;
+
+ internal LinkedListNode? Node
+ {
+ get;
+ set;
+ }
+
+ internal Task Task => _source.Task;
+
+ internal bool TryBeginCompletion()
+ {
+ return Interlocked.CompareExchange(ref _completionStarted, 1, 0) == 0;
+ }
+
+ internal void RegisterCancellation()
+ {
+ if (!_cancellationToken.CanBeCanceled)
+ {
+ return;
+ }
+
+ _registration = _cancellationToken.UnsafeRegister(static state =>
+ {
+ PendingRead pendingRead = (PendingRead) state!;
+ pendingRead._owner.CancelPendingRead(pendingRead, pendingRead._cancellationToken);
+ }, this);
+
+ if (Volatile.Read(ref _completionStarted) != 0)
+ {
+ _registration.Unregister();
+ }
+ }
+
+ internal void Complete(ReadResult result)
+ {
+ _registration.Unregister();
+ _source.TrySetResult(result);
+ }
+
+ internal void Cancel(CancellationToken cancellationToken)
+ {
+ _source.TrySetCanceled(cancellationToken);
+ }
+
+ internal void Fail(Exception error)
+ {
+ _registration.Unregister();
+ _source.TrySetException(error);
+ }
+ }
+
+ private sealed class Enumerator(BoundedEventStream owner, CancellationToken cancellationToken)
+ : IAsyncEnumerator
+ {
+ private readonly CancellationToken _cancellationToken = cancellationToken;
+ private readonly BoundedEventStream _owner = owner;
+ private int _disposed;
+ private int _moveNextInProgress;
+
+ public T Current
+ {
+ get;
+ private set;
+ } = default!;
+
+ public async ValueTask MoveNextAsync()
+ {
+ if (Volatile.Read(ref _disposed) != 0)
+ {
+ return false;
+ }
+
+ if (Interlocked.Exchange(ref _moveNextInProgress, 1) != 0)
+ {
+ throw new InvalidOperationException(
+ "Concurrent MoveNextAsync calls are not supported for one event-stream enumerator.");
+ }
+
+ try
+ {
+ ReadResult result = await _owner.ReadAsync(_cancellationToken).ConfigureAwait(false);
+ if (!result.HasValue)
+ {
+ Current = default!;
+ return false;
+ }
+
+ Current = result.Value!;
+ return true;
+ }
+ finally
+ {
+ Volatile.Write(ref _moveNextInProgress, 0);
+ }
+ }
+
+ public ValueTask DisposeAsync()
+ {
+ Interlocked.Exchange(ref _disposed, 1);
+ return ValueTask.CompletedTask;
+ }
+ }
+}
diff --git a/libs/CheatEngine.Client.Core/Domains/Events/EventStreamLease.cs b/libs/CheatEngine.Client.Core/Domains/Events/EventStreamLease.cs
new file mode 100644
index 0000000..17c937b
--- /dev/null
+++ b/libs/CheatEngine.Client.Core/Domains/Events/EventStreamLease.cs
@@ -0,0 +1,94 @@
+using System.Runtime.ExceptionServices;
+
+using CheatEngine.Client.Events;
+
+namespace CheatEngine.Client.Core.Domains.Events;
+
+///
+/// Owns the release sequence for one host callback and its copied, bounded Client event stream. The sequence is
+/// deliberately ordered so that the host can no longer enqueue an event before consumers observe completion.
+///
+/// The copied event type admitted by the callback.
+internal sealed class EventStreamLease(
+ BoundedEventStream stream,
+ Action neutralizeCallback,
+ Action releaseHostRegistration,
+ Action>? untrack = null) : IEventStreamLease
+{
+ private readonly Lock _gate = new();
+
+ private readonly Action _neutralizeCallback =
+ neutralizeCallback ?? throw new ArgumentNullException(nameof(neutralizeCallback));
+
+ private readonly Action _releaseHostRegistration = releaseHostRegistration ??
+ throw new ArgumentNullException(nameof(releaseHostRegistration));
+
+ private readonly BoundedEventStream _stream = stream ?? throw new ArgumentNullException(nameof(stream));
+ private readonly Action>? _untrack = untrack;
+ private int _disposing;
+ private int _released;
+
+ ///
+ public IAsyncEnumerable Events => _stream;
+
+ ///
+ public long DroppedEventCount => _stream.LostCount;
+
+ ///
+ public bool IsReleased => Volatile.Read(ref _released) != 0;
+
+ ///
+ /// Releases the subscription in lifecycle order: stop admission, neutralize the host callback, complete the
+ /// stream, release the host registration, and finally untrack the lease.
+ ///
+ public void Dispose()
+ {
+ lock (_gate)
+ {
+ if (Volatile.Read(ref _released) != 0 || Volatile.Read(ref _disposing) != 0)
+ {
+ return;
+ }
+
+ Volatile.Write(ref _disposing, 1);
+ Exception? firstFailure = null;
+ firstFailure = RunCleanupStep(_stream.CloseAdmission, firstFailure);
+ firstFailure = RunCleanupStep(_neutralizeCallback, firstFailure);
+ firstFailure = RunCleanupStep(_stream.Complete, firstFailure);
+ firstFailure = RunCleanupStep(_releaseHostRegistration, firstFailure);
+ firstFailure = RunCleanupStep(Untrack, firstFailure);
+ Volatile.Write(ref _released, 1);
+ Volatile.Write(ref _disposing, 0);
+
+ if (firstFailure is not null)
+ {
+ ExceptionDispatchInfo.Capture(firstFailure).Throw();
+ }
+ }
+ }
+
+ /// Attempts to admit one copied event from the host callback without waiting for a consumer.
+ internal bool TryPublish(TEvent value)
+ {
+ return _stream.TryPublish(value);
+ }
+
+ private void Untrack()
+ {
+ _untrack?.Invoke(this);
+ }
+
+ private static Exception? RunCleanupStep(Action cleanup, Exception? firstFailure)
+ {
+ try
+ {
+ cleanup();
+ }
+ catch (Exception exception)
+ {
+ return firstFailure ?? exception;
+ }
+
+ return firstFailure;
+ }
+}
diff --git a/libs/CheatEngine.Client.Core/Domains/Events/UnavailableCapabilityFailure.cs b/libs/CheatEngine.Client.Core/Domains/Events/UnavailableCapabilityFailure.cs
new file mode 100644
index 0000000..15e68e8
--- /dev/null
+++ b/libs/CheatEngine.Client.Core/Domains/Events/UnavailableCapabilityFailure.cs
@@ -0,0 +1,34 @@
+using System.Diagnostics;
+
+using CheatEngine.Client.Results;
+
+namespace CheatEngine.Client.Core.Domains.Events;
+
+/// Creates the uniform capability-gated result used by high-level domains awaiting their live-host gate.
+internal static class UnavailableCapabilityFailure
+{
+ internal static CheatEngineFailure Create(string capabilityName, string operation,
+ CancellationToken cancellationToken)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(capabilityName);
+ ArgumentException.ThrowIfNullOrWhiteSpace(operation);
+
+ return cancellationToken.IsCancellationRequested
+ ? 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.");
+ }
+
+ internal static T Throw(CheatEngineFailure failure)
+ {
+ failure.Throw();
+ throw new UnreachableException();
+ }
+
+ internal static void Throw(CheatEngineFailure failure)
+ {
+ failure.Throw();
+ }
+}
diff --git a/libs/CheatEngine.Client.Core/Domains/Hashing/UnavailableHashingClient.cs b/libs/CheatEngine.Client.Core/Domains/Hashing/UnavailableHashingClient.cs
new file mode 100644
index 0000000..8804838
--- /dev/null
+++ b/libs/CheatEngine.Client.Core/Domains/Hashing/UnavailableHashingClient.cs
@@ -0,0 +1,42 @@
+using CheatEngine.Client.Core.Domains.Events;
+using CheatEngine.Client.Hashing;
+using CheatEngine.Client.Results;
+
+namespace CheatEngine.Client.Core.Domains.Hashing;
+
+/// Preserves the separate file and target-memory hash contracts until their live-host gates are complete.
+internal sealed class UnavailableHashingClient : IHashingClient
+{
+ public bool TryHashMemory(MemoryHashRequest request, out HashDigest digest, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default)
+ {
+ digest = default;
+ failure = CreateFailure("Hashing.HashMemory", cancellationToken);
+ return false;
+ }
+
+ public HashDigest HashMemory(MemoryHashRequest request, CancellationToken cancellationToken = default)
+ {
+ _ = TryHashMemory(request, out _, out CheatEngineFailure failure, cancellationToken);
+ return UnavailableCapabilityFailure.Throw(failure);
+ }
+
+ public bool TryHashFile(FileHashRequest request, out HashDigest digest, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default)
+ {
+ digest = default;
+ failure = CreateFailure("Hashing.HashFile", cancellationToken);
+ return false;
+ }
+
+ public HashDigest HashFile(FileHashRequest request, CancellationToken cancellationToken = default)
+ {
+ _ = TryHashFile(request, out _, out CheatEngineFailure failure, cancellationToken);
+ return UnavailableCapabilityFailure.Throw(failure);
+ }
+
+ private static CheatEngineFailure CreateFailure(string operation, CancellationToken cancellationToken)
+ {
+ return UnavailableCapabilityFailure.Create("Target-memory and file hashing", operation, cancellationToken);
+ }
+}
diff --git a/libs/CheatEngine.Client.Core/Domains/Hotkeys/UnavailableHotkeyClient.cs b/libs/CheatEngine.Client.Core/Domains/Hotkeys/UnavailableHotkeyClient.cs
new file mode 100644
index 0000000..459e033
--- /dev/null
+++ b/libs/CheatEngine.Client.Core/Domains/Hotkeys/UnavailableHotkeyClient.cs
@@ -0,0 +1,31 @@
+using System.Diagnostics.CodeAnalysis;
+
+using CheatEngine.Client.Core.Domains.Events;
+using CheatEngine.Client.Events;
+using CheatEngine.Client.Hotkeys;
+using CheatEngine.Client.Results;
+
+namespace CheatEngine.Client.Core.Domains.Hotkeys;
+
+/// Preserves copied hotkey semantics until host callback lifecycle behavior passes the live-host gate.
+internal sealed class UnavailableHotkeyClient : IHotkeyClient
+{
+ public bool TryRegister(HotkeyRegistration registration, HotkeyHandler handler, EventStreamOptions streamOptions,
+ [NotNullWhen(true)] out IHotkeyLease? lease, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(handler);
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(streamOptions.Capacity);
+ lease = null;
+ failure = UnavailableCapabilityFailure.Create("Hotkeys", "Hotkeys.Register", cancellationToken);
+ return false;
+ }
+
+ public IHotkeyLease Register(HotkeyRegistration registration, HotkeyHandler handler,
+ EventStreamOptions streamOptions,
+ CancellationToken cancellationToken = default)
+ {
+ _ = TryRegister(registration, handler, streamOptions, out _, out CheatEngineFailure failure, cancellationToken);
+ return UnavailableCapabilityFailure.Throw(failure);
+ }
+}
diff --git a/libs/CheatEngine.Client.Core/Domains/IProcessHost.cs b/libs/CheatEngine.Client.Core/Domains/IProcessHost.cs
index 2defb5b..57dec30 100644
--- a/libs/CheatEngine.Client.Core/Domains/IProcessHost.cs
+++ b/libs/CheatEngine.Client.Core/Domains/IProcessHost.cs
@@ -8,6 +8,7 @@ internal interface IProcessHost
public long GetOpenedProcessId();
public void OpenProcess(long processId);
public bool TryGetLocalProcess(int processId, out LocalProcessInfo process);
+ public IReadOnlyList GetLocalProcesses();
public IReadOnlyList FindProcessesByExactName(string processName);
public CheatEngineArchitecture GetTargetArchitecture();
}
diff --git a/libs/CheatEngine.Client.Core/Domains/LocalProcessHost.cs b/libs/CheatEngine.Client.Core/Domains/LocalProcessHost.cs
index 42d79e0..f8a5959 100644
--- a/libs/CheatEngine.Client.Core/Domains/LocalProcessHost.cs
+++ b/libs/CheatEngine.Client.Core/Domains/LocalProcessHost.cs
@@ -32,6 +32,21 @@ public bool TryGetLocalProcess(int processId, out LocalProcessInfo process)
}
}
+ public IReadOnlyList GetLocalProcesses()
+ {
+ using ProcessCollection processes = new(Process.GetProcesses());
+ List captured = new(processes.Count);
+ for (int index = 0; index < processes.Count; index++)
+ {
+ if (TryCapture(processes[index], out LocalProcessInfo process))
+ {
+ captured.Add(process);
+ }
+ }
+
+ return captured;
+ }
+
public IReadOnlyList FindProcessesByExactName(string processName)
{
ArgumentException.ThrowIfNullOrWhiteSpace(processName);
diff --git a/libs/CheatEngine.Client.Core/Domains/LuaClient.cs b/libs/CheatEngine.Client.Core/Domains/LuaClient.cs
index 047b2ad..fd3d6a4 100644
--- a/libs/CheatEngine.Client.Core/Domains/LuaClient.cs
+++ b/libs/CheatEngine.Client.Core/Domains/LuaClient.cs
@@ -13,9 +13,18 @@ internal sealed class LuaClient : ILuaClient
{
private readonly ICheatEngineDispatcher _dispatcher;
private readonly Func _epochProvider;
+
private readonly Func _isContextCurrent;
- private readonly HashSet _registeredModules = new(ReferenceEqualityComparer.Instance);
+
+ // This is deliberately a single lock-protected reservation. A generated module has two identities that must move
+ // together: its managed module instance and the immutable Lua name set published in its descriptor. Reserving the
+ // complete set before entering the dispatcher prevents a concurrent registration from partially mutating Lua.
+ private readonly Dictionary _registeredModules =
+ new(ReferenceEqualityComparer.Instance);
+
private readonly Lock _registeredModulesLock = new();
+ private readonly HashSet _reservedExportNames = new(StringComparer.Ordinal);
+ private readonly HashSet _reservedModuleNames = new(StringComparer.Ordinal);
private readonly Action _trackLease;
private readonly Action _untrackLease;
@@ -65,8 +74,16 @@ public bool TryRegisterModule(ILuaModule luaModule, [NotNullWhen(true)] out ILua
return false;
}
- if (!TryReserveModule(luaModule, out failure))
+ try
+ {
+ if (!TryReserveModule(luaModule, out failure))
+ {
+ return false;
+ }
+ }
+ catch (Exception exception)
{
+ failure = CoreFailureFactory.FromException("Lua.RegisterModule", exception);
return false;
}
@@ -147,31 +164,14 @@ public bool TryExecute(ILuaOperation operation, [MaybeNullWhen
}
long epoch = _epochProvider();
- LuaOperationResult operationResult = default;
- if (!_dispatcher.TryInvoke(
- () => operationResult = ExecuteOperation(operation, epoch),
- out failure,
+ if (!TryDispatchOperation(operation, epoch, out LuaOperationResult operationResult, out failure,
cancellationToken))
{
result = default;
return false;
}
- if (!operationResult.Succeeded)
- {
- result = default;
- failure = IsDefined(operationResult.Failure)
- ? operationResult.Failure
- : new CheatEngineFailure(
- CheatEngineFailureKind.Unknown,
- "Lua.Execute",
- "The typed Lua operation returned failure without an associated Cheat Engine failure.");
- return false;
- }
-
- result = operationResult.Result;
- failure = default;
- return true;
+ return TryMaterializeOperationResult(operationResult, out result, out failure);
}
public TResult Execute(ILuaOperation operation, CancellationToken cancellationToken = default)
@@ -185,16 +185,37 @@ public TResult Execute(ILuaOperation operation, CancellationTo
}
public bool TryExecute(TOperation operation, [MaybeNullWhen(false)] out TResult result,
- out CheatEngineFailure failure, CancellationToken cancellationToken = default)
- where TOperation : ILuaOperation
+ out CheatEngineFailure failure, CancellationToken cancellationToken)
+ where TOperation : struct, ILuaOperation
{
- return TryExecute(operation, out result, out failure, cancellationToken);
+ if (cancellationToken.IsCancellationRequested)
+ {
+ result = default;
+ failure = CoreFailureFactory.Cancelled("Lua.Execute");
+ return false;
+ }
+
+ long epoch = _epochProvider();
+ if (!TryDispatchOperation(operation, epoch, out LuaOperationResult operationResult, out failure,
+ cancellationToken))
+ {
+ result = default;
+ return false;
+ }
+
+ return TryMaterializeOperationResult(operationResult, out result, out failure);
}
- public TResult Execute(TOperation operation, CancellationToken cancellationToken = default)
- where TOperation : ILuaOperation
+ public TResult Execute(TOperation operation, CancellationToken cancellationToken)
+ where TOperation : struct, ILuaOperation
{
- return Execute(operation, cancellationToken);
+ if (TryExecute(operation, out TResult? result, out CheatEngineFailure failure,
+ cancellationToken))
+ {
+ return result;
+ }
+
+ return ThrowFailure(failure);
}
private static T ThrowFailure(CheatEngineFailure failure)
@@ -218,6 +239,95 @@ private LuaOperationResult ExecuteOperation(ILuaOperation ExecuteOperation(TOperation operation, long epoch)
+ where TOperation : struct, ILuaOperation
+ {
+ LuaOperationContext context = new(epoch, _isContextCurrent);
+ try
+ {
+ context.ThrowIfExpired();
+ // The constraint produces a constrained interface call for generated readonly record structs. This keeps the
+ // normal generated-operation path free of an ILuaOperation box.
+ bool succeeded = operation.TryExecute(context, out TResult? result, out CheatEngineFailure failure);
+ return new LuaOperationResult(succeeded, result!, failure);
+ }
+ finally
+ {
+ context.Expire();
+ }
+ }
+
+ private bool TryDispatchOperation(
+ ILuaOperation operation,
+ long epoch,
+ out LuaOperationResult result,
+ out CheatEngineFailure failure,
+ CancellationToken cancellationToken)
+ {
+ if (_dispatcher is IStatefulCheatEngineDispatcher statefulDispatcher)
+ {
+ return statefulDispatcher.TryInvoke(
+ new LuaOperationDispatchState(this, operation, epoch),
+ static dispatchState => dispatchState.Execute(),
+ out result,
+ out failure,
+ cancellationToken);
+ }
+
+ return _dispatcher.TryInvoke(
+ () => ExecuteOperation(operation, epoch),
+ out result,
+ out failure,
+ cancellationToken);
+ }
+
+ private bool TryDispatchOperation(
+ TOperation operation,
+ long epoch,
+ out LuaOperationResult result,
+ out CheatEngineFailure failure,
+ CancellationToken cancellationToken)
+ where TOperation : struct, ILuaOperation
+ {
+ if (_dispatcher is IStatefulCheatEngineDispatcher statefulDispatcher)
+ {
+ return statefulDispatcher.TryInvoke(
+ new LuaOperationDispatchState