diff --git a/.editorconfig b/.editorconfig
index 208be63..c1897ef 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,11 +1,145 @@
root = true
+# Repository-wide, editor-agnostic defaults. Language-specific sections below
+# intentionally override these values where a format has stricter constraints.
[*]
charset = utf-8
insert_final_newline = true
indent_style = space
indent_size = 4
+tab_width = 4
+max_line_length = 120
trim_trailing_whitespace = true
[*.{csproj,props,targets,slnx,config,json,yml,yaml}]
+# YAML cannot use tabs for indentation. Keep project and configuration files
+# compact and portable even though C# uses tabs below.
+indent_style = space
indent_size = 2
+tab_width = 2
+
+[*.{cs,csx}]
+################################################################################
+# C# team contract
+#
+# Roslyn settings are the portable source of truth: Rider, VS, dotnet format,
+# and CI all understand them. Rider-only settings are marked as such.
+################################################################################
+
+# Tabs describe logical nesting; spaces are retained solely to align continuations.
+indent_style = tab
+indent_size = tab
+tab_width = 4
+csharp_indent_style = tab
+csharp_indent_size = 4
+csharp_tab_width = 4
+csharp_alignment_tab_fill_style = use_spaces
+
+# Make structural boundaries easy to scan.
+csharp_new_line_before_open_brace = all
+csharp_new_line_before_catch = true
+csharp_new_line_before_else = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_between_query_expression_clauses = true
+csharp_indent_braces = false
+csharp_indent_switch_labels = true
+csharp_preserve_single_line_blocks = false
+csharp_preserve_single_line_statements = false
+csharp_space_after_cast = true
+csharp_space_around_binary_operators = before_and_after
+csharp_preferred_modifier_order = public, private, protected, internal, file, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, required, volatile, async
+
+# Keep imports and namespaces deterministic and minimise needless nesting.
+dotnet_separate_import_directive_groups = true
+dotnet_sort_system_directives_first = true
+csharp_using_directive_placement = outside_namespace
+csharp_style_namespace_declarations = file_scoped
+
+# Prefer syntax that leaves declarations, construction, and contracts visible.
+# `var` is statically typed in C#, but explicit types are the repository's
+# readability convention whenever the language can express them.
+csharp_style_var_for_built_in_types = false
+csharp_style_var_when_type_is_apparent = false
+csharp_style_var_elsewhere = false
+csharp_style_implicit_object_creation_when_type_is_apparent = false
+dotnet_style_require_accessibility_modifiers = always
+dotnet_style_readonly_field = true
+dotnet_style_explicit_tuple_names = true
+dotnet_style_predefined_type_for_locals_parameters_members = true
+dotnet_style_predefined_type_for_member_access = true
+csharp_prefer_braces = true
+csharp_style_prefer_top_level_statements = false
+
+# Enforced now: redundant suppressions report genuine noise and remain build
+# blockers because Directory.Build.props treats warnings as errors.
+dotnet_diagnostic.IDE0079.severity = error
+
+# Migration rules: surface the historical backlog in every Roslyn-aware editor
+# and participate in code cleanup without breaking the currently dirty tree.
+# Promote these to warning/error in a dedicated cleanup change once the existing
+# source has been reformatted and explicit types have been introduced.
+dotnet_diagnostic.IDE0007.severity = none
+dotnet_diagnostic.IDE0008.severity = suggestion
+dotnet_diagnostic.IDE0011.severity = suggestion
+dotnet_diagnostic.IDE0033.severity = suggestion
+dotnet_diagnostic.IDE0040.severity = suggestion
+dotnet_diagnostic.IDE0044.severity = suggestion
+# The formatter still follows the tab contract above. Do not flood every open
+# legacy file with a formatting diagnostic before the dedicated reformat pass.
+dotnet_diagnostic.IDE0055.severity = none
+dotnet_diagnostic.IDE0005.severity = suggestion
+dotnet_diagnostic.IDE0065.severity = suggestion
+dotnet_diagnostic.IDE0090.severity = suggestion
+dotnet_diagnostic.IDE0160.severity = none
+dotnet_diagnostic.IDE0161.severity = suggestion
+dotnet_diagnostic.IDE1006.severity = suggestion
+
+# Rider derives its own "var or explicit type" inspections from the shared
+# csharp_style_var_* values. Silence those copies so IDE0008 is the one
+# portable diagnostic developers see, rather than reporting each issue twice.
+resharper_suggest_var_or_type_built_in_types_highlighting = none
+resharper_suggest_var_or_type_simple_types_highlighting = none
+resharper_suggest_var_or_type_elsewhere_highlighting = none
+resharper_suggest_var_or_type_deconstruction_declarations_highlighting = none
+
+# Rider-specific, live diagnostic. It complements compiler nullable analysis;
+# it is deliberately a warning rather than a CI-only, non-portable error.
+resharper_possible_null_reference_exception_highlighting = warning
+
+# Naming rules keep public contracts recognisable before a reader opens a type.
+dotnet_naming_rule.interfaces_must_be_prefixed_i.severity = suggestion
+dotnet_naming_rule.interfaces_must_be_prefixed_i.symbols = interfaces
+dotnet_naming_rule.interfaces_must_be_prefixed_i.style = i_pascal_case
+dotnet_naming_symbols.interfaces.applicable_kinds = interface
+dotnet_naming_style.i_pascal_case.required_prefix = I
+dotnet_naming_style.i_pascal_case.capitalization = pascal_case
+
+dotnet_naming_rule.types_must_be_pascal_case.severity = suggestion
+dotnet_naming_rule.types_must_be_pascal_case.symbols = types
+dotnet_naming_rule.types_must_be_pascal_case.style = pascal_case
+dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum, delegate
+dotnet_naming_style.pascal_case.capitalization = pascal_case
+
+dotnet_naming_rule.non_private_members_must_be_pascal_case.severity = suggestion
+dotnet_naming_rule.non_private_members_must_be_pascal_case.symbols = non_private_members
+dotnet_naming_rule.non_private_members_must_be_pascal_case.style = pascal_case
+dotnet_naming_symbols.non_private_members.applicable_kinds = property, method, event
+dotnet_naming_symbols.non_private_members.applicable_accessibilities = public, internal, protected, protected_internal, private_protected
+
+dotnet_naming_rule.private_fields_must_be_underscore_camel_case.severity = suggestion
+dotnet_naming_rule.private_fields_must_be_underscore_camel_case.symbols = private_fields
+dotnet_naming_rule.private_fields_must_be_underscore_camel_case.style = underscore_camel_case
+dotnet_naming_symbols.private_fields.applicable_kinds = field
+dotnet_naming_symbols.private_fields.applicable_accessibilities = private
+dotnet_naming_style.underscore_camel_case.required_prefix = _
+dotnet_naming_style.underscore_camel_case.capitalization = camel_case
+
+# IDE0005 requires XML documentation generation to run during a command-line
+# build. Shipping projects already enable it; tests and samples intentionally
+# retain it as an editor suggestion rather than acquiring build-only XML output.
+[libs/**.cs]
+dotnet_diagnostic.IDE0005.severity = error
+
+[src/**.cs]
+dotnet_diagnostic.IDE0005.severity = error
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
deleted file mode 100644
index ea07b55..0000000
--- a/.github/workflows/build.yml
+++ /dev/null
@@ -1,54 +0,0 @@
-name: SonarQube
-on:
- push:
- branches:
- - main
- pull_request:
- types: [opened, synchronize, reopened]
-jobs:
- build:
- name: Build and analyze
- runs-on: windows-latest
- steps:
- - name: Set up JDK 21
- uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4.8.0
- with:
- java-version: 21
- distribution: "zulu" # Alternative distribution options are available.
- - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- with:
- fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- - name: Set up .NET SDK
- uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
- with:
- dotnet-version: 10.0.401
- - name: Cache SonarQube Cloud packages
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
- with:
- path: ~\sonar\cache
- key: ${{ runner.os }}-sonar
- restore-keys: ${{ runner.os }}-sonar
- - name: Cache SonarQube Cloud scanner
- id: cache-sonar-scanner
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
- with:
- path: ${{ runner.temp }}\scanner
- key: ${{ runner.os }}-sonar-scanner
- restore-keys: ${{ runner.os }}-sonar-scanner
- - name: Install SonarQube Cloud scanner
- shell: powershell
- run: |
- $scannerPath = Join-Path $env:RUNNER_TEMP 'scanner'
- $scannerExecutable = Join-Path $scannerPath 'dotnet-sonarscanner.exe'
- New-Item -Path $scannerPath -ItemType Directory -Force | Out-Null
- if (-not (Test-Path $scannerExecutable)) {
- dotnet tool update dotnet-sonarscanner --tool-path $scannerPath
- }
- - name: Build and analyze
- env:
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- shell: powershell
- run: |
- ${{ runner.temp }}\scanner\dotnet-sonarscanner begin /k:"CheatEngineNet_CheatEngine.Client" /o:"cheatenginenet" /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
- dotnet build
- ${{ runner.temp }}\scanner\dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..5bbd905
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,171 @@
+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
+
+defaults:
+ run:
+ shell: pwsh
+
+env:
+ DOTNET_NOLOGO: true
+ DOTNET_CLI_TELEMETRY_OPTOUT: true
+ MSBUILDDISABLENODEREUSE: true
+
+jobs:
+ validate:
+ name: Validate packages, template, and AOT graph
+ runs-on: windows-latest
+ timeout-minutes: 35
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+
+ - name: Install pinned .NET SDK
+ uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
+ with:
+ global-json-file: global.json
+ cache: true
+ cache-dependency-path: |
+ **/packages.lock.json
+ Directory.Packages.props
+
+ - name: Restore locked dependency graph
+ run: dotnet restore CheatEngine.Client.slnx --locked-mode
+
+ - name: Build Release
+ 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) {
+ throw 'No *.Tests.csproj project found under tests.'
+ }
+
+ foreach ($project in $projects) {
+ $results = Join-Path 'artifacts/test-results' $project.BaseName
+ $options = @(
+ '--project', $project.FullName,
+ '--configuration', 'Release',
+ '--no-build', '--no-restore',
+ '--report-trx',
+ '--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
+ }
+
+ - name: Summarize test results
+ if: ${{ !cancelled() }}
+ run: |
+ $reports = @(Get-ChildItem -Path artifacts/test-results -Filter *.trx -Recurse -File -ErrorAction SilentlyContinue)
+ if ($reports.Count -eq 0) {
+ throw 'Microsoft Testing Platform did not produce a TRX test report.'
+ }
+
+ $results = @($reports | ForEach-Object {
+ ([xml](Get-Content -LiteralPath $_.FullName -Raw)).TestRun.Results.UnitTestResult
+ } | Where-Object { $_ })
+ $passed = @($results | Where-Object outcome -eq 'Passed').Count
+ $skipped = @($results | Where-Object outcome -eq 'NotExecuted').Count
+ $failed = @($results | Where-Object { $_.outcome -notin 'Passed', 'NotExecuted' })
+
+ @(
+ '### Test results',
+ '',
+ '| Total | Passed | Failed | Skipped |',
+ '| ---: | ---: | ---: | ---: |',
+ "| $($results.Count) | $passed | $($failed.Count) | $skipped |"
+ ) | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8
+
+ - name: Upload test results
+ if: ${{ !cancelled() }}
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: test-results
+ path: artifacts/test-results/**/*.trx
+ 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
+
+ - name: Smoke test isolated package consumption
+ shell: pwsh
+ run: ./eng/Invoke-PackageSmoke.ps1 -PackageSource ./artifacts/packages
+
+ - name: Smoke test local template installation
+ run: ./eng/Invoke-TemplateSmoke.ps1 -PackageSource ./artifacts/packages
+
+ - name: Upload package artifacts
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: nuget-packages
+ path: |
+ artifacts/packages/*.nupkg
+ artifacts/packages/*.snupkg
+ if-no-files-found: error
+ retention-days: 14
+
+ - name: Publish Native AOT reference probe
+ run: dotnet publish tests/CheatEngine.Client.AotProbe/CheatEngine.Client.AotProbe.csproj --configuration Release --runtime win-x64 --no-restore --output ./artifacts/aot-probe
+
+ - name: Run Native AOT reference probe
+ run: ./artifacts/aot-probe/CheatEngine.Client.AotProbe.exe
+
+ - name: Upload Native AOT probe
+ if: ${{ !cancelled() }}
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: native-aot-probe
+ path: artifacts/aot-probe
+ if-no-files-found: warn
+ retention-days: 14
+
+ gate:
+ name: Gate
+ if: ${{ always() }}
+ needs: validate
+ runs-on: windows-latest
+ timeout-minutes: 5
+ permissions: {}
+
+ steps:
+ - name: Check validation result
+ env:
+ VALIDATE_RESULT: ${{ needs.validate.result }}
+ run: |
+ "| Job | Result |", "| --- | --- |", "| validate | $env:VALIDATE_RESULT |" |
+ Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8
+ if ($env:VALIDATE_RESULT -ne 'success') {
+ Write-Host "::error::Validation finished with '$env:VALIDATE_RESULT'."
+ exit 1
+ }
diff --git a/.github/workflows/main-ci.yml b/.github/workflows/main-ci.yml
new file mode 100644
index 0000000..c820499
--- /dev/null
+++ b/.github/workflows/main-ci.yml
@@ -0,0 +1,45 @@
+name: Main CI
+
+on:
+ push:
+ branches: [main]
+ paths:
+ - '.editorconfig'
+ - '.github/**'
+ - 'CheatEngine.Client.slnx'
+ - 'Directory.Build.props'
+ - 'Directory.Build.targets'
+ - 'Directory.Packages.props'
+ - 'README.md'
+ - 'eng/**'
+ - 'global.json'
+ - 'libs/**'
+ - 'src/**'
+ - 'templates/**'
+ - 'tests/**'
+ merge_group:
+ types: [checks_requested]
+ workflow_dispatch:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: false
+
+permissions:
+ contents: read
+
+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'
+ 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
new file mode 100644
index 0000000..fb67677
--- /dev/null
+++ b/.github/workflows/pull-request-ci.yml
@@ -0,0 +1,79 @@
+name: Pull request CI
+
+on:
+ pull_request:
+ types: [opened, synchronize, reopened, ready_for_review]
+ paths:
+ - '.editorconfig'
+ - '.github/**'
+ - 'CheatEngine.Client.slnx'
+ - 'Directory.Build.props'
+ - 'Directory.Build.targets'
+ - 'Directory.Packages.props'
+ - 'README.md'
+ - 'eng/**'
+ - 'global.json'
+ - 'libs/**'
+ - 'src/**'
+ - 'templates/**'
+ - 'tests/**'
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+jobs:
+ ci:
+ 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.
+ sonar:
+ name: Sonar
+ needs: ci
+ if: >-
+ github.event.pull_request.head.repo.full_name == github.repository
+ && github.actor != 'dependabot[bot]'
+ uses: ./.github/workflows/sonar.yml
+ secrets:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+
+ lint-workflows:
+ name: Lint workflows
+ if: github.event.pull_request.draft == false
+ runs-on: windows-latest
+ timeout-minutes: 5
+ env:
+ ACTIONLINT_VERSION: 1.7.12
+ ACTIONLINT_SHA256: 6e7241b51e6817ea6a047693d8e6fed13b31819c9a0dd6c5a726e1592d22f6e9
+
+ steps:
+ - name: Checkout workflow definitions
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ sparse-checkout: .github
+ persist-credentials: false
+
+ - name: Run actionlint
+ run: |
+ $ErrorActionPreference = 'Stop'
+ $archive = Join-Path $env:RUNNER_TEMP 'actionlint.zip'
+ $url = "https://github.com/rhysd/actionlint/releases/download/v$env:ACTIONLINT_VERSION/actionlint_$($env:ACTIONLINT_VERSION)_windows_amd64.zip"
+ Invoke-WebRequest -Uri $url -OutFile $archive -MaximumRetryCount 3 -RetryIntervalSec 5
+
+ $actual = (Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash.ToLowerInvariant()
+ if ($actual -ne $env:ACTIONLINT_SHA256) {
+ throw "actionlint $env:ACTIONLINT_VERSION has SHA-256 $actual, expected $env:ACTIONLINT_SHA256."
+ }
+
+ $destination = Join-Path $env:RUNNER_TEMP 'actionlint'
+ Expand-Archive -LiteralPath $archive -DestinationPath $destination
+ & (Join-Path $destination 'actionlint.exe') -color
diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml
new file mode 100644
index 0000000..57650ee
--- /dev/null
+++ b/.github/workflows/sonar.yml
@@ -0,0 +1,149 @@
+name: Sonar
+
+on:
+ workflow_call:
+ inputs:
+ project-key:
+ description: SonarQube Cloud project key.
+ type: string
+ default: CheatEngineNet_CheatEngine.Client
+ organization:
+ description: SonarQube Cloud organization key.
+ type: string
+ default: cheatenginenet
+ wait-quality-gate:
+ description: Fail the analysis job when SonarQube Cloud reports a failing Quality Gate.
+ type: boolean
+ default: false
+ secrets:
+ SONAR_TOKEN:
+ description: SonarQube Cloud token. Analysis is skipped when it is unavailable.
+ required: false
+
+permissions:
+ contents: read
+
+defaults:
+ run:
+ shell: pwsh
+
+env:
+ DOTNET_NOLOGO: true
+ DOTNET_CLI_TELEMETRY_OPTOUT: true
+ MSBUILDDISABLENODEREUSE: true
+ SONAR_SCANNER_VERSION: 11.3.0
+
+jobs:
+ analyze:
+ name: Analyze
+ if: github.event_name != 'merge_group'
+ runs-on: windows-latest
+ timeout-minutes: 30
+ env:
+ SONAR_ENABLED: ${{ secrets.SONAR_TOKEN != '' }}
+ SONAR_PROJECT_KEY: ${{ inputs.project-key }}
+ SONAR_ORGANIZATION: ${{ inputs.organization }}
+ SONAR_WAIT_QUALITY_GATE: ${{ inputs.wait-quality-gate }}
+
+ steps:
+ - name: Skip without token
+ if: env.SONAR_ENABLED != 'true'
+ run: >-
+ Write-Host '::notice title=Sonar analysis skipped::SONAR_TOKEN is empty. Fork and Dependabot pull requests
+ receive no repository secret; configure SONAR_TOKEN to enable analysis.'
+
+ - name: Check analysis method
+ if: env.SONAR_ENABLED == 'true'
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ run: |
+ $uri = "https://sonarcloud.io/api/settings/values?component=$env:SONAR_PROJECT_KEY&keys=sonar.autoscan.enabled"
+ try {
+ $response = Invoke-RestMethod -Uri $uri -Headers @{ Authorization = "Bearer $env:SONAR_TOKEN" } `
+ -TimeoutSec 30 -MaximumRetryCount 2 -RetryIntervalSec 3
+ }
+ catch {
+ Write-Host "::warning::Could not read the analysis method of $env:SONAR_PROJECT_KEY. $($_.Exception.Message)"
+ return
+ }
+
+ if (@($response.settings | Where-Object { $_.key -eq 'sonar.autoscan.enabled' -and $_.value -eq 'true' })) {
+ Write-Host "::error::Automatic Analysis is enabled on $env:SONAR_PROJECT_KEY, so SonarQube Cloud rejects CI analysis. Turn it off in Administration, Analysis Method."
+ exit 1
+ }
+
+ - name: Checkout
+ if: env.SONAR_ENABLED == 'true'
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+
+ - name: Set up JDK 21
+ if: env.SONAR_ENABLED == 'true'
+ uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
+ with:
+ distribution: zulu
+ java-version: '21'
+
+ - name: Install pinned .NET SDK
+ if: env.SONAR_ENABLED == 'true'
+ uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
+ with:
+ global-json-file: global.json
+ cache: true
+ cache-dependency-path: |
+ **/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: >-
+ dotnet tool install dotnet-sonarscanner --tool-path "$env:RUNNER_TEMP/sonar-scanner"
+ --version $env:SONAR_SCANNER_VERSION
+
+ - name: Restore locked dependency graph
+ if: env.SONAR_ENABLED == 'true'
+ run: dotnet restore CheatEngine.Client.slnx --locked-mode
+
+ - name: Begin analysis
+ if: env.SONAR_ENABLED == 'true'
+ 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)"
+ $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",
+ "/d:sonar.qualitygate.wait=$env:SONAR_WAIT_QUALITY_GATE"
+ )
+ & "$env:RUNNER_TEMP/sonar-scanner/dotnet-sonarscanner.exe" begin @arguments
+
+ - name: Build
+ if: env.SONAR_ENABLED == 'true'
+ run: >-
+ dotnet build CheatEngine.Client.slnx --configuration Release --no-restore --no-incremental
+ --disable-build-servers --warnaserror
+
+ - name: End analysis
+ if: env.SONAR_ENABLED == 'true'
+ env:
+ SONARQUBE_SCANNER_PARAMS: '{"sonar.token":"${{ secrets.SONAR_TOKEN }}"}'
+ run: '& "$env:RUNNER_TEMP/sonar-scanner/dotnet-sonarscanner.exe" end'
diff --git a/.gitignore b/.gitignore
index c6c696b..029b475 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,5 @@ TestResults/
*.user
*.suo
-.claude/scheduled_tasks.lock
-.claude/settings.local.json
+.claude/*
/CLAUDE.md
diff --git a/CheatEngine.Client.slnx b/CheatEngine.Client.slnx
index d872e9d..710e369 100644
--- a/CheatEngine.Client.slnx
+++ b/CheatEngine.Client.slnx
@@ -11,20 +11,31 @@
+
-
+
+
+
-
+
+
+
+
+
+
+
diff --git a/Directory.Build.props b/Directory.Build.props
index 71c605e..769878f 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -11,6 +11,26 @@
14.0
enable
+ enable
+ true
+ latest-recommended
+ true
+ true
+ true
+ true
+
+
+
+ 0.1.0
+ CheatEngineNet
+ CheatEngineNet
+ git
+ https://github.com/CheatEngineNet/CheatEngine.Client
+ MIT
+ false
+ README.md
+ true
+ true
@@ -26,4 +46,6 @@
Condition="$(_CheatEngineClientFolder.StartsWith('src/', System.StringComparison.OrdinalIgnoreCase)) or $(_CheatEngineClientFolder.StartsWith('libs/', System.StringComparison.OrdinalIgnoreCase))"/>
+
diff --git a/Directory.Build.targets b/Directory.Build.targets
index 9d2a1d8..f7d374e 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -1,9 +1,72 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_CheatEngineClientDirectProjectReference Include="@(ProjectReference)"
+ Condition="'%(ProjectReference.DefiningProjectName)' == '$(MSBuildProjectName)'"/>
+ <_CheatEngineClientSdkReference Include="@(PackageReference)"
+ Condition="'%(PackageReference.Identity)' == 'CheatEngine.SDK' and '%(PackageReference.DefiningProjectName)' == '$(MSBuildProjectName)'"/>
+ <_CheatEngineClientSdkRangeReference Include="@(_CheatEngineClientSdkReference)"
+ Condition="'%(_CheatEngineClientSdkReference.VersionOverride)' == '[1.0.0,2.0.0)'"/>
+
+
+ <_CheatEngineClientProjectReferences>@(_CheatEngineClientDirectProjectReference->'%(Filename)')
+
+
+
+
+
+
+
+
+
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 1932808..07c6748 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -2,4 +2,28 @@
true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/README.md b/README.md
index 1fe3585..4a6b39e 100644
--- a/README.md
+++ b/README.md
@@ -1,77 +1,283 @@
+
+
# CheatEngine.Client
-A public, fluent, strongly-typed C# 14 / .NET 10 client API, built as a mapper/binder over CheatEngine.SDK.
+**High-level, lifecycle-safe C# APIs for modern Cheat Engine plugins.**
+
+[](https://github.com/CheatEngineNet/CheatEngine.Client/actions/workflows/main-ci.yml)
+[](https://www.nuget.org/packages/CheatEngine.Client)
+[](https://dotnet.microsoft.com/download/dotnet/10.0)
+[](#requirements)
+
+[Quick start](#quick-start) · [Lifecycle](#the-plugin-lifecycle) · [Packages](#packages-and-direct-sdk-reference) · [Capabilities](#v010-capability-status) · [Contributing](#build-and-validation)
+
+
+
+## Context
+
+`CheatEngine.Client` is an in-process, dependency-injection-first layer for plugins loaded by Cheat Engine. It builds on
+[`CheatEngine.SDK`](https://www.nuget.org/packages/CheatEngine.SDK) and turns its low-level host bindings into
+bounded, typed, fluent C# operations for the lifetime of one plugin activation.
+
+The aggregate `ICheatEngineClient` gives an enabled plugin access to runtime facts and capabilities, main-thread
+dispatch, process selection, typed memory, AOB scans, inspection, address tables, and protected Lua operations. It
+never exposes a `LuaState`, CE object handle, raw native pointer, or SDK ownership wrapper to plugin code.
+
+## Why this project exists
+
+`CheatEngine.SDK` deliberately owns the difficult boundary: the generated Cheat Engine entry point, Lua protection,
+native bridge, host object model, and compile-time plugin/Lua diagnostics. Those are SDK concerns and
+`CheatEngine.Client` does not reimplement them.
+
+The Client exists for the application layer above that boundary. It makes recurring plugin concerns explicit and
+testable:
+
+| SDK boundary | Client policy above it |
+|-------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------|
+| Plugin bootstrap and protected Lua calls | One activation-scoped `ICheatEngineClient`; no raw Lua lifetime escapes |
+| Host-owned temporary objects and main-thread affinity | Synchronous dispatcher boundary, copied results, and deterministic cleanup |
+| Primitive Lua/host operations | Typed memory codecs, bounded strings and pointer chains, immutable AOB builders |
+| Plugin construction | One validated DI provider per enable epoch; explicit modules and configuration |
+| Host failures and capability differences | `Try...` methods with `CheatEngineFailure`, convenience methods that throw, and runtime capability observations |
+
+This separation lets a plugin stay ordinary, DI-friendly C# while retaining the SDK as the sole authority for ABI and
+Lua safety. It also keeps the high-level surface honest: a contract is not presented as a working Cheat Engine feature
+until its ownership, thread-affinity, and lifecycle path are established.
+
+## How it helps improve Cheat Engine plugin projects
+
+The Client centralizes lifecycle, ownership, dispatch, options, and capability policy once, rather than requiring each
+plugin to reproduce them around low-level SDK calls. This lowers the cost of adding a feature, gives tests a stable
+contract boundary, and keeps the generated plugin template focused on application code. It also makes the supported
+surface reviewable: high-level APIs remain fluent for consumers while the Core remains the only SDK mapper.
+
+## Requirements
+
+| Requirement | Baseline |
+|-----------------------------|------------------------------------------------------------------------------------|
+| .NET SDK | 10.0.401 or later |
+| Target framework / language | `net10.0` / C# 14 |
+| Cheat Engine host | 7.7, Windows x64 |
+| Plugin form | Framework-dependent managed plugin output folder |
+| SDK package | `CheatEngine.SDK` 1.x; the Client publishes a compatible range of `[1.0.0, 2.0.0)` |
+
+Cheat Engine remains the compatibility authority. The Client is not an IPC client, a remote-process service, or a
+standalone executable; v0.1 runs only inside an enabled Cheat Engine plugin.
+
+## Quick start
+
+The maintained starting point is the `ceplugin` template. It is both a usable project and the repository's executable
+example of the required plugin shape.
```powershell
-dotnet build CheatEngine.Client.slnx
+dotnet new install CheatEngine.Client.Templates
+dotnet new ceplugin --name MyPlugin
+cd MyPlugin
+dotnet build --configuration Release
+```
+
+The generated project intentionally retains these direct dependencies:
+
+```xml
+
+ net10.0
+ 14.0
+ x64
+ true
+ true
+
+
+
+
+
+
+
+```
+
+`CheatEngine.SDK` must be referenced **directly by the plugin project**. Its build assets generate the Cheat Engine
+entry point and provide the native Lua bridge; NuGet transitivity is not sufficient at that host boundary. Setting
+`CheatEngineClientPluginProject` opts the project into the Hosting package's `CECLIENT001` guard, which fails the
+build if the direct SDK reference is removed.
+
+Start with the template rather than copying this fragment into an existing plugin: it also demonstrates module
+registration, generated Lua exports, validated options, bounded AOB and typed-memory access, and an Address List
+snapshot. See the [template guide](templates/CheatEngine.Client.Templates/README.md) and the generated
+[plugin README](templates/CheatEngine.Client.Templates/content/CheatEngine.Plugin/README.md).
+
+### Minimal plugin shape
+
+The SDK still owns the plugin annotation. The Client base owns the activation-scoped composition:
+
+```csharp
+using CheatEngine.Client.Hosting;
+using CheatEngine.SDK.Annotations.Plugin;
+using Microsoft.Extensions.Configuration;
+
+namespace MyPlugin;
+
+[CheatEnginePlugin("My Plugin")]
+public sealed class Plugin : CheatEngineClientPlugin
+{
+ protected override void Configure(CheatEnginePluginBuilder builder)
+ {
+ builder.Configuration
+ .SetBasePath(AppContext.BaseDirectory)
+ .AddJsonFile("appsettings.json", optional: true, reloadOnChange: false);
+
+ builder.Client.AddModule();
+ }
+}
+```
+
+An activation module receives the scoped client in `OnEnabled` and `OnDisabling`. Fluent calls remain bounded and
+handle-free:
+
+```csharp
+Address address = client.Patterns
+ .Aob("48 8B ?? ?? ?? 89")
+ .InModule("game.exe")
+ .ReadableExecutable()
+ .RequireSingle()
+ .Execute();
+
+client.Memory.At(address + 0x14).Write(999);
```
-## Layout
+Use the `Try...` terminal operations when absence of a process, scan result, or runtime capability is an expected
+condition. Do not make a worker wait for the Cheat Engine thread if that worker can call back into the Client.
+
+## The plugin lifecycle
+
+The parameterless plugin instance is created by the SDK, but every enable creates new managed state:
-The solution folders of `CheatEngine.Client.slnx` mirror the directories below one to one, so a path in Rider's Solution
-Explorer is also the path on disk. `/Solution Items/` is the only virtual folder.
+```text
+OnEnable
+ -> Configure a new builder (explicit configuration, services, modules, codecs)
+ -> Build and validate a new provider and scope
+ -> Resolve options and ICheatEngineClient
+ -> Enable modules in registration order
+ -> OnClientEnabled
+OnDisable
+ -> Stop admitting the active client
+ -> OnClientDisabling
+ -> Disable modules in reverse order
+ -> Drain Client-owned CE resources while the SDK context is valid
+ -> Dispose scope, provider, and configuration
```
-CheatEngine.Client/
-├─ eng/ MSBuild profiles, selected by the top-level folder of a project
-├─ libs/ Small layered libraries
-│ ├─ CheatEngine.Client.Abstractions/ Public vocabulary and contracts
-│ ├─ CheatEngine.Client.Binding/ Mapper/binder onto CheatEngine.SDK
-│ └─ CheatEngine.Client.Fluent/ Fluent public API
-├─ src/
-│ └─ CheatEngine.Client/ The one project a consumer references
-└─ tests/ One .Tests twin per project above
+
+`ICheatEngineClient.Epoch` and `ICheatEngineClient.Stopping` identify that activation. Never retain the client, a
+resource lease, a Lua reference, a cancellation token, or a target-bound value across disable/re-enable. Constructors,
+field initializers, and static initialization must not call Cheat Engine; the SDK binding is valid only after enable.
+
+All Client operations are synchronous. A cancellation token can prevent dispatch or stop Client-managed work between
+steps, but it does not claim to interrupt a Lua primitive that has already started. Read
+[ADR 0002](docs/adr/0002-plugin-activation-lifecycle.md) before adding a service that touches Cheat Engine.
+
+## Packages and direct SDK reference
+
+The recommended package is `CheatEngine.Client`. The delivery graph stays deliberately one-way:
+
+```text
+CheatEngine.Client
+├─ CheatEngine.Client.Fluent ────────────────> public contracts
+└─ CheatEngine.Client.Hosting
+ ├─ CheatEngine.Client.Extensions.DependencyInjection
+ │ ├─ CheatEngine.Client.Core ────────────> CheatEngine.SDK
+ │ └─ public contracts
+ └─ CheatEngine.SDK
+
+public contracts ────────────────────────────> stable SDK value/runtime types only
```
-## Projects
-
-| Project | Role | References |
-|-----------------------------------|----------------------------------------------------------------------------------------|-------------------------------------|
-| `CheatEngine.Client.Abstractions` | Strongly-typed public vocabulary, and the contracts between the fluent and the binding | nothing |
-| `CheatEngine.Client.Fluent` | Fluent, composable public API | `Abstractions` |
-| `CheatEngine.Client.Binding` | Mapper/binder onto CheatEngine.SDK. Internal by default | `Abstractions` |
-| `CheatEngine.Client` | Composition root: the single project a consumer references | `Abstractions`, `Fluent`, `Binding` |
-
-Dependency rules:
-
-- `Abstractions` is the base. `Fluent` and `Binding` never reference each other, and only `CheatEngine.Client` composes
- them.
-- `libs/` never references `src/` or `tests/`. A consumer references `CheatEngine.Client` only.
-- `Binding` is the only project meant to depend on CheatEngine.SDK (the default rule, to revisit if `Abstractions` reuses CheatEngine.SDK
- vocabulary).
-- A test project references its subject only.
-
-## Conventions
-
-- Folder name, project file name, assembly name and root namespace are the same string.
-- Every project has a `README.md` next to its project file. The build fails without it (`CHEATENGINECLIENT9001`).
-- Every project in `libs/` and `src/` has a twin `tests/.Tests`, which sees its internals. The `.Tests` suffix is
- reserved for test projects.
-- The top-level folder of a project selects its profile: `libs/` and `src/` use `eng/Shipping.props`, `tests/` uses
- `eng/Tests.props`. A project outside these folders has no target framework and does not build.
-- `Directory.Build.props` is the only one of the repository: no nested `Directory.Build.*` files.
-- Build output goes to `artifacts/`, never inside a project folder.
-- No `Common`, `Utils` or `Helpers` folders.
-
-## Solution folders
-
-- One solution folder per directory, with the same path (`/libs/`, `/src/`, `/tests/`, `/eng/`). They stay flat, and
- nest only where the disk nests.
-- Entries are sorted by path, case-insensitively. Project sources and project READMEs are not listed. `artifacts/`,
- `.idea/` and `.claude/` are never listed.
-- Adding a project: its folder, its `.csproj`, its `README.md`, one `` line in the matching solution folder, and
- its `.Tests` twin.
-
-## Reserved, not created yet
-
-A folder exists only when it holds a real file: no empty placeholder directories, no `.gitkeep`. A new top-level folder
-that holds projects (`samples/`, `benchmarks/`) needs its own profile in `eng/`, an `` for it in
-`Directory.Build.props`, and its own solution folder. `tests/CheatEngine.Client.Tests.Shared/` reuses the `tests/`
-profile and the `/tests/` solution folder. `docs/` holds no project, so it only gets a file-only solution folder.
-
-| Location | Created when |
-|------------------------------------------|------------------------------------------------------------------------------------|
-| `samples/` | The first public API is worth demonstrating |
-| `tests/CheatEngine.Client.Tests.Shared/` | A second test project needs the same fake |
-| `benchmarks/` | The first performance-sensitive path exists |
-| `docs/` | A document no longer fits in a README (`docs/adr/` with the first decision record) |
+| Package | Purpose | Consume directly when |
+|-------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------|--------------------------------------------------------|
+| [`CheatEngine.Client`](src/CheatEngine.Client/README.md) | Umbrella package for the high-level fluent and hosting experience | Building a normal plugin |
+| [`CheatEngine.Client.Hosting`](libs/CheatEngine.Client.Hosting/README.md) | `CheatEngineClientPlugin` and one-provider-per-activation host | Integrating the host into an existing composition root |
+| [`CheatEngine.Client.Extensions.DependencyInjection`](libs/CheatEngine.Client.Extensions.DependencyInjection/README.md) | Explicit DI registrations, modules, memory codecs, and options | Composing the Client without the plugin base |
+| [`CheatEngine.Client.Fluent`](libs/CheatEngine.Client.Fluent/README.md) | Immutable fluent memory and AOB builders | Depending only on fluent request construction |
+| [`CheatEngine.Client.Abstractions`](libs/CheatEngine.Client.Abstractions/README.md) | Contracts, requests, failures, and value vocabulary | Referencing contracts without an implementation |
+| [`CheatEngine.Client.Core`](libs/CheatEngine.Client.Core/README.md) | SDK-facing implementation | Normally composed through DI, not called directly |
+| [`CheatEngine.Client.Templates`](templates/CheatEngine.Client.Templates/README.md) | `dotnet new ceplugin` | Starting a new plugin |
+
+Package and assembly names describe delivery, not user code. Consumer-facing APIs use functional namespaces such as
+`CheatEngine.Client.Memory`, `.Scanning`, `.Tables`, `.Lua`, `.Processes`, `.Runtime`, and `.Hosting`.
+
+## v0.1.0 capability status
+
+The Client reports runtime capability rather than assuming a particular Cheat Engine global or ownership contract. The
+following table is a delivery statement, not a substitute for a live host check.
+
+| Area | v0.1.0 status | Boundary |
+|-----------------------------------------------------|---------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| Lifecycle, dispatch, DI, modules, options | Available | Per-enable provider and scope; modules stop in reverse order |
+| Runtime facts and selected process | Available | Snapshot and attachment state are re-read through the active host |
+| Typed memory and finite pointer chains | Available | Built-in primitives plus explicitly registered deterministic codecs; strings and byte ranges are bounded |
+| Modules, regions, symbols, and custom-symbol leases | Available | Results are copied; leases are activation-scoped |
+| AOB scanning | Available | Patterns are normalized; terminals are `FirstOrNone`, `RequireSingle`, or bounded `Take` |
+| Address List and memory records | Available | Snapshots and hierarchy materialization are bounded; table file access requires an allowed root |
+| Typed protected Lua and explicit Lua modules | Available | No Lua state crosses the public Client contract |
+| 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).
+
+## AOT, trimming, and deployment
+
+Shipping Client projects target `net10.0`, enable nullable analysis, warnings as errors, trim/AOT compatibility
+analysis, reference-AOT verification, deterministic builds, XML documentation, Source Link, symbol packages, and
+package/API validation. `CheatEngine.Client.AotProbe` publishes the complete Client graph as Native AOT for `win-x64`
+to validate those library constraints.
+
+That is **not** a claim that Cheat Engine can load a Native AOT plugin DLL. The supported deployment remains the
+framework-dependent managed plugin output folder. Deploy it as one unit: your plugin assembly, its `.deps.json` and
+`.runtimeconfig.json`, Client and SDK assemblies, and the SDK's `cheatengine-sdk-lua-bridge.dll` must remain together.
+
+The template sets `IsAotCompatible` and `VerifyReferenceAotCompatibility` to protect the application code path, while
+leaving the plugin itself in the SDK-supported managed form. See [ADR 0003](docs/adr/0003-package-and-aot-policy.md)
+for the package and AOT policy.
+
+## Build and validation
+
+The repository pins the .NET SDK in [global.json](global.json), uses Central Package Management, and commits NuGet
+lock files. Run the normal Windows validation sequence from the repository root:
+
+```powershell
+dotnet restore CheatEngine.Client.slnx --locked-mode
+dotnet build CheatEngine.Client.slnx --configuration Release --no-restore
+dotnet test --solution CheatEngine.Client.slnx --configuration Release --no-build --no-restore
+dotnet pack CheatEngine.Client.slnx --configuration Release --no-build --no-restore
+./eng/Invoke-PackageSmoke.ps1 -PackageSource ./artifacts/packages
+./eng/Invoke-TemplateSmoke.ps1 -PackageSource ./artifacts/packages
+dotnet publish tests/CheatEngine.Client.AotProbe/CheatEngine.Client.AotProbe.csproj --configuration Release --runtime win-x64 --no-restore --output ./artifacts/aot-probe
+./artifacts/aot-probe/CheatEngine.Client.AotProbe.exe
+```
+
+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.
+
+## Security and scope
+
+This project is for local processes you are authorized to inspect or modify. It does not add network control, remote
+transport, or a mechanism to bypass Cheat Engine or host protections.
+
+Table loading can execute Lua in the host. Keep `AllowedTableRoots` empty unless the plugin has an explicit,
+trusted import/export location; an empty set disables table file access. Arbitrary Lua source is separately opt-in and
+should remain disabled unless the plugin has a deliberate trust boundary. Avoid logging target-memory contents or Lua
+source by default.
+
+## Architecture records
+
+The decisions that constrain the public surface and delivery model are maintained as short ADRs:
+
+- [Layered in-process architecture](docs/adr/0001-layered-in-process-architecture.md)
+- [One Client activation per plugin enable epoch](docs/adr/0002-plugin-activation-lifecycle.md)
+- [Package and AOT policy](docs/adr/0003-package-and-aot-policy.md)
+- [Capability delivery matrix](docs/adr/0004-capability-matrix.md)
+
+For the SDK's bootstrap, generated Lua bindings, native bridge, and host ABI details, start with the
+[CheatEngine.SDK README](https://github.com/CheatEngineNet/CheatEngine.SDK#readme).
diff --git a/docs/adr/0001-layered-in-process-architecture.md b/docs/adr/0001-layered-in-process-architecture.md
new file mode 100644
index 0000000..def67fe
--- /dev/null
+++ b/docs/adr/0001-layered-in-process-architecture.md
@@ -0,0 +1,46 @@
+# ADR 0001: Layered in-process Client architecture
+
+- Status: Accepted
+- Date: 2026-09-20
+
+## Context
+
+`CheatEngine.SDK` exposes a managed route into a live Cheat Engine plugin host. Its Lua state, CE objects, ownership
+wrappers, and thread-affinity rules are host-bound implementation details. The Client needs a higher-level,
+dependency-injection-friendly API without recreating the SDK ABI or turning those implementation details into public
+lifetime obligations.
+
+## Decision and why
+
+`CheatEngine.Client` is an in-process, high-level API hosted by a Cheat Engine plugin. It is not an external-process
+adapter for `CheatEngine.SDK`, and V1 has no IPC endpoint.
+
+```text
+Plugin assembly
+ -> CheatEngine.Client.Hosting
+ -> CheatEngine.Client.Extensions.DependencyInjection
+ -> CheatEngine.Client.Core -> CheatEngine.SDK -> Cheat Engine Lua/runtime
+ ^
+CheatEngine.Client.Abstractions <- CheatEngine.Client.Fluent
+```
+
+`Abstractions` owns public contracts, copied value types, failures, and module boundaries. `Fluent` creates immutable
+operation descriptions and has no SDK access. `Core` is the only Client domain layer that maps operations to the SDK.
+The DI extension owns explicit registrations and options validation; `Hosting` connects that composition to the plugin
+lifecycle. The root `CheatEngine.Client` package is the consumer-facing umbrella.
+
+The labels in the diagram are package and assembly identities, not consumer namespace prefixes. Public contracts use
+functional namespaces such as `CheatEngine.Client.Memory`, `.Scanning`, `.Tables`, `.Lua`, `.Runtime`, and `.Processes`
+regardless of their delivery package.
+
+## Consequences and project value
+
+- Public APIs do not expose `LuaState`, `CEObject`, `Owned`, native pointers, or other host-bound SDK lifetimes.
+- Application Lua exports are registered explicitly through `ILuaClient.RegisterModule`; the returned lease owns
+ activation-scoped unregistration. An `ILuaModule` may encapsulate generated SDK bindings internally but cannot let a
+ Lua state or SDK handle cross the Client contract.
+- Builders remain pure. A complete Cheat Engine operation, including temporary-owner cleanup, crosses the SDK boundary
+ as one synchronous operation. This keeps fluent composition testable without a live host.
+- A future remote bridge is a separate plugin-hosted product with its own protocol, authentication, backpressure, and
+ epoch-lifetime decision. It cannot introduce retries, transport concerns, or serializable handles into
+ `ICheatEngineClient` V1.
diff --git a/docs/adr/0002-plugin-activation-lifecycle.md b/docs/adr/0002-plugin-activation-lifecycle.md
new file mode 100644
index 0000000..069bc82
--- /dev/null
+++ b/docs/adr/0002-plugin-activation-lifecycle.md
@@ -0,0 +1,43 @@
+# ADR 0002: One Client activation per plugin enable epoch
+
+- Status: Accepted
+- Date: 2026-09-20
+
+## Context
+
+The SDK creates a plugin through a parameterless constructor and attaches the Lua runtime only for the enabled
+lifetime. A long-lived provider, service, callback, or CE resource would therefore be able to outlive the host state
+that makes it valid.
+
+## Decision and why
+
+`CheatEngineClientPlugin` creates a new `CheatEnginePluginBuilder`, service provider, and DI scope from `OnEnable`.
+The derived plugin adds its own configuration sources in `Configure`; the Client does not load configuration implicitly.
+For file-based configuration, a plugin may explicitly add an optional `appsettings.json` with `reloadOnChange: false`.
+
+The base validates options after building the provider, resolves the activation-scoped aggregate `ICheatEngineClient`,
+enables registered modules in registration order, then invokes `OnClientEnabled`. A failed enable rolls back every
+callback that was entered.
+
+On disable, the base clears the active Client reference first, invokes `OnClientDisabling`, disables enabled modules in
+reverse order, drains Client-owned CE resources while the host is still attached, and finally disposes the scope,
+provider, and configuration. Cleanup is best-effort and aggregates failures only after every step has been attempted.
+
+## Invariants
+
+- SDK calls are forbidden from plugin constructors, field initializers, and static initialization. SDK-dependent
+ services exist only while the plugin is enabled.
+- `Configure` is a one-activation composition hook. It must not build a provider or reconfigure Client options after
+ provider construction; reloadable runtime configuration would violate the activation boundary.
+- A Client scope, cancellation token, SDK handle, Lua reference, or CE-owned resource never crosses an enable/disable
+ epoch. `ICheatEngineClient.Epoch` and `Stopping` identify the active lifetime.
+- Public Client operations are synchronous. They do not retain Lua state across an `await`, and main-thread work enters
+ the SDK dispatcher as a bounded operation.
+
+## Consequences and project value
+
+- Re-enable starts from a new composition rather than a partially disposed singleton graph.
+- Reverse-order cleanup and rollback make module ownership explicit and make failure paths unit-testable without
+ mocking SDK statics.
+- Clearing admission before cleanup prevents consumers from acquiring an activation while its resources are being
+ released.
diff --git a/docs/adr/0003-package-and-aot-policy.md b/docs/adr/0003-package-and-aot-policy.md
new file mode 100644
index 0000000..7b02a80
--- /dev/null
+++ b/docs/adr/0003-package-and-aot-policy.md
@@ -0,0 +1,47 @@
+# ADR 0003: Package and AOT policy
+
+- Status: Accepted
+- Date: 2026-09-20
+
+## Context
+
+The SDK's plugin entry-point generator and native Lua bridge are activated by a direct package reference in the plugin
+project. Indirect NuGet dependencies do not provide a safe substitute for those build assets. At the same time, Native
+AOT compatibility analysis can validate library dependencies without proving that Cheat Engine can load a Native AOT
+plugin DLL.
+
+## Decision and why
+
+The Client baseline is version `0.1.0`, targets .NET 10 with C# 14, and enables trimming and AOT compatibility analysis
+for shipping projects. Central package management pins `CheatEngine.SDK` to `1.0.0` in this repository; SDK-facing
+published packages declare the compatible dependency range `[1.0.0, 2.0.0)`.
+
+The standalone template targets Windows x64 and references the Client, SDK, and JSON configuration provider directly:
+
+```xml
+
+
+
+```
+
+The direct `CheatEngine.SDK` reference is deliberate. It activates the generated Cheat Engine plugin entry point and
+copies the native Lua bridge. Generated plugin projects set `true`;
+the `CheatEngine.Client.Hosting` build target then reports `CECLIENT001` if the project omits that direct SDK reference.
+
+## Delivery verification
+
+Windows CI restores in locked mode; builds and tests Release; packs with public API validation; smoke-tests isolated
+package consumption and local template installation; and publishes then runs the `win-x64` Native AOT reference probe.
+Live Cheat Engine checks are intentionally outside ordinary CI and remain explicit host-validation gates.
+
+## Consequences and project value
+
+- The maintained template is the source example for real package consumption, including the two direct references a
+ plugin needs outside this repository.
+- A standalone generated plugin uses explicit local package versions. A future template using central package management
+ must bring its own `Directory.Packages.props`; it cannot inherit this repository's file.
+- `CheatEngine.Client.AotProbe` validates the shipping graph under Native AOT analysis. It does not claim that Cheat
+ Engine can load a Native AOT plugin; supported deployment remains the managed SDK plugin output together with its
+ runtime configuration and native bridge.
+- A dependency that requires reflection, runtime type discovery, dynamic code, or reflection-based JSON serialization
+ needs a trimming-safe alternative before entering a shipping Client project.
diff --git a/docs/adr/0004-capability-matrix.md b/docs/adr/0004-capability-matrix.md
new file mode 100644
index 0000000..35ec06a
--- /dev/null
+++ b/docs/adr/0004-capability-matrix.md
@@ -0,0 +1,57 @@
+# ADR 0004: Capability delivery matrix
+
+- Status: Accepted
+- Date: 2026-09-20
+
+## Context
+
+An interface alone does not establish that a Cheat Engine operation is safe to create, use, dispose, or repeat across
+plugin activation. In particular, SDK 1.0.0 does not expose the ownership factory required for the Client to create and
+adopt `MemScan` and `FoundList` instances without inventing an unverified handle-lifetime contract.
+
+## Decision and why
+
+The Client reports implementation status separately from public vocabulary. A capability is not represented as usable
+only because an abstraction can describe it.
+
+### Status vocabulary
+
+- **Implemented**: the aggregate `ICheatEngineClient` composes an operational Client implementation for an enable
+ epoch. The listed host boundary still applies before claiming live-host qualification.
+- **Capability-gated**: a public surface exists, but the implementation reports an unsupported capability instead of
+ assuming an unproven SDK ownership, affinity, or cancellation contract.
+- **Deferred**: V1 intentionally provides no operational route through the aggregate Client.
+
+| Area | Public surface | Current source status | Boundary before live qualification |
+|---|---|---|---|
+| Plugin lifecycle and DI | `CheatEngineClientPlugin`, `CheatEnginePluginBuilder`, modules, options | Implemented | Exercise enable, rollback, disable, and repeated epoch activation in a CE host. |
+| Runtime and capability facts | `ICheatEngineRuntime` | Implemented | Add evidence-backed observations per CE version and architecture as the SDK surface expands. |
+| Process, module, and inspection | `IProcessClient`, `IInspectionClient` | Implemented | Map only SDK APIs with verified normal-return and thread contracts. |
+| Typed memory | `IMemoryClient`, codecs, and requests | Implemented | Keep reads and writes bounded and classify host failures without leaking Lua state. |
+| AOB scan | `IPatternScanner`, `AobScanRequest` | Implemented | Copy returned addresses and release temporary SDK owners in the same CE operation. |
+| Value scan | `IValueScanner`, `IValueScanSession`, page records | Capability-gated | Require CE 7.7 x64 evidence for creation, first/next scan, read, ordered destruction, disable, and re-enable. |
+| Address tables | `ITableClient`, copied record contracts | Implemented | Validate current-table lifetime, updates, and configured table-root enforcement. |
+| Protected and unsafe Lua | `ILuaClient`, `IUnsafeLuaClient` | Protected operations implemented; arbitrary source is policy-gated | Keep arbitrary source opt-in and unavailable by default. |
+| IPC or remote Client | None in V1 | Deferred | Define transport, authentication, handle epochs, and backpressure in a separate product decision. |
+
+## Consequences and project value
+
+- `IValueScanner.CreateSession` remains unavailable until the internal `createMemScan` and `createFoundList` owner has
+ passed creation, first and next scans, reading, ordered destruction, disable, and re-enable in Cheat Engine 7.7 x64.
+ The Client will not bypass the SDK restriction with reflection or a hand-rolled public owner.
+- Templates and README examples can expose only guarded operations. They must not imply value scanning, arbitrary Lua,
+ or host-side behavior that ordinary CI has not established.
+- The matrix gives reviewers one place to distinguish a deliberate product gate from a missing implementation, keeping
+ package release claims honest as SDK evidence changes.
+
+## Current template boundary
+
+The template is the maintained source example. It compiles against the hosted plugin shape with explicit JSON
+configuration and no reload watcher, demonstrates explicit module DI and validated options, and registers an
+application-owned `ILuaModule` through `ILuaClient.RegisterModule`. Its activation-scoped lease owns unregistration of
+the generated SDK Lua export. SDK binding calls remain inside that application module; no Lua state or SDK ownership
+handle crosses the Client contract.
+
+The generated project takes an Address List snapshot and performs a guarded AOB and typed-memory probe. It performs no
+operation when the required runtime precondition is absent, does not log target-memory contents, and retains the epoch,
+ownership, and main-thread constraints defined by these ADRs.
diff --git a/docs/adr/README.md b/docs/adr/README.md
new file mode 100644
index 0000000..8f024d2
--- /dev/null
+++ b/docs/adr/README.md
@@ -0,0 +1,32 @@
+# Architecture Decision Records
+
+## Context
+
+These records define the architectural constraints for `CheatEngine.Client`: its public boundaries, plugin lifetime,
+package layout, and delivered capability scope. They complement API documentation; they are not a product roadmap or a
+substitute for the SDK contract.
+
+## Why this directory exists
+
+The Client sits above a host-bound SDK with thread-affinity and ownership rules. Recording the decisions keeps a
+convenient public API from silently acquiring unsafe handles, cross-epoch state, indirect SDK build assets, or
+unsupported host assumptions.
+
+## How the records improve the project
+
+Each ADR is a review boundary. A change that alters a listed decision must update the relevant record or add a new one,
+so source code, package behavior, templates, and CI gates remain aligned.
+
+| Record | Decision | Primary effect |
+|---|---|---|
+| [0001](0001-layered-in-process-architecture.md) | Keep the Client in-process and isolate SDK domain mapping in Core. | Prevent host handles and transport concerns from leaking through functional Client APIs. |
+| [0002](0002-plugin-activation-lifecycle.md) | Create one composition and Client scope for each plugin enable epoch. | Makes cleanup, module rollback, and epoch invalidation deterministic. |
+| [0003](0003-package-and-aot-policy.md) | Require a direct SDK reference in plugin projects and distinguish AOT analysis from an AOT plugin binary. | Preserves SDK generators and native bridge assets at the plugin boundary. |
+| [0004](0004-capability-matrix.md) | Report implemented, gated, and deferred capabilities separately. | Prevents contracts and templates from implying unverified Cheat Engine behavior. |
+
+## Delivery alignment
+
+The Windows CI workflow restores the locked graph, builds and tests Release, packs the public APIs, validates isolated
+package and template consumption, then publishes and runs the Native AOT graph probe. Live Cheat Engine validation is
+intentionally opt-in and remains a release boundary where an ADR identifies it; ordinary CI does not claim to replace
+that host evidence.
diff --git a/eng/Invoke-PackageSmoke.ps1 b/eng/Invoke-PackageSmoke.ps1
new file mode 100644
index 0000000..75a2d55
--- /dev/null
+++ b/eng/Invoke-PackageSmoke.ps1
@@ -0,0 +1,174 @@
+[CmdletBinding()]
+param(
+ [Parameter(Mandatory)]
+ [ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
+ [string]$PackageSource,
+
+ [ValidatePattern('^\d+\.\d+\.\d+([-.].+)?$')]
+ [string]$ClientVersion = '0.1.0'
+)
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+
+$resolvedPackageSource = (Resolve-Path -LiteralPath $PackageSource).Path
+$expectedPackage = Join-Path $resolvedPackageSource "CheatEngine.Client.$ClientVersion.nupkg"
+if (-not (Test-Path -LiteralPath $expectedPackage -PathType Leaf)) {
+ throw "Expected package '$expectedPackage' 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)) {
+ throw "Refusing to use a smoke-test directory outside the system temporary directory: '$smokeDirectory'."
+}
+
+function Write-SmokeProject {
+ param(
+ [Parameter(Mandatory)] [string]$ProjectDirectory,
+ [Parameter(Mandatory)] [bool]$IncludeSdkReference
+ )
+
+ $sdkReference = if ($IncludeSdkReference) {
+ ' '
+ }
+ else {
+ ''
+ }
+
+ $projectXml = @"
+
+
+ net10.0
+ 14.0
+ enable
+ enable
+ true
+ false
+ true
+ obj/Generated
+
+
+
+$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
+{
+ protected override void Configure(CheatEnginePluginBuilder builder)
+ {
+ }
+
+ protected override void OnClientEnabled(ICheatEngineClient client)
+ {
+ _ = client.Memory.At(default(Address));
+ _ = client.Patterns.Aob("00").FirstOrNone();
+ }
+}
+'@
+ Set-Content -LiteralPath (Join-Path $ProjectDirectory 'Plugin.cs') -Value $pluginSource -Encoding utf8NoBOM
+}
+
+try {
+ New-Item -ItemType Directory -Path $smokeDirectory | Out-Null
+
+ $configurationPath = Join-Path $smokeDirectory 'NuGet.Config'
+ $escapedSource = [Security.SecurityElement]::Escape($resolvedPackageSource)
+ $escapedPackageCache = [Security.SecurityElement]::Escape((Join-Path $smokeDirectory '.packages'))
+ $nuGetConfiguration = @"
+
+
+
+
+
+
+
+
+
+
+
+"@
+ Set-Content -LiteralPath $configurationPath -Value $nuGetConfiguration -Encoding utf8NoBOM
+
+ $positiveDirectory = Join-Path $smokeDirectory 'positive'
+ New-Item -ItemType Directory -Path $positiveDirectory | Out-Null
+ Write-SmokeProject -ProjectDirectory $positiveDirectory -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
+ if ($LASTEXITCODE -ne 0) {
+ throw 'The positive isolated package build failed.'
+ }
+
+ $positiveOutput = Join-Path $positiveDirectory 'bin/Release/net10.0'
+ $requiredOutputFiles = @(
+ 'Smoke.Plugin.dll',
+ 'Smoke.Plugin.runtimeconfig.json',
+ 'cheatengine-sdk-lua-bridge.dll',
+ 'CheatEngine.SDK.dll',
+ 'CheatEngine.Client.Abstractions.dll',
+ 'CheatEngine.Client.Core.dll',
+ 'CheatEngine.Client.Fluent.dll',
+ 'CheatEngine.Client.Extensions.DependencyInjection.dll',
+ 'CheatEngine.Client.Hosting.dll'
+ )
+ foreach ($requiredOutputFile in $requiredOutputFiles) {
+ $requiredOutputPath = Join-Path $positiveOutput $requiredOutputFile
+ if (-not (Test-Path -LiteralPath $requiredOutputPath -PathType Leaf)) {
+ throw "The positive isolated package output is missing '$requiredOutputFile'."
+ }
+ }
+
+ $generatedEntryPoints = @(Get-ChildItem -LiteralPath (Join-Path $positiveDirectory 'obj') -Recurse -File |
+ Where-Object Name -eq 'CheatEngine.SDK.EntryPoint.g.cs')
+ if ($generatedEntryPoints.Count -ne 1) {
+ throw "Expected exactly one generated CESDK bootstrap source, found $($generatedEntryPoints.Count)."
+ }
+ $generatedEntryPoint = $generatedEntryPoints[0]
+ $generatedEntryPointText = Get-Content -LiteralPath $generatedEntryPoint.FullName -Raw
+ if ($generatedEntryPointText -notmatch 'namespace CESDK' -or
+ $generatedEntryPointText -notmatch 'CEPluginInitialize') {
+ throw 'The direct SDK reference did not emit the expected CESDK bootstrap source.'
+ }
+
+ $negativeDirectory = Join-Path $smokeDirectory 'negative'
+ New-Item -ItemType Directory -Path $negativeDirectory | Out-Null
+ Write-SmokeProject -ProjectDirectory $negativeDirectory -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.'
+ }
+ if ($negativeOutput -notmatch 'CECLIENT001') {
+ throw "The negative isolated plugin build failed, but did not report CECLIENT001.`n$negativeOutput"
+ }
+
+ # 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.'
+}
+finally {
+ if (Test-Path -LiteralPath $smokeDirectory -PathType Container) {
+ Remove-Item -LiteralPath $smokeDirectory -Recurse -Force
+ }
+}
diff --git a/eng/Invoke-TemplateSmoke.ps1 b/eng/Invoke-TemplateSmoke.ps1
new file mode 100644
index 0000000..7384445
--- /dev/null
+++ b/eng/Invoke-TemplateSmoke.ps1
@@ -0,0 +1,98 @@
+[CmdletBinding()]
+param(
+ [Parameter(Mandatory)]
+ [ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
+ [string]$PackageSource,
+
+ [ValidatePattern('^\d+\.\d+\.\d+([-.].+)?$')]
+ [string]$TemplateVersion = '0.1.0'
+)
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+
+$resolvedPackageSource = (Resolve-Path -LiteralPath $PackageSource).Path
+$templatePackage = Join-Path $resolvedPackageSource "CheatEngine.Client.Templates.$TemplateVersion.nupkg"
+if (-not (Test-Path -LiteralPath $templatePackage -PathType Leaf)) {
+ throw "Expected template package '$templatePackage' 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.TemplateSmoke." + [Guid]::NewGuid().ToString('N'))))
+if (-not $smokeDirectory.StartsWith($temporaryBase, [StringComparison]::OrdinalIgnoreCase)) {
+ throw "Refusing to use a smoke-test directory outside the system temporary directory: '$smokeDirectory'."
+}
+
+$previousDotnetCliHome = $env:DOTNET_CLI_HOME
+$previousDotnetNewHome = $env:DOTNET_NEW_HOME
+try {
+ New-Item -ItemType Directory -Path $smokeDirectory | Out-Null
+ $env:DOTNET_CLI_HOME = Join-Path $smokeDirectory '.dotnet-cli'
+ $env:DOTNET_NEW_HOME = Join-Path $smokeDirectory '.template-engine'
+
+ $configurationPath = Join-Path $smokeDirectory 'NuGet.Config'
+ $escapedSource = [Security.SecurityElement]::Escape($resolvedPackageSource)
+ $escapedPackageCache = [Security.SecurityElement]::Escape((Join-Path $smokeDirectory '.packages'))
+ $nuGetConfiguration = @"
+
+
+
+
+
+
+
+
+
+
+
+"@
+ Set-Content -LiteralPath $configurationPath -Value $nuGetConfiguration -Encoding utf8NoBOM
+
+ & dotnet new install $templatePackage --force
+ if ($LASTEXITCODE -ne 0) {
+ throw 'Local template installation failed.'
+ }
+
+ & dotnet new ceplugin --dry-run --name Smoke.Plugin --output (Join-Path $smokeDirectory 'dry-run')
+ if ($LASTEXITCODE -ne 0) {
+ throw 'Template dry run failed.'
+ }
+
+ $instantiatedDirectory = Join-Path $smokeDirectory 'Smoke.Plugin'
+ & dotnet new ceplugin --name Smoke.Plugin --output $instantiatedDirectory
+ if ($LASTEXITCODE -ne 0) {
+ throw 'Template instantiation failed.'
+ }
+
+ $projectPath = Join-Path $instantiatedDirectory 'Smoke.Plugin.csproj'
+ & dotnet restore $projectPath --configfile $configurationPath
+ if ($LASTEXITCODE -ne 0) {
+ throw 'Instantiated template restore failed.'
+ }
+
+ & dotnet build $projectPath --configuration Release --no-restore
+ if ($LASTEXITCODE -ne 0) {
+ throw 'Instantiated template build failed.'
+ }
+
+ Write-Host 'Template smoke test passed: local installation, dry run, instantiation, restore, and Release build succeeded.'
+}
+finally {
+ if ([string]::IsNullOrEmpty($previousDotnetCliHome)) {
+ Remove-Item Env:DOTNET_CLI_HOME -ErrorAction SilentlyContinue
+ }
+ else {
+ $env:DOTNET_CLI_HOME = $previousDotnetCliHome
+ }
+
+ if ([string]::IsNullOrEmpty($previousDotnetNewHome)) {
+ Remove-Item Env:DOTNET_NEW_HOME -ErrorAction SilentlyContinue
+ }
+ else {
+ $env:DOTNET_NEW_HOME = $previousDotnetNewHome
+ }
+
+ if (Test-Path -LiteralPath $smokeDirectory -PathType Container) {
+ Remove-Item -LiteralPath $smokeDirectory -Recurse -Force
+ }
+}
diff --git a/eng/Shipping.props b/eng/Shipping.props
index 8f3d8a9..0983b8d 100644
--- a/eng/Shipping.props
+++ b/eng/Shipping.props
@@ -3,5 +3,25 @@
net10.0
+ true
+ true
+ true
+ true
+ snupkg
+ true
+ true
+ $(ArtifactsPath)/packages
+ Fluent, dependency-injection-friendly C# APIs for authorized local Cheat Engine plugins.
+ cheat-engine;plugin;memory;dotnet;fluent;dependency-injection
+
+
+
+
+
+
+
diff --git a/eng/Templates.props b/eng/Templates.props
new file mode 100644
index 0000000..1a1c92e
--- /dev/null
+++ b/eng/Templates.props
@@ -0,0 +1,9 @@
+
+
+ net10.0
+ false
+ $(ArtifactsPath)/packages
+ dotnet new template for an in-process, dependency-injection-first Cheat Engine plugin.
+ dotnet-new;template;cheat-engine;plugin
+
+
diff --git a/eng/Tests.props b/eng/Tests.props
index 3da4387..b66e29e 100644
--- a/eng/Tests.props
+++ b/eng/Tests.props
@@ -5,4 +5,17 @@
net10.0
false
+
+
+ true
+ Exe
+ true
+
+
+
+
+
+
+
+
diff --git a/global.json b/global.json
index b5715a7..9773a5f 100644
--- a/global.json
+++ b/global.json
@@ -3,5 +3,8 @@
"version": "10.0.401",
"rollForward": "latestFeature",
"allowPrerelease": false
+ },
+ "test": {
+ "runner": "Microsoft.Testing.Platform"
}
}
diff --git a/libs/CheatEngine.Client.Abstractions/CheatEngine.Client.Abstractions.csproj b/libs/CheatEngine.Client.Abstractions/CheatEngine.Client.Abstractions.csproj
index 61718a1..294c798 100644
--- a/libs/CheatEngine.Client.Abstractions/CheatEngine.Client.Abstractions.csproj
+++ b/libs/CheatEngine.Client.Abstractions/CheatEngine.Client.Abstractions.csproj
@@ -1,3 +1,14 @@
+
+ CheatEngine.Client
+
+
+
+
+
+
diff --git a/libs/CheatEngine.Client.Abstractions/Dispatching/ICheatEngineDispatcher.cs b/libs/CheatEngine.Client.Abstractions/Dispatching/ICheatEngineDispatcher.cs
new file mode 100644
index 0000000..080f467
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Dispatching/ICheatEngineDispatcher.cs
@@ -0,0 +1,37 @@
+using System.Diagnostics.CodeAnalysis;
+
+using CheatEngine.Client.Results;
+
+namespace CheatEngine.Client.Dispatching;
+
+/// Synchronously dispatches managed work to Cheat Engine's captured main thread.
+public interface ICheatEngineDispatcher
+{
+ /// Gets whether the caller is already on Cheat Engine's captured main thread.
+ public bool IsMainThread
+ {
+ get;
+ }
+
+ /// Runs a callback on the captured main thread.
+ ///
+ /// is observed before dispatch admission only. It never attempts to
+ /// interrupt a callback or Lua primitive that has already begun on Cheat Engine's main thread.
+ ///
+ public bool TryInvoke(Action callback, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Runs a callback on the captured main thread and returns its managed result.
+ /// Cancellation is observed before dispatch admission and never while the callback is running.
+ public bool TryInvoke(Func callback, [MaybeNullWhen(false)] out T result,
+ out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Runs a callback on the captured main thread or throws when dispatch fails.
+ /// Cancellation is observed before dispatch admission only, never while a callback is running.
+ public void Invoke(Action callback, CancellationToken cancellationToken = default);
+
+ /// Runs a callback on the captured main thread or throws when dispatch fails.
+ /// Cancellation is observed before dispatch admission only, never while a callback is running.
+ public T Invoke(Func callback, CancellationToken cancellationToken = default);
+}
diff --git a/libs/CheatEngine.Client.Abstractions/ICheatEngineClient.cs b/libs/CheatEngine.Client.Abstractions/ICheatEngineClient.cs
new file mode 100644
index 0000000..a5c1938
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/ICheatEngineClient.cs
@@ -0,0 +1,85 @@
+using CheatEngine.Client.Dispatching;
+using CheatEngine.Client.Inspection;
+using CheatEngine.Client.Lua;
+using CheatEngine.Client.Memory;
+using CheatEngine.Client.Processes;
+using CheatEngine.Client.Runtime;
+using CheatEngine.Client.Scanning;
+using CheatEngine.Client.Tables;
+
+namespace CheatEngine.Client;
+
+/// A scoped, high-level client for the active Cheat Engine plugin lifecycle.
+///
+/// The client never exposes Lua states, CE object handles, or SDK ownership wrappers. Its members are synchronous
+/// because
+/// an attached Cheat Engine Lua runtime cannot safely be retained across an await boundary.
+///
+public interface ICheatEngineClient
+{
+ /// Gets the SDK lifecycle epoch captured for this scoped client.
+ public long Epoch
+ {
+ get;
+ }
+
+ /// Gets a token cancelled when the active plugin lifecycle begins stopping.
+ public CancellationToken Stopping
+ {
+ get;
+ }
+
+ /// Gets the runtime and capability service.
+ public ICheatEngineRuntime Runtime
+ {
+ get;
+ }
+
+ /// Gets the explicit main-thread dispatcher.
+ public ICheatEngineDispatcher Dispatcher
+ {
+ get;
+ }
+
+ /// Gets the selected-target process service.
+ public IProcessClient Processes
+ {
+ get;
+ }
+
+ /// Gets typed target-memory operations.
+ public IMemoryClient Memory
+ {
+ get;
+ }
+
+ /// Gets AOB and value-scan operations.
+ public IPatternScanner Patterns
+ {
+ get;
+ }
+
+ /// Gets value-scan operations.
+ public IValueScanner Scans
+ {
+ get;
+ }
+
+ /// Gets copied module, symbol, and region inspection operations.
+ public IInspectionClient Inspection
+ {
+ get;
+ }
+
+ /// Gets copied address-table and memory-record operations.
+ public ITableClient Tables
+ {
+ get;
+ }
+
+ /// Gets the protected, handle-free Lua execution service.
+ public ILuaClient Lua
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Inspection/IInspectionClient.cs b/libs/CheatEngine.Client.Abstractions/Inspection/IInspectionClient.cs
new file mode 100644
index 0000000..2e64d8b
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Inspection/IInspectionClient.cs
@@ -0,0 +1,100 @@
+using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
+
+using CheatEngine.Client.Results;
+using CheatEngine.SDK.Engine.Inspection;
+using CheatEngine.SDK.Engine.Values;
+
+namespace CheatEngine.Client.Inspection;
+
+/// Reads copied modules, sections, symbols, and memory regions from the selected Cheat Engine target.
+public interface IInspectionClient
+{
+ /// Tries to copy the target modules, optionally for an explicit process identifier.
+ public bool TryGetModules(
+ InspectionCollectionRequest request,
+ out ImmutableArray modules,
+ out CheatEngineFailure failure,
+ TargetProcessId? processId = null,
+ CancellationToken cancellationToken = default);
+
+ /// Copies the target modules or throws when inspection fails.
+ public ImmutableArray GetModules(InspectionCollectionRequest request,
+ TargetProcessId? processId = null, CancellationToken cancellationToken = default);
+
+ /// Copies the sections belonging to one module.
+ public bool TryGetModuleSections(
+ ModuleName moduleName,
+ InspectionCollectionRequest request,
+ out ImmutableArray sections,
+ out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Copies one module's sections or throws when inspection fails.
+ public ImmutableArray GetModuleSections(ModuleName moduleName,
+ InspectionCollectionRequest request, CancellationToken cancellationToken = default);
+
+ /// Copies the target memory-region map.
+ public bool TryGetMemoryRegions(
+ InspectionCollectionRequest request,
+ out ImmutableArray regions,
+ out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Copies the target memory-region map or throws when inspection fails.
+ public ImmutableArray GetMemoryRegions(InspectionCollectionRequest request,
+ CancellationToken cancellationToken = default);
+
+ /// Copies the memory-region metadata containing one target address.
+ public bool TryGetMemoryRegion(
+ Address address,
+ out MemoryRegionInfo region,
+ out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Gets one region or throws when inspection fails.
+ public MemoryRegionInfo GetMemoryRegion(Address address, CancellationToken cancellationToken = default);
+
+ /// Copies metadata for one Cheat Engine symbol expression.
+ public bool TryGetSymbol(
+ SymbolExpression expression,
+ out SymbolInfo symbol,
+ out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Gets symbol information or throws when inspection fails.
+ public SymbolInfo GetSymbol(SymbolExpression expression, CancellationToken cancellationToken = default);
+
+ /// Resolves the best Cheat Engine symbol name for one target address.
+ public bool TryResolveName(
+ Address address,
+ [NotNullWhen(true)] out string? name,
+ out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Resolves a symbol name or throws when no name can be resolved.
+ public string ResolveName(Address address, CancellationToken cancellationToken = default);
+
+ /// Registers a Client-owned Cheat Engine symbol and returns the lease that removes it.
+ public bool TryRegisterSymbol(
+ SymbolRegistration registration,
+ [NotNullWhen(true)] out ISymbolRegistrationLease? lease,
+ out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Registers a Client-owned symbol or throws when Cheat Engine rejects the registration.
+ public ISymbolRegistrationLease RegisterSymbol(SymbolRegistration registration,
+ CancellationToken cancellationToken = default);
+
+ /// Resolves one Cheat Engine address expression with the documented SDK options.
+ public bool TryResolveAddress(
+ SymbolExpression expression,
+ AddressResolutionOptions options,
+ out Address address,
+ out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Resolves one address or throws when resolution fails.
+ public Address ResolveAddress(SymbolExpression expression, AddressResolutionOptions options,
+ CancellationToken cancellationToken = default);
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Inspection/ISymbolRegistrationLease.cs b/libs/CheatEngine.Client.Abstractions/Inspection/ISymbolRegistrationLease.cs
new file mode 100644
index 0000000..24c2d14
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Inspection/ISymbolRegistrationLease.cs
@@ -0,0 +1,29 @@
+using CheatEngine.SDK.Engine.Values;
+
+namespace CheatEngine.Client.Inspection;
+
+/// Owns one Client-created Cheat Engine symbol registration for the current activation epoch.
+///
+/// Disposing the lease removes only the symbol registered through this lease. The activation owner also disposes a
+/// forgotten lease before the SDK detaches the Lua runtime.
+///
+public interface ISymbolRegistrationLease : IDisposable
+{
+ /// Gets the exact symbol name registered in Cheat Engine.
+ public string Name
+ {
+ get;
+ }
+
+ /// Gets the target address bound to .
+ public Address Address
+ {
+ get;
+ }
+
+ /// Gets a value indicating whether this lease has already released its registration.
+ public bool IsReleased
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Inspection/InspectionCollectionRequest.cs b/libs/CheatEngine.Client.Abstractions/Inspection/InspectionCollectionRequest.cs
new file mode 100644
index 0000000..1aced3c
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Inspection/InspectionCollectionRequest.cs
@@ -0,0 +1,18 @@
+namespace CheatEngine.Client.Inspection;
+
+/// Bounds the number of copied inspection records an operation may materialize.
+public readonly record struct InspectionCollectionRequest
+{
+ /// Creates a bounded inspection request.
+ public InspectionCollectionRequest(int maximumItems)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maximumItems);
+ MaximumItems = maximumItems;
+ }
+
+ /// Gets the maximum number of copied records the caller permits.
+ public int MaximumItems
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Inspection/SymbolRegistration.cs b/libs/CheatEngine.Client.Abstractions/Inspection/SymbolRegistration.cs
new file mode 100644
index 0000000..e4680dc
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Inspection/SymbolRegistration.cs
@@ -0,0 +1,39 @@
+using CheatEngine.SDK.Engine.Values;
+
+namespace CheatEngine.Client.Inspection;
+
+/// Describes one explicitly Client-owned symbol registration.
+///
+/// Names share Cheat Engine's session-wide symbol table. Callers should use a plugin-specific prefix and retain the
+/// resulting lease instead of registering a name already owned by another component.
+///
+public readonly record struct SymbolRegistration
+{
+ /// Creates a custom symbol definition.
+ public SymbolRegistration(string name, Address address, bool doNotSave = true)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(name);
+
+ Name = name;
+ Address = address;
+ DoNotSave = doNotSave;
+ }
+
+ /// Gets the session-wide symbol name.
+ public string Name
+ {
+ get;
+ }
+
+ /// Gets the target address to bind.
+ public Address Address
+ {
+ get;
+ }
+
+ /// Gets whether Cheat Engine should omit this registration when saving a table.
+ public bool DoNotSave
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Lua/ILuaClient.cs b/libs/CheatEngine.Client.Abstractions/Lua/ILuaClient.cs
new file mode 100644
index 0000000..b6d6349
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Lua/ILuaClient.cs
@@ -0,0 +1,30 @@
+using System.Diagnostics.CodeAnalysis;
+
+using CheatEngine.Client.Results;
+
+namespace CheatEngine.Client.Lua;
+
+/// Executes typed Lua operations without exposing a Lua state or CE object handle.
+public interface ILuaClient
+{
+ /// Tries to register one explicitly supplied application Lua module on Cheat Engine's main thread.
+ ///
+ /// The Client neither scans assemblies nor discovers exports by reflection. The module owns its generated SDK
+ /// calls; the returned lease gives the Client deterministic, activation-scoped cleanup ownership.
+ ///
+ public bool TryRegisterModule(
+ ILuaModule luaModule,
+ [NotNullWhen(true)] out ILuaModuleLease? lease,
+ out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Registers one explicitly supplied application Lua module or throws when registration fails.
+ public ILuaModuleLease RegisterModule(ILuaModule luaModule, CancellationToken cancellationToken = default);
+
+ /// 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);
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Lua/ILuaExecutionContext.cs b/libs/CheatEngine.Client.Abstractions/Lua/ILuaExecutionContext.cs
new file mode 100644
index 0000000..c960cb1
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Lua/ILuaExecutionContext.cs
@@ -0,0 +1,30 @@
+namespace CheatEngine.Client.Lua;
+
+///
+/// Describes the short-lived, handle-free scope in which a typed Lua operation executes.
+///
+///
+/// A context is valid only while the typed operation invocation that received it is running. It deliberately exposes
+/// neither LuaState, LuaRef, CE objects nor raw Lua stack access.
+/// Operations should use their SDK-generated [LuaGlobal] bindings while this context is active.
+///
+public interface ILuaExecutionContext
+{
+ /// Gets the plugin activation epoch captured for this operation.
+ public long Epoch
+ {
+ get;
+ }
+
+ /// Gets whether the context is still valid on the current Client execution boundary.
+ public bool IsActive
+ {
+ get;
+ }
+
+ /// Throws when this context no longer belongs to the active plugin activation.
+ ///
+ /// The operation returned, its activation ended, or the context was observed outside its Client execution boundary.
+ ///
+ public void ThrowIfExpired();
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Lua/ILuaModule.cs b/libs/CheatEngine.Client.Abstractions/Lua/ILuaModule.cs
new file mode 100644
index 0000000..c50778d
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Lua/ILuaModule.cs
@@ -0,0 +1,25 @@
+namespace CheatEngine.Client.Lua;
+
+/// Explicitly registers one application-owned set of Lua exports for the current Client activation.
+///
+/// The Client invokes both methods synchronously on Cheat Engine's main thread and owns the resulting registration
+/// lease. An implementation can call its SDK-generated RegisterLuaFunctions and
+/// UnregisterLuaFunctions methods internally, including acquisition of the SDK state required by those
+/// generated methods. No SDK Lua state, reference, or raw stack access crosses this Client contract.
+///
+public interface ILuaModule
+{
+ /// Registers this module's exports for the current Cheat Engine activation.
+ ///
+ /// Throw when the generated SDK registration reports a non-success status so
+ /// can return the mapped failure.
+ ///
+ public void Register();
+
+ /// Unregisters this module's exports for the current Cheat Engine activation.
+ ///
+ /// This is called at most once by the Client, either by disposing the returned lease or during activation
+ /// cleanup. Implementations should make their own cleanup safe if an SDK operation reports a failure.
+ ///
+ public void Unregister();
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Lua/ILuaModuleLease.cs b/libs/CheatEngine.Client.Abstractions/Lua/ILuaModuleLease.cs
new file mode 100644
index 0000000..bc448ca
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Lua/ILuaModuleLease.cs
@@ -0,0 +1,22 @@
+namespace CheatEngine.Client.Lua;
+
+/// Owns one explicit Lua module registration in a single Cheat Engine client epoch.
+///
+/// Disposing the lease unregisters its module synchronously on Cheat Engine's main thread. Forgotten leases are
+/// released in LIFO order while the activation cleanup scope still permits SDK dispatch. Disposing a lease more
+/// than once has no effect. The lease never exposes a Lua state, Lua reference, or native Lua handle.
+///
+public interface ILuaModuleLease : IDisposable
+{
+ /// Gets the Client activation epoch that owns this registration.
+ public long Epoch
+ {
+ get;
+ }
+
+ /// Gets whether this lease has already completed its one release attempt.
+ public bool IsReleased
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Lua/ILuaOperation.cs b/libs/CheatEngine.Client.Abstractions/Lua/ILuaOperation.cs
new file mode 100644
index 0000000..d13e675
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Lua/ILuaOperation.cs
@@ -0,0 +1,22 @@
+using System.Diagnostics.CodeAnalysis;
+
+using CheatEngine.Client.Results;
+
+namespace CheatEngine.Client.Lua;
+
+/// A typed, handle-free Lua operation implemented with SDK-generated bindings.
+/// The copied managed result type.
+public interface ILuaOperation
+{
+ /// Executes while the Client holds a valid activation and main-thread boundary.
+ /// The ephemeral, handle-free context for this one synchronous operation.
+ /// The copied managed result on success.
+ /// The expected operation failure on failure.
+ /// when the operation completed successfully.
+ ///
+ /// Do not retain . Generated SDK bindings own their own protected Lua calls; this
+ /// context only proves that the Client dispatched the operation in its active activation scope.
+ ///
+ public bool TryExecute(ILuaExecutionContext context, [MaybeNullWhen(false)] out TResult result,
+ out CheatEngineFailure failure);
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Lua/IUnsafeLuaClient.cs b/libs/CheatEngine.Client.Abstractions/Lua/IUnsafeLuaClient.cs
new file mode 100644
index 0000000..38b7277
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Lua/IUnsafeLuaClient.cs
@@ -0,0 +1,14 @@
+using CheatEngine.Client.Results;
+
+namespace CheatEngine.Client.Lua;
+
+/// Opt-in execution of trusted arbitrary Lua source.
+public interface IUnsafeLuaClient
+{
+ /// Tries to execute trusted Lua source through the SDK protected-call boundary.
+ public bool TryExecute(LuaScript script, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Executes trusted Lua source or throws when execution fails.
+ public void Execute(LuaScript script, CancellationToken cancellationToken = default);
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Lua/LuaScript.cs b/libs/CheatEngine.Client.Abstractions/Lua/LuaScript.cs
new file mode 100644
index 0000000..0f709eb
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Lua/LuaScript.cs
@@ -0,0 +1,30 @@
+namespace CheatEngine.Client.Lua;
+
+/// An immutable Lua chunk to execute under the SDK's protected Lua boundary.
+public readonly record struct LuaScript
+{
+ /// Creates a Lua script request.
+ public LuaScript(string source, string? chunkName = null)
+ {
+ ArgumentNullException.ThrowIfNull(source);
+ if (chunkName is { Length: 0 })
+ {
+ throw new ArgumentException("A Lua chunk name must be null or non-empty.", nameof(chunkName));
+ }
+
+ Source = source;
+ ChunkName = chunkName;
+ }
+
+ /// Gets the UTF-16 source text to encode as UTF-8 for Lua.
+ public string Source
+ {
+ get;
+ }
+
+ /// Gets the optional chunk name used in Lua diagnostics.
+ public string? ChunkName
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Memory/IMemoryClient.cs b/libs/CheatEngine.Client.Abstractions/Memory/IMemoryClient.cs
new file mode 100644
index 0000000..7e61307
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Memory/IMemoryClient.cs
@@ -0,0 +1,76 @@
+using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
+
+using CheatEngine.Client.Results;
+using CheatEngine.SDK.Engine.Values;
+
+namespace CheatEngine.Client.Memory;
+
+/// Executes typed target-memory reads and writes without exposing a Lua state.
+public interface IMemoryClient
+{
+ /// Tries to read a built-in scalar or pointer type without requiring a custom codec.
+ public bool TryReadPrimitive(Address address, [MaybeNullWhen(false)] out T value,
+ out CheatEngineFailure failure, CancellationToken cancellationToken = default);
+
+ /// Reads a built-in scalar or pointer type or throws when unsupported or rejected.
+ public T ReadPrimitive(Address address, CancellationToken cancellationToken = default);
+
+ /// Tries to write a built-in scalar or pointer type without requiring a custom codec.
+ public bool TryWritePrimitive(Address address, T value, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// 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 copy an exact, caller-bounded byte range.
+ public bool TryReadBytes(MemoryBytesReadRequest request, out ImmutableArray bytes,
+ out CheatEngineFailure failure, CancellationToken cancellationToken = default);
+
+ /// Copies an exact byte range or throws on an expected host failure.
+ public ImmutableArray ReadBytes(MemoryBytesReadRequest request,
+ CancellationToken cancellationToken = default);
+
+ /// Tries to write an immutable byte request.
+ public bool TryWriteBytes(MemoryBytesWriteRequest request, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Writes bytes or throws on an expected host failure.
+ public void WriteBytes(MemoryBytesWriteRequest request, CancellationToken cancellationToken = default);
+
+ /// Tries to read a string with an explicit maximum length.
+ public bool TryReadString(MemoryStringReadRequest request, [NotNullWhen(true)] out string? value,
+ out CheatEngineFailure failure, CancellationToken cancellationToken = default);
+
+ /// Reads a bounded string or throws on an expected host failure.
+ public string ReadString(MemoryStringReadRequest request, CancellationToken cancellationToken = default);
+
+ /// Tries to write managed text with an explicit narrow/wide target representation.
+ public bool TryWriteString(MemoryStringWriteRequest request, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Writes managed text or throws on an expected host failure.
+ public void WriteString(MemoryStringWriteRequest request, CancellationToken cancellationToken = default);
+
+ /// Tries to resolve a finite target-aware pointer chain.
+ public bool TryResolvePointerChain(PointerChainRequest request, out Address address,
+ out CheatEngineFailure failure, CancellationToken cancellationToken = default);
+
+ /// Resolves a finite pointer chain or throws on an expected host failure.
+ public Address ResolvePointerChain(PointerChainRequest request,
+ CancellationToken cancellationToken = default);
+
+ /// Tries to read one typed value from target memory.
+ public bool TryRead(MemoryReadRequest request, [MaybeNullWhen(false)] out T value,
+ out CheatEngineFailure failure, CancellationToken cancellationToken = default);
+
+ /// Reads one typed value or throws when the operation fails.
+ public T Read(MemoryReadRequest request, CancellationToken cancellationToken = default);
+
+ /// Tries to write one typed value to target memory.
+ public bool TryWrite(MemoryWriteRequest request, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Writes one typed value or throws when the operation fails.
+ public void Write(MemoryWriteRequest request, CancellationToken cancellationToken = default);
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Memory/IMemoryCodec.cs b/libs/CheatEngine.Client.Abstractions/Memory/IMemoryCodec.cs
new file mode 100644
index 0000000..4e96201
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Memory/IMemoryCodec.cs
@@ -0,0 +1,20 @@
+using System.Diagnostics.CodeAnalysis;
+
+using CheatEngine.SDK.Engine.Values;
+
+namespace CheatEngine.Client.Memory;
+
+/// Maps one managed value type to bounded target-memory operations.
+/// The managed value type.
+///
+/// Implementations must be deterministic, allocation-conscious, AOT-safe, and independent of Lua or CE object
+/// handles. A codec is invoked synchronously on Cheat Engine's dispatch boundary and must not retain its context.
+///
+public interface IMemoryCodec
+{
+ /// Tries to read one value from the requested target address.
+ public bool TryRead(IMemoryReadContext context, Address address, [MaybeNullWhen(false)] out T value);
+
+ /// Tries to write one value to the requested target address.
+ public bool TryWrite(IMemoryWriteContext context, Address address, in T value);
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Memory/IMemoryReadContext.cs b/libs/CheatEngine.Client.Abstractions/Memory/IMemoryReadContext.cs
new file mode 100644
index 0000000..ca411be
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Memory/IMemoryReadContext.cs
@@ -0,0 +1,16 @@
+using CheatEngine.SDK.Engine.Values;
+
+namespace CheatEngine.Client.Memory;
+
+/// Provides the bounded raw-memory operations available to an application read codec.
+public interface IMemoryReadContext
+{
+ /// Gets the selected target's pointer size in bytes.
+ public int PointerSize
+ {
+ get;
+ }
+
+ /// Tries to fill the exact caller-provided buffer from target memory.
+ public bool TryReadBytes(Address address, Span destination);
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Memory/IMemoryWriteContext.cs b/libs/CheatEngine.Client.Abstractions/Memory/IMemoryWriteContext.cs
new file mode 100644
index 0000000..f7cc6ab
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Memory/IMemoryWriteContext.cs
@@ -0,0 +1,16 @@
+using CheatEngine.SDK.Engine.Values;
+
+namespace CheatEngine.Client.Memory;
+
+/// Provides the bounded raw-memory operations available to an application write codec.
+public interface IMemoryWriteContext
+{
+ /// Gets the selected target's pointer size in bytes.
+ public int PointerSize
+ {
+ get;
+ }
+
+ /// Tries to write the exact caller-provided bytes to target memory.
+ public bool TryWriteBytes(Address address, ReadOnlySpan source);
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Memory/MemoryBytesReadRequest.cs b/libs/CheatEngine.Client.Abstractions/Memory/MemoryBytesReadRequest.cs
new file mode 100644
index 0000000..37736cb
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Memory/MemoryBytesReadRequest.cs
@@ -0,0 +1,27 @@
+using CheatEngine.SDK.Engine.Values;
+
+namespace CheatEngine.Client.Memory;
+
+/// A bounded request to copy a fixed number of target bytes.
+public readonly record struct MemoryBytesReadRequest
+{
+ /// Creates a bounded byte read.
+ public MemoryBytesReadRequest(Address address, int length)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(length);
+ Address = address;
+ Length = length;
+ }
+
+ /// Gets the first target address.
+ public Address Address
+ {
+ get;
+ }
+
+ /// Gets the exact number of bytes to copy.
+ public int Length
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Memory/MemoryBytesWriteRequest.cs b/libs/CheatEngine.Client.Abstractions/Memory/MemoryBytesWriteRequest.cs
new file mode 100644
index 0000000..a64b651
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Memory/MemoryBytesWriteRequest.cs
@@ -0,0 +1,33 @@
+using System.Collections.Immutable;
+
+using CheatEngine.SDK.Engine.Values;
+
+namespace CheatEngine.Client.Memory;
+
+/// A copied byte sequence to write to target memory.
+public readonly record struct MemoryBytesWriteRequest
+{
+ /// Creates a byte write and copies the caller's data immediately.
+ public MemoryBytesWriteRequest(Address address, ReadOnlySpan bytes)
+ {
+ if (bytes.IsEmpty)
+ {
+ throw new ArgumentException("At least one byte is required.", nameof(bytes));
+ }
+
+ Address = address;
+ Bytes = ImmutableArray.Create(bytes.ToArray());
+ }
+
+ /// Gets the first target address.
+ public Address Address
+ {
+ get;
+ }
+
+ /// Gets the independent immutable bytes.
+ public ImmutableArray Bytes
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Memory/MemoryReadRequest.cs b/libs/CheatEngine.Client.Abstractions/Memory/MemoryReadRequest.cs
new file mode 100644
index 0000000..23d0d7b
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Memory/MemoryReadRequest.cs
@@ -0,0 +1,27 @@
+using CheatEngine.SDK.Engine.Values;
+
+namespace CheatEngine.Client.Memory;
+
+/// An immutable typed target-memory read request.
+public readonly record struct MemoryReadRequest
+{
+ /// Creates a memory read request.
+ public MemoryReadRequest(Address address, IMemoryCodec codec)
+ {
+ ArgumentNullException.ThrowIfNull(codec);
+ Address = address;
+ Codec = codec;
+ }
+
+ /// Gets the target address to read.
+ public Address Address
+ {
+ get;
+ }
+
+ /// Gets the codec used to translate the value.
+ public IMemoryCodec Codec
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Memory/MemoryStringReadRequest.cs b/libs/CheatEngine.Client.Abstractions/Memory/MemoryStringReadRequest.cs
new file mode 100644
index 0000000..4d9b66f
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Memory/MemoryStringReadRequest.cs
@@ -0,0 +1,34 @@
+using CheatEngine.SDK.Engine.Values;
+
+namespace CheatEngine.Client.Memory;
+
+/// A bounded target-string read.
+public readonly record struct MemoryStringReadRequest
+{
+ /// Creates a bounded text read.
+ public MemoryStringReadRequest(Address address, int maximumLength, bool wideCharacter = false)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maximumLength);
+ Address = address;
+ MaximumLength = maximumLength;
+ WideCharacter = wideCharacter;
+ }
+
+ /// Gets the first target address.
+ public Address Address
+ {
+ get;
+ }
+
+ /// Gets the maximum character count passed to Cheat Engine.
+ public int MaximumLength
+ {
+ get;
+ }
+
+ /// Gets whether Cheat Engine should interpret the target as UTF-16 text.
+ public bool WideCharacter
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Memory/MemoryStringWriteRequest.cs b/libs/CheatEngine.Client.Abstractions/Memory/MemoryStringWriteRequest.cs
new file mode 100644
index 0000000..c7240a9
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Memory/MemoryStringWriteRequest.cs
@@ -0,0 +1,34 @@
+using CheatEngine.SDK.Engine.Values;
+
+namespace CheatEngine.Client.Memory;
+
+/// A copied string to write to target memory.
+public readonly record struct MemoryStringWriteRequest
+{
+ /// Creates a text write.
+ public MemoryStringWriteRequest(Address address, string value, bool wideCharacter = false)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+ Address = address;
+ Value = value;
+ WideCharacter = wideCharacter;
+ }
+
+ /// Gets the first target address.
+ public Address Address
+ {
+ get;
+ }
+
+ /// Gets the managed text copied by the request.
+ public string Value
+ {
+ get;
+ }
+
+ /// Gets whether Cheat Engine should write UTF-16 target text.
+ public bool WideCharacter
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Memory/MemoryWriteRequest.cs b/libs/CheatEngine.Client.Abstractions/Memory/MemoryWriteRequest.cs
new file mode 100644
index 0000000..9e91b3c
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Memory/MemoryWriteRequest.cs
@@ -0,0 +1,34 @@
+using CheatEngine.SDK.Engine.Values;
+
+namespace CheatEngine.Client.Memory;
+
+/// An immutable typed target-memory write request.
+public readonly record struct MemoryWriteRequest
+{
+ /// Creates a memory write request.
+ public MemoryWriteRequest(Address address, T value, IMemoryCodec codec)
+ {
+ ArgumentNullException.ThrowIfNull(codec);
+ Address = address;
+ Value = value;
+ Codec = codec;
+ }
+
+ /// Gets the target address to write.
+ public Address Address
+ {
+ get;
+ }
+
+ /// Gets the value to write.
+ public T Value
+ {
+ get;
+ }
+
+ /// Gets the codec used to translate the value.
+ public IMemoryCodec Codec
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Memory/PointerChainRequest.cs b/libs/CheatEngine.Client.Abstractions/Memory/PointerChainRequest.cs
new file mode 100644
index 0000000..a7d2007
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Memory/PointerChainRequest.cs
@@ -0,0 +1,38 @@
+using System.Collections.Immutable;
+
+using CheatEngine.SDK.Engine.Values;
+
+namespace CheatEngine.Client.Memory;
+
+/// A finite pointer chain whose offsets are applied after each target-aware pointer dereference.
+public readonly record struct PointerChainRequest
+{
+ /// Creates a bounded pointer chain by copying its offsets.
+ public PointerChainRequest(Address baseAddress, ReadOnlySpan offsets)
+ {
+ if (offsets.IsEmpty)
+ {
+ throw new ArgumentException("A pointer chain requires at least one offset.", nameof(offsets));
+ }
+
+ if (offsets.Length > 64)
+ {
+ throw new ArgumentOutOfRangeException(nameof(offsets), "A pointer chain is limited to 64 hops.");
+ }
+
+ BaseAddress = baseAddress;
+ Offsets = ImmutableArray.Create(offsets.ToArray());
+ }
+
+ /// Gets the address containing the first target pointer.
+ public Address BaseAddress
+ {
+ get;
+ }
+
+ /// Gets the finite offsets, one per dereference.
+ public ImmutableArray Offsets
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Modules/ICheatEngineClientModule.cs b/libs/CheatEngine.Client.Abstractions/Modules/ICheatEngineClientModule.cs
new file mode 100644
index 0000000..6954ab9
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Modules/ICheatEngineClientModule.cs
@@ -0,0 +1,11 @@
+namespace CheatEngine.Client.Modules;
+
+/// A scoped feature that participates in the Cheat Engine client lifecycle.
+public interface ICheatEngineClientModule
+{
+ /// Runs after the client scope and Lua runtime are active.
+ public void OnEnabled(ICheatEngineClient client);
+
+ /// Runs before the client scope is disposed and the Lua runtime is detached.
+ public void OnDisabling(ICheatEngineClient client);
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Processes/IProcessClient.cs b/libs/CheatEngine.Client.Abstractions/Processes/IProcessClient.cs
new file mode 100644
index 0000000..efa5018
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Processes/IProcessClient.cs
@@ -0,0 +1,40 @@
+using CheatEngine.Client.Results;
+using CheatEngine.SDK.Engine.Inspection;
+
+namespace CheatEngine.Client.Processes;
+
+/// Reads the process selected by the active Cheat Engine session.
+public interface IProcessClient
+{
+ /// Tries to get a copied snapshot of the currently selected target process.
+ public bool TryGetCurrent(out ProcessSnapshot snapshot, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Gets the selected process or throws when no target is attached.
+ public ProcessSnapshot GetCurrent(CancellationToken cancellationToken = default);
+
+ ///
+ /// Re-reads Cheat Engine's selected target and advances the selection epoch when its PID or observed
+ /// architecture changed.
+ ///
+ public bool TryRefresh(out ProcessSnapshot snapshot, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Refreshes the selected target or throws when no target is attached.
+ public ProcessSnapshot Refresh(CancellationToken cancellationToken = default);
+
+ /// Tries to attach Cheat Engine to an explicit process identifier and verifies the selected target.
+ public bool TryAttach(TargetProcessId processId, out ProcessSnapshot snapshot,
+ out CheatEngineFailure failure, CancellationToken cancellationToken = default);
+
+ /// Attaches to an explicit process identifier or throws when the host rejects it.
+ public ProcessSnapshot Attach(TargetProcessId processId,
+ CancellationToken cancellationToken = default);
+
+ /// Tries to attach to the single local process whose executable name matches exactly.
+ public bool TryAttachExactName(string processName, out ProcessSnapshot snapshot, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Attaches to the single exact-name match or throws when none or several exist.
+ public ProcessSnapshot AttachExactName(string processName, CancellationToken cancellationToken = default);
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Processes/ProcessSnapshot.cs b/libs/CheatEngine.Client.Abstractions/Processes/ProcessSnapshot.cs
new file mode 100644
index 0000000..c5bab29
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Processes/ProcessSnapshot.cs
@@ -0,0 +1,77 @@
+using CheatEngine.SDK.Engine.Inspection;
+using CheatEngine.SDK.Engine.Runtime;
+
+namespace CheatEngine.Client.Processes;
+
+/// An immutable snapshot of the process currently selected in Cheat Engine.
+public readonly record struct ProcessSnapshot
+{
+ /// Creates a selected-process snapshot without a target-architecture observation.
+ public ProcessSnapshot(TargetProcessId id, string? name, string? executablePath)
+ : this(id, name, executablePath, CheatEngineArchitecture.Unknown, 0)
+ {
+ }
+
+ /// Creates a selected-process snapshot from copied host observations.
+ public ProcessSnapshot(
+ TargetProcessId id,
+ string? name,
+ string? executablePath,
+ CheatEngineArchitecture targetArchitecture,
+ long selectionEpoch)
+ {
+ 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));
+ }
+
+ ArgumentOutOfRangeException.ThrowIfNegative(selectionEpoch);
+
+ Id = id;
+ Name = name;
+ ExecutablePath = executablePath;
+ TargetArchitecture = targetArchitecture;
+ SelectionEpoch = selectionEpoch;
+ }
+
+ /// Gets the selected process identifier.
+ public TargetProcessId Id
+ {
+ get;
+ }
+
+ /// Gets the process display name when the host supplied one.
+ public string? Name
+ {
+ get;
+ }
+
+ /// Gets the executable path when the host supplied one.
+ public string? ExecutablePath
+ {
+ get;
+ }
+
+ /// Gets the target architecture observed by Cheat Engine, or unknown when no probe established it.
+ public CheatEngineArchitecture TargetArchitecture
+ {
+ get;
+ }
+
+ /// Gets the pointer width implied by , or unknown.
+ public PointerSize TargetPointerSize => PointerSize.FromArchitecture(TargetArchitecture);
+
+ ///
+ /// Gets the target-selection epoch. A change means target-bound sessions and leases captured for an earlier
+ /// selection are no longer valid.
+ ///
+ public long SelectionEpoch
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/PublicAPI.Shipped.txt b/libs/CheatEngine.Client.Abstractions/PublicAPI.Shipped.txt
new file mode 100644
index 0000000..f755b96
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/PublicAPI.Shipped.txt
@@ -0,0 +1,680 @@
+#nullable enable
+~override CheatEngine.Client.Inspection.InspectionCollectionRequest.Equals(object obj) -> bool
+~override CheatEngine.Client.Inspection.InspectionCollectionRequest.ToString() -> string
+~override CheatEngine.Client.Inspection.SymbolRegistration.Equals(object obj) -> bool
+~override CheatEngine.Client.Inspection.SymbolRegistration.ToString() -> string
+~override CheatEngine.Client.Lua.LuaScript.Equals(object obj) -> bool
+~override CheatEngine.Client.Lua.LuaScript.ToString() -> string
+~override CheatEngine.Client.Memory.MemoryBytesReadRequest.Equals(object obj) -> bool
+~override CheatEngine.Client.Memory.MemoryBytesReadRequest.ToString() -> string
+~override CheatEngine.Client.Memory.MemoryBytesWriteRequest.Equals(object obj) -> bool
+~override CheatEngine.Client.Memory.MemoryBytesWriteRequest.ToString() -> string
+~override CheatEngine.Client.Memory.MemoryReadRequest.Equals(object obj) -> bool
+~override CheatEngine.Client.Memory.MemoryReadRequest.ToString() -> string
+~override CheatEngine.Client.Memory.MemoryStringReadRequest.Equals(object obj) -> bool
+~override CheatEngine.Client.Memory.MemoryStringReadRequest.ToString() -> string
+~override CheatEngine.Client.Memory.MemoryStringWriteRequest.Equals(object obj) -> bool
+~override CheatEngine.Client.Memory.MemoryStringWriteRequest.ToString() -> string
+~override CheatEngine.Client.Memory.MemoryWriteRequest.Equals(object obj) -> bool
+~override CheatEngine.Client.Memory.MemoryWriteRequest.ToString() -> string
+~override CheatEngine.Client.Memory.PointerChainRequest.Equals(object obj) -> bool
+~override CheatEngine.Client.Memory.PointerChainRequest.ToString() -> string
+~override CheatEngine.Client.Processes.ProcessSnapshot.Equals(object obj) -> bool
+~override CheatEngine.Client.Processes.ProcessSnapshot.ToString() -> string
+~override CheatEngine.Client.Results.CheatEngineFailure.Equals(object obj) -> bool
+~override CheatEngine.Client.Results.CheatEngineFailure.ToString() -> string
+~override CheatEngine.Client.Runtime.CheatEngineRuntimePlatformInfo.Equals(object obj) -> bool
+~override CheatEngine.Client.Runtime.CheatEngineRuntimePlatformInfo.ToString() -> string
+~override CheatEngine.Client.Runtime.CheatEngineRuntimeSnapshot.Equals(object obj) -> bool
+~override CheatEngine.Client.Runtime.CheatEngineRuntimeSnapshot.ToString() -> string
+~override CheatEngine.Client.Runtime.CheatEngineRuntimeVersionInfo.Equals(object obj) -> bool
+~override CheatEngine.Client.Runtime.CheatEngineRuntimeVersionInfo.ToString() -> string
+~override CheatEngine.Client.Runtime.ClientCapabilityAvailability.Equals(object obj) -> bool
+~override CheatEngine.Client.Runtime.ClientCapabilityAvailability.ToString() -> string
+~override CheatEngine.Client.Scanning.AobPattern.Equals(object obj) -> bool
+~override CheatEngine.Client.Scanning.AobScanRange.Equals(object obj) -> bool
+~override CheatEngine.Client.Scanning.AobScanRange.ToString() -> string
+~override CheatEngine.Client.Scanning.AobScanRequest.Equals(object obj) -> bool
+~override CheatEngine.Client.Scanning.AobScanRequest.ToString() -> string
+~override CheatEngine.Client.Scanning.AobScanResult.Equals(object obj) -> bool
+~override CheatEngine.Client.Scanning.AobScanResult.ToString() -> string
+~override CheatEngine.Client.Scanning.ValueScanMatch.Equals(object obj) -> bool
+~override CheatEngine.Client.Scanning.ValueScanMatch.ToString() -> string
+~override CheatEngine.Client.Scanning.ValueScanPage.Equals(object obj) -> bool
+~override CheatEngine.Client.Scanning.ValueScanPage.ToString() -> string
+~override CheatEngine.Client.Scanning.ValueScanReadRequest.Equals(object obj) -> bool
+~override CheatEngine.Client.Scanning.ValueScanReadRequest.ToString() -> string
+~override CheatEngine.Client.Tables.AddressTableSnapshot.Equals(object obj) -> bool
+~override CheatEngine.Client.Tables.AddressTableSnapshot.ToString() -> string
+~override CheatEngine.Client.Tables.MemoryRecordCollectionRequest.Equals(object obj) -> bool
+~override CheatEngine.Client.Tables.MemoryRecordCollectionRequest.ToString() -> string
+~override CheatEngine.Client.Tables.MemoryRecordDefinition.Equals(object obj) -> bool
+~override CheatEngine.Client.Tables.MemoryRecordDefinition.ToString() -> string
+~override CheatEngine.Client.Tables.MemoryRecordHierarchyRequest.Equals(object obj) -> bool
+~override CheatEngine.Client.Tables.MemoryRecordHierarchyRequest.ToString() -> string
+~override CheatEngine.Client.Tables.MemoryRecordHierarchySnapshot.Equals(object obj) -> bool
+~override CheatEngine.Client.Tables.MemoryRecordHierarchySnapshot.ToString() -> string
+~override CheatEngine.Client.Tables.MemoryRecordSearch.Equals(object obj) -> bool
+~override CheatEngine.Client.Tables.MemoryRecordSearch.ToString() -> string
+~override CheatEngine.Client.Tables.MemoryRecordContentSnapshot.Equals(object obj) -> bool
+~override CheatEngine.Client.Tables.MemoryRecordContentSnapshot.ToString() -> string
+~override CheatEngine.Client.Tables.MemoryRecordSnapshot.Equals(object obj) -> bool
+~override CheatEngine.Client.Tables.MemoryRecordSnapshot.ToString() -> string
+~override CheatEngine.Client.Tables.MemoryRecordStateSnapshot.Equals(object obj) -> bool
+~override CheatEngine.Client.Tables.MemoryRecordStateSnapshot.ToString() -> string
+~override CheatEngine.Client.Tables.MemoryRecordUpdate.Equals(object obj) -> bool
+~override CheatEngine.Client.Tables.MemoryRecordUpdate.ToString() -> string
+~override CheatEngine.Client.Tables.TableLoadRequest.Equals(object obj) -> bool
+~override CheatEngine.Client.Tables.TableLoadRequest.ToString() -> string
+~override CheatEngine.Client.Tables.TableSaveRequest.Equals(object obj) -> bool
+~override CheatEngine.Client.Tables.TableSaveRequest.ToString() -> string
+~override CheatEngine.Client.Tables.TrustedTableFile.Equals(object obj) -> bool
+~override CheatEngine.Client.Tables.TrustedTableFile.ToString() -> string
+CheatEngine.Client.Dispatching.ICheatEngineDispatcher
+CheatEngine.Client.Dispatching.ICheatEngineDispatcher.Invoke(System.Action! callback, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> void
+CheatEngine.Client.Dispatching.ICheatEngineDispatcher.Invoke(System.Func! callback, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> T
+CheatEngine.Client.Dispatching.ICheatEngineDispatcher.IsMainThread.get -> bool
+CheatEngine.Client.Dispatching.ICheatEngineDispatcher.TryInvoke(System.Action! callback, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Dispatching.ICheatEngineDispatcher.TryInvoke(System.Func! callback, out T result, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.ICheatEngineClient
+CheatEngine.Client.ICheatEngineClient.Dispatcher.get -> CheatEngine.Client.Dispatching.ICheatEngineDispatcher!
+CheatEngine.Client.ICheatEngineClient.Epoch.get -> long
+CheatEngine.Client.ICheatEngineClient.Inspection.get -> CheatEngine.Client.Inspection.IInspectionClient!
+CheatEngine.Client.ICheatEngineClient.Lua.get -> CheatEngine.Client.Lua.ILuaClient!
+CheatEngine.Client.ICheatEngineClient.Memory.get -> CheatEngine.Client.Memory.IMemoryClient!
+CheatEngine.Client.ICheatEngineClient.Patterns.get -> CheatEngine.Client.Scanning.IPatternScanner!
+CheatEngine.Client.ICheatEngineClient.Processes.get -> CheatEngine.Client.Processes.IProcessClient!
+CheatEngine.Client.ICheatEngineClient.Runtime.get -> CheatEngine.Client.Runtime.ICheatEngineRuntime!
+CheatEngine.Client.ICheatEngineClient.Scans.get -> CheatEngine.Client.Scanning.IValueScanner!
+CheatEngine.Client.ICheatEngineClient.Stopping.get -> System.Threading.CancellationToken
+CheatEngine.Client.ICheatEngineClient.Tables.get -> CheatEngine.Client.Tables.ITableClient!
+CheatEngine.Client.Inspection.IInspectionClient
+CheatEngine.Client.Inspection.IInspectionClient.GetMemoryRegion(CheatEngine.SDK.Engine.Values.Address address, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.SDK.Engine.Inspection.MemoryRegionInfo
+CheatEngine.Client.Inspection.IInspectionClient.GetMemoryRegions(CheatEngine.Client.Inspection.InspectionCollectionRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Collections.Immutable.ImmutableArray
+CheatEngine.Client.Inspection.IInspectionClient.GetModules(CheatEngine.Client.Inspection.InspectionCollectionRequest request, CheatEngine.SDK.Engine.Inspection.TargetProcessId? processId = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Collections.Immutable.ImmutableArray
+CheatEngine.Client.Inspection.IInspectionClient.GetModuleSections(CheatEngine.SDK.Engine.Inspection.ModuleName moduleName, CheatEngine.Client.Inspection.InspectionCollectionRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Collections.Immutable.ImmutableArray
+CheatEngine.Client.Inspection.IInspectionClient.GetSymbol(CheatEngine.SDK.Engine.Inspection.SymbolExpression expression, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.SDK.Engine.Inspection.SymbolInfo
+CheatEngine.Client.Inspection.IInspectionClient.RegisterSymbol(CheatEngine.Client.Inspection.SymbolRegistration registration, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Inspection.ISymbolRegistrationLease!
+CheatEngine.Client.Inspection.IInspectionClient.ResolveAddress(CheatEngine.SDK.Engine.Inspection.SymbolExpression expression, CheatEngine.SDK.Engine.Inspection.AddressResolutionOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.SDK.Engine.Values.Address
+CheatEngine.Client.Inspection.IInspectionClient.ResolveName(CheatEngine.SDK.Engine.Values.Address address, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> string!
+CheatEngine.Client.Inspection.IInspectionClient.TryGetMemoryRegion(CheatEngine.SDK.Engine.Values.Address address, out CheatEngine.SDK.Engine.Inspection.MemoryRegionInfo region, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Inspection.IInspectionClient.TryGetMemoryRegions(CheatEngine.Client.Inspection.InspectionCollectionRequest request, out System.Collections.Immutable.ImmutableArray regions, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Inspection.IInspectionClient.TryGetModules(CheatEngine.Client.Inspection.InspectionCollectionRequest request, out System.Collections.Immutable.ImmutableArray modules, out CheatEngine.Client.Results.CheatEngineFailure failure, CheatEngine.SDK.Engine.Inspection.TargetProcessId? processId = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Inspection.IInspectionClient.TryGetModuleSections(CheatEngine.SDK.Engine.Inspection.ModuleName moduleName, CheatEngine.Client.Inspection.InspectionCollectionRequest request, out System.Collections.Immutable.ImmutableArray sections, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Inspection.IInspectionClient.TryGetSymbol(CheatEngine.SDK.Engine.Inspection.SymbolExpression expression, out CheatEngine.SDK.Engine.Inspection.SymbolInfo symbol, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Inspection.IInspectionClient.TryRegisterSymbol(CheatEngine.Client.Inspection.SymbolRegistration registration, out CheatEngine.Client.Inspection.ISymbolRegistrationLease? lease, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Inspection.IInspectionClient.TryResolveAddress(CheatEngine.SDK.Engine.Inspection.SymbolExpression expression, CheatEngine.SDK.Engine.Inspection.AddressResolutionOptions options, out CheatEngine.SDK.Engine.Values.Address address, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Inspection.IInspectionClient.TryResolveName(CheatEngine.SDK.Engine.Values.Address address, out string? name, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Inspection.InspectionCollectionRequest
+CheatEngine.Client.Inspection.InspectionCollectionRequest.Equals(CheatEngine.Client.Inspection.InspectionCollectionRequest other) -> bool
+CheatEngine.Client.Inspection.InspectionCollectionRequest.InspectionCollectionRequest() -> void
+CheatEngine.Client.Inspection.InspectionCollectionRequest.InspectionCollectionRequest(int maximumItems) -> void
+CheatEngine.Client.Inspection.InspectionCollectionRequest.MaximumItems.get -> int
+CheatEngine.Client.Inspection.ISymbolRegistrationLease
+CheatEngine.Client.Inspection.ISymbolRegistrationLease.Address.get -> CheatEngine.SDK.Engine.Values.Address
+CheatEngine.Client.Inspection.ISymbolRegistrationLease.IsReleased.get -> bool
+CheatEngine.Client.Inspection.ISymbolRegistrationLease.Name.get -> string!
+CheatEngine.Client.Inspection.SymbolRegistration
+CheatEngine.Client.Inspection.SymbolRegistration.Address.get -> CheatEngine.SDK.Engine.Values.Address
+CheatEngine.Client.Inspection.SymbolRegistration.DoNotSave.get -> bool
+CheatEngine.Client.Inspection.SymbolRegistration.Equals(CheatEngine.Client.Inspection.SymbolRegistration other) -> bool
+CheatEngine.Client.Inspection.SymbolRegistration.Name.get -> string!
+CheatEngine.Client.Inspection.SymbolRegistration.SymbolRegistration() -> void
+CheatEngine.Client.Inspection.SymbolRegistration.SymbolRegistration(string! name, CheatEngine.SDK.Engine.Values.Address address, bool doNotSave = true) -> void
+CheatEngine.Client.Lua.ILuaClient
+CheatEngine.Client.Lua.ILuaClient.Execute(CheatEngine.Client.Lua.ILuaOperation! operation, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> TResult
+CheatEngine.Client.Lua.ILuaClient.RegisterModule(CheatEngine.Client.Lua.ILuaModule! luaModule, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Lua.ILuaModuleLease!
+CheatEngine.Client.Lua.ILuaClient.TryExecute(CheatEngine.Client.Lua.ILuaOperation! operation, out TResult result, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Lua.ILuaClient.TryRegisterModule(CheatEngine.Client.Lua.ILuaModule! luaModule, out CheatEngine.Client.Lua.ILuaModuleLease? lease, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Lua.ILuaExecutionContext
+CheatEngine.Client.Lua.ILuaExecutionContext.Epoch.get -> long
+CheatEngine.Client.Lua.ILuaExecutionContext.IsActive.get -> bool
+CheatEngine.Client.Lua.ILuaExecutionContext.ThrowIfExpired() -> void
+CheatEngine.Client.Lua.ILuaModule
+CheatEngine.Client.Lua.ILuaModule.Register() -> void
+CheatEngine.Client.Lua.ILuaModule.Unregister() -> void
+CheatEngine.Client.Lua.ILuaModuleLease
+CheatEngine.Client.Lua.ILuaModuleLease.Epoch.get -> long
+CheatEngine.Client.Lua.ILuaModuleLease.IsReleased.get -> bool
+CheatEngine.Client.Lua.ILuaOperation
+CheatEngine.Client.Lua.ILuaOperation.TryExecute(CheatEngine.Client.Lua.ILuaExecutionContext! context, out TResult result, out CheatEngine.Client.Results.CheatEngineFailure failure) -> bool
+CheatEngine.Client.Lua.IUnsafeLuaClient
+CheatEngine.Client.Lua.IUnsafeLuaClient.Execute(CheatEngine.Client.Lua.LuaScript script, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> void
+CheatEngine.Client.Lua.IUnsafeLuaClient.TryExecute(CheatEngine.Client.Lua.LuaScript script, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Lua.LuaScript
+CheatEngine.Client.Lua.LuaScript.ChunkName.get -> string?
+CheatEngine.Client.Lua.LuaScript.Equals(CheatEngine.Client.Lua.LuaScript other) -> bool
+CheatEngine.Client.Lua.LuaScript.LuaScript() -> void
+CheatEngine.Client.Lua.LuaScript.LuaScript(string! source, string? chunkName = null) -> void
+CheatEngine.Client.Lua.LuaScript.Source.get -> string!
+CheatEngine.Client.Memory.IMemoryClient
+CheatEngine.Client.Memory.IMemoryClient.Read(CheatEngine.Client.Memory.MemoryReadRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> T
+CheatEngine.Client.Memory.IMemoryClient.ReadBytes(CheatEngine.Client.Memory.MemoryBytesReadRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Collections.Immutable.ImmutableArray
+CheatEngine.Client.Memory.IMemoryClient.ReadPrimitive(CheatEngine.SDK.Engine.Values.Address address, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> T
+CheatEngine.Client.Memory.IMemoryClient.ReadString(CheatEngine.Client.Memory.MemoryStringReadRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> string!
+CheatEngine.Client.Memory.IMemoryClient.ResolvePointerChain(CheatEngine.Client.Memory.PointerChainRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.SDK.Engine.Values.Address
+CheatEngine.Client.Memory.IMemoryClient.TryRead(CheatEngine.Client.Memory.MemoryReadRequest request, out T value, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Memory.IMemoryClient.TryReadBytes(CheatEngine.Client.Memory.MemoryBytesReadRequest request, out System.Collections.Immutable.ImmutableArray bytes, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Memory.IMemoryClient.TryReadPrimitive(CheatEngine.SDK.Engine.Values.Address address, out T value, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Memory.IMemoryClient.TryReadString(CheatEngine.Client.Memory.MemoryStringReadRequest request, out string? value, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Memory.IMemoryClient.TryResolvePointerChain(CheatEngine.Client.Memory.PointerChainRequest request, out CheatEngine.SDK.Engine.Values.Address address, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Memory.IMemoryClient.TryWrite(CheatEngine.Client.Memory.MemoryWriteRequest request, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Memory.IMemoryClient.TryWriteBytes(CheatEngine.Client.Memory.MemoryBytesWriteRequest request, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Memory.IMemoryClient.TryWritePrimitive(CheatEngine.SDK.Engine.Values.Address address, T value, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Memory.IMemoryClient.TryWriteString(CheatEngine.Client.Memory.MemoryStringWriteRequest request, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Memory.IMemoryClient.Write(CheatEngine.Client.Memory.MemoryWriteRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> void
+CheatEngine.Client.Memory.IMemoryClient.WriteBytes(CheatEngine.Client.Memory.MemoryBytesWriteRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> void
+CheatEngine.Client.Memory.IMemoryClient.WritePrimitive(CheatEngine.SDK.Engine.Values.Address address, T value, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> void
+CheatEngine.Client.Memory.IMemoryClient.WriteString(CheatEngine.Client.Memory.MemoryStringWriteRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> void
+CheatEngine.Client.Memory.IMemoryCodec
+CheatEngine.Client.Memory.IMemoryCodec.TryRead(CheatEngine.Client.Memory.IMemoryReadContext! context, CheatEngine.SDK.Engine.Values.Address address, out T value) -> bool
+CheatEngine.Client.Memory.IMemoryCodec.TryWrite(CheatEngine.Client.Memory.IMemoryWriteContext! context, CheatEngine.SDK.Engine.Values.Address address, in T value) -> bool
+CheatEngine.Client.Memory.IMemoryReadContext
+CheatEngine.Client.Memory.IMemoryReadContext.PointerSize.get -> int
+CheatEngine.Client.Memory.IMemoryReadContext.TryReadBytes(CheatEngine.SDK.Engine.Values.Address address, System.Span destination) -> bool
+CheatEngine.Client.Memory.IMemoryWriteContext
+CheatEngine.Client.Memory.IMemoryWriteContext.PointerSize.get -> int
+CheatEngine.Client.Memory.IMemoryWriteContext.TryWriteBytes(CheatEngine.SDK.Engine.Values.Address address, System.ReadOnlySpan source) -> bool
+CheatEngine.Client.Memory.MemoryBytesReadRequest
+CheatEngine.Client.Memory.MemoryBytesReadRequest.Address.get -> CheatEngine.SDK.Engine.Values.Address
+CheatEngine.Client.Memory.MemoryBytesReadRequest.Equals(CheatEngine.Client.Memory.MemoryBytesReadRequest other) -> bool
+CheatEngine.Client.Memory.MemoryBytesReadRequest.Length.get -> int
+CheatEngine.Client.Memory.MemoryBytesReadRequest.MemoryBytesReadRequest() -> void
+CheatEngine.Client.Memory.MemoryBytesReadRequest.MemoryBytesReadRequest(CheatEngine.SDK.Engine.Values.Address address, int length) -> void
+CheatEngine.Client.Memory.MemoryBytesWriteRequest
+CheatEngine.Client.Memory.MemoryBytesWriteRequest.Address.get -> CheatEngine.SDK.Engine.Values.Address
+CheatEngine.Client.Memory.MemoryBytesWriteRequest.Bytes.get -> System.Collections.Immutable.ImmutableArray
+CheatEngine.Client.Memory.MemoryBytesWriteRequest.Equals(CheatEngine.Client.Memory.MemoryBytesWriteRequest other) -> bool
+CheatEngine.Client.Memory.MemoryBytesWriteRequest.MemoryBytesWriteRequest() -> void
+CheatEngine.Client.Memory.MemoryBytesWriteRequest.MemoryBytesWriteRequest(CheatEngine.SDK.Engine.Values.Address address, System.ReadOnlySpan bytes) -> void
+CheatEngine.Client.Memory.MemoryReadRequest
+CheatEngine.Client.Memory.MemoryReadRequest.Address.get -> CheatEngine.SDK.Engine.Values.Address
+CheatEngine.Client.Memory.MemoryReadRequest.Codec.get -> CheatEngine.Client.Memory.IMemoryCodec!
+CheatEngine.Client.Memory.MemoryReadRequest.Equals(CheatEngine.Client.Memory.MemoryReadRequest other) -> bool
+CheatEngine.Client.Memory.MemoryReadRequest.MemoryReadRequest() -> void
+CheatEngine.Client.Memory.MemoryReadRequest.MemoryReadRequest(CheatEngine.SDK.Engine.Values.Address address, CheatEngine.Client.Memory.IMemoryCodec! codec) -> void
+CheatEngine.Client.Memory.MemoryStringReadRequest
+CheatEngine.Client.Memory.MemoryStringReadRequest.Address.get -> CheatEngine.SDK.Engine.Values.Address
+CheatEngine.Client.Memory.MemoryStringReadRequest.Equals(CheatEngine.Client.Memory.MemoryStringReadRequest other) -> bool
+CheatEngine.Client.Memory.MemoryStringReadRequest.MaximumLength.get -> int
+CheatEngine.Client.Memory.MemoryStringReadRequest.MemoryStringReadRequest() -> void
+CheatEngine.Client.Memory.MemoryStringReadRequest.MemoryStringReadRequest(CheatEngine.SDK.Engine.Values.Address address, int maximumLength, bool wideCharacter = false) -> void
+CheatEngine.Client.Memory.MemoryStringReadRequest.WideCharacter.get -> bool
+CheatEngine.Client.Memory.MemoryStringWriteRequest
+CheatEngine.Client.Memory.MemoryStringWriteRequest.Address.get -> CheatEngine.SDK.Engine.Values.Address
+CheatEngine.Client.Memory.MemoryStringWriteRequest.Equals(CheatEngine.Client.Memory.MemoryStringWriteRequest other) -> bool
+CheatEngine.Client.Memory.MemoryStringWriteRequest.MemoryStringWriteRequest() -> void
+CheatEngine.Client.Memory.MemoryStringWriteRequest.MemoryStringWriteRequest(CheatEngine.SDK.Engine.Values.Address address, string! value, bool wideCharacter = false) -> void
+CheatEngine.Client.Memory.MemoryStringWriteRequest.Value.get -> string!
+CheatEngine.Client.Memory.MemoryStringWriteRequest.WideCharacter.get -> bool
+CheatEngine.Client.Memory.MemoryWriteRequest
+CheatEngine.Client.Memory.MemoryWriteRequest.Address.get -> CheatEngine.SDK.Engine.Values.Address
+CheatEngine.Client.Memory.MemoryWriteRequest.Codec.get -> CheatEngine.Client.Memory.IMemoryCodec!
+CheatEngine.Client.Memory.MemoryWriteRequest.Equals(CheatEngine.Client.Memory.MemoryWriteRequest other) -> bool
+CheatEngine.Client.Memory.MemoryWriteRequest.MemoryWriteRequest() -> void
+CheatEngine.Client.Memory.MemoryWriteRequest.MemoryWriteRequest(CheatEngine.SDK.Engine.Values.Address address, T value, CheatEngine.Client.Memory.IMemoryCodec! codec) -> void
+CheatEngine.Client.Memory.MemoryWriteRequest.Value.get -> T
+CheatEngine.Client.Memory.PointerChainRequest
+CheatEngine.Client.Memory.PointerChainRequest.BaseAddress.get -> CheatEngine.SDK.Engine.Values.Address
+CheatEngine.Client.Memory.PointerChainRequest.Equals(CheatEngine.Client.Memory.PointerChainRequest other) -> bool
+CheatEngine.Client.Memory.PointerChainRequest.Offsets.get -> System.Collections.Immutable.ImmutableArray
+CheatEngine.Client.Memory.PointerChainRequest.PointerChainRequest() -> void
+CheatEngine.Client.Memory.PointerChainRequest.PointerChainRequest(CheatEngine.SDK.Engine.Values.Address baseAddress, System.ReadOnlySpan offsets) -> void
+CheatEngine.Client.Modules.ICheatEngineClientModule
+CheatEngine.Client.Modules.ICheatEngineClientModule.OnDisabling(CheatEngine.Client.ICheatEngineClient! client) -> void
+CheatEngine.Client.Modules.ICheatEngineClientModule.OnEnabled(CheatEngine.Client.ICheatEngineClient! client) -> void
+CheatEngine.Client.Processes.IProcessClient
+CheatEngine.Client.Processes.IProcessClient.Attach(CheatEngine.SDK.Engine.Inspection.TargetProcessId processId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Processes.ProcessSnapshot
+CheatEngine.Client.Processes.IProcessClient.AttachExactName(string! processName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Processes.ProcessSnapshot
+CheatEngine.Client.Processes.IProcessClient.GetCurrent(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Processes.ProcessSnapshot
+CheatEngine.Client.Processes.IProcessClient.Refresh(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Processes.ProcessSnapshot
+CheatEngine.Client.Processes.IProcessClient.TryAttach(CheatEngine.SDK.Engine.Inspection.TargetProcessId processId, 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.TryAttachExactName(string! processName, 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.TryGetCurrent(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.TryRefresh(out CheatEngine.Client.Processes.ProcessSnapshot snapshot, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Processes.ProcessSnapshot
+CheatEngine.Client.Processes.ProcessSnapshot.Equals(CheatEngine.Client.Processes.ProcessSnapshot other) -> bool
+CheatEngine.Client.Processes.ProcessSnapshot.ExecutablePath.get -> string?
+CheatEngine.Client.Processes.ProcessSnapshot.Id.get -> CheatEngine.SDK.Engine.Inspection.TargetProcessId
+CheatEngine.Client.Processes.ProcessSnapshot.Name.get -> string?
+CheatEngine.Client.Processes.ProcessSnapshot.ProcessSnapshot() -> void
+CheatEngine.Client.Processes.ProcessSnapshot.ProcessSnapshot(CheatEngine.SDK.Engine.Inspection.TargetProcessId id, string? name, string? executablePath, CheatEngine.SDK.Engine.Runtime.CheatEngineArchitecture targetArchitecture, long selectionEpoch) -> void
+CheatEngine.Client.Processes.ProcessSnapshot.ProcessSnapshot(CheatEngine.SDK.Engine.Inspection.TargetProcessId id, string? name, string? executablePath) -> void
+CheatEngine.Client.Processes.ProcessSnapshot.SelectionEpoch.get -> long
+CheatEngine.Client.Processes.ProcessSnapshot.TargetArchitecture.get -> CheatEngine.SDK.Engine.Runtime.CheatEngineArchitecture
+CheatEngine.Client.Processes.ProcessSnapshot.TargetPointerSize.get -> CheatEngine.SDK.Engine.Runtime.PointerSize
+CheatEngine.Client.Results.CheatEngineActivationExpiredException
+CheatEngine.Client.Results.CheatEngineActivationExpiredException.CheatEngineActivationExpiredException(string! operation, string! message, System.Exception? innerException = null) -> void
+CheatEngine.Client.Results.CheatEngineClientException
+CheatEngine.Client.Results.CheatEngineClientException.CheatEngineClientException(CheatEngine.Client.Results.CheatEngineFailure failure) -> void
+CheatEngine.Client.Results.CheatEngineClientException.Failure.get -> CheatEngine.Client.Results.CheatEngineFailure
+CheatEngine.Client.Results.CheatEngineClientLifecycleException
+CheatEngine.Client.Results.CheatEngineClientLifecycleException.CheatEngineClientLifecycleException(string! operation, string! message, System.Exception? innerException = null) -> void
+CheatEngine.Client.Results.CheatEngineFailure
+CheatEngine.Client.Results.CheatEngineFailure.CheatEngineFailure() -> void
+CheatEngine.Client.Results.CheatEngineFailure.CheatEngineFailure(CheatEngine.Client.Results.CheatEngineFailureKind kind, string! operation, string! message, System.Exception? exception = null) -> void
+CheatEngine.Client.Results.CheatEngineFailure.Equals(CheatEngine.Client.Results.CheatEngineFailure other) -> bool
+CheatEngine.Client.Results.CheatEngineFailure.Exception.get -> System.Exception?
+CheatEngine.Client.Results.CheatEngineFailure.Kind.get -> CheatEngine.Client.Results.CheatEngineFailureKind
+CheatEngine.Client.Results.CheatEngineFailure.Message.get -> string!
+CheatEngine.Client.Results.CheatEngineFailure.Operation.get -> string!
+CheatEngine.Client.Results.CheatEngineFailure.Throw() -> void
+CheatEngine.Client.Results.CheatEngineFailureKind
+CheatEngine.Client.Results.CheatEngineFailureKind.ActivationExpired = 14 -> CheatEngine.Client.Results.CheatEngineFailureKind
+CheatEngine.Client.Results.CheatEngineFailureKind.AmbiguousMatch = 5 -> CheatEngine.Client.Results.CheatEngineFailureKind
+CheatEngine.Client.Results.CheatEngineFailureKind.BindingError = 8 -> CheatEngine.Client.Results.CheatEngineFailureKind
+CheatEngine.Client.Results.CheatEngineFailureKind.Cancelled = 1 -> CheatEngine.Client.Results.CheatEngineFailureKind
+CheatEngine.Client.Results.CheatEngineFailureKind.CapabilityUnavailable = 2 -> CheatEngine.Client.Results.CheatEngineFailureKind
+CheatEngine.Client.Results.CheatEngineFailureKind.InvalidHostResult = 9 -> CheatEngine.Client.Results.CheatEngineFailureKind
+CheatEngine.Client.Results.CheatEngineFailureKind.InvalidState = 15 -> CheatEngine.Client.Results.CheatEngineFailureKind
+CheatEngine.Client.Results.CheatEngineFailureKind.LuaError = 7 -> CheatEngine.Client.Results.CheatEngineFailureKind
+CheatEngine.Client.Results.CheatEngineFailureKind.MemoryReadFailed = 12 -> CheatEngine.Client.Results.CheatEngineFailureKind
+CheatEngine.Client.Results.CheatEngineFailureKind.MemoryWriteFailed = 13 -> CheatEngine.Client.Results.CheatEngineFailureKind
+CheatEngine.Client.Results.CheatEngineFailureKind.NotFound = 4 -> CheatEngine.Client.Results.CheatEngineFailureKind
+CheatEngine.Client.Results.CheatEngineFailureKind.OperationRejected = 3 -> CheatEngine.Client.Results.CheatEngineFailureKind
+CheatEngine.Client.Results.CheatEngineFailureKind.ResultLimitExceeded = 6 -> CheatEngine.Client.Results.CheatEngineFailureKind
+CheatEngine.Client.Results.CheatEngineFailureKind.TargetNotAttached = 11 -> CheatEngine.Client.Results.CheatEngineFailureKind
+CheatEngine.Client.Results.CheatEngineFailureKind.Unknown = 0 -> CheatEngine.Client.Results.CheatEngineFailureKind
+CheatEngine.Client.Results.CheatEngineFailureKind.Unsupported = 10 -> CheatEngine.Client.Results.CheatEngineFailureKind
+CheatEngine.Client.Results.CheatEngineOperationException
+CheatEngine.Client.Results.CheatEngineOperationException.CheatEngineOperationException(CheatEngine.Client.Results.CheatEngineFailure failure) -> void
+CheatEngine.Client.Runtime.CheatEngineRuntimePlatformInfo
+CheatEngine.Client.Runtime.CheatEngineRuntimePlatformInfo.CheatEngineRuntimePlatformInfo() -> void
+CheatEngine.Client.Runtime.CheatEngineRuntimePlatformInfo.CheatEngineRuntimePlatformInfo(CheatEngine.SDK.Engine.Runtime.CheatEngineArchitecture systemArchitecture, CheatEngine.SDK.Engine.Runtime.CheatEngineArchitecture targetArchitecture, CheatEngine.SDK.Engine.Runtime.PointerSize targetPointerSize, CheatEngine.SDK.Engine.Runtime.TargetAbi targetAbi) -> void
+CheatEngine.Client.Runtime.CheatEngineRuntimePlatformInfo.Equals(CheatEngine.Client.Runtime.CheatEngineRuntimePlatformInfo other) -> bool
+CheatEngine.Client.Runtime.CheatEngineRuntimePlatformInfo.SystemArchitecture.get -> CheatEngine.SDK.Engine.Runtime.CheatEngineArchitecture
+CheatEngine.Client.Runtime.CheatEngineRuntimePlatformInfo.TargetAbi.get -> CheatEngine.SDK.Engine.Runtime.TargetAbi
+CheatEngine.Client.Runtime.CheatEngineRuntimePlatformInfo.TargetArchitecture.get -> CheatEngine.SDK.Engine.Runtime.CheatEngineArchitecture
+CheatEngine.Client.Runtime.CheatEngineRuntimePlatformInfo.TargetPointerSize.get -> CheatEngine.SDK.Engine.Runtime.PointerSize
+CheatEngine.Client.Runtime.CheatEngineRuntimeSnapshot
+CheatEngine.Client.Runtime.CheatEngineRuntimeSnapshot.CheatEngineRuntimeSnapshot() -> void
+CheatEngine.Client.Runtime.CheatEngineRuntimeSnapshot.CheatEngineRuntimeSnapshot(long epoch, CheatEngine.Client.Runtime.CheatEngineRuntimeVersionInfo versionInfo, CheatEngine.Client.Runtime.CheatEngineRuntimePlatformInfo platformInfo, CheatEngine.SDK.Engine.Runtime.RuntimeCapabilities! sdkCapabilities, CheatEngine.Client.Runtime.ClientCapabilities! clientCapabilities) -> void
+CheatEngine.Client.Runtime.CheatEngineRuntimeSnapshot.ClientAssemblyVersion.get -> System.Version!
+CheatEngine.Client.Runtime.CheatEngineRuntimeSnapshot.ClientCapabilities.get -> CheatEngine.Client.Runtime.ClientCapabilities!
+CheatEngine.Client.Runtime.CheatEngineRuntimeSnapshot.Epoch.get -> long
+CheatEngine.Client.Runtime.CheatEngineRuntimeSnapshot.Equals(CheatEngine.Client.Runtime.CheatEngineRuntimeSnapshot other) -> bool
+CheatEngine.Client.Runtime.CheatEngineRuntimeSnapshot.IsOnQualifiedCheatEngineLine.get -> bool
+CheatEngine.Client.Runtime.CheatEngineRuntimeSnapshot.ObservedCheatEngineVersion.get -> double?
+CheatEngine.Client.Runtime.CheatEngineRuntimeSnapshot.QualifiedCheatEngineBaseline.get -> CheatEngine.SDK.Engine.Runtime.CheatEngineVersion
+CheatEngine.Client.Runtime.CheatEngineRuntimeSnapshot.SdkAssemblyVersion.get -> System.Version!
+CheatEngine.Client.Runtime.CheatEngineRuntimeSnapshot.SdkCapabilities.get -> CheatEngine.SDK.Engine.Runtime.RuntimeCapabilities!
+CheatEngine.Client.Runtime.CheatEngineRuntimeSnapshot.SystemArchitecture.get -> CheatEngine.SDK.Engine.Runtime.CheatEngineArchitecture
+CheatEngine.Client.Runtime.CheatEngineRuntimeSnapshot.TargetAbi.get -> CheatEngine.SDK.Engine.Runtime.TargetAbi
+CheatEngine.Client.Runtime.CheatEngineRuntimeSnapshot.TargetArchitecture.get -> CheatEngine.SDK.Engine.Runtime.CheatEngineArchitecture
+CheatEngine.Client.Runtime.CheatEngineRuntimeSnapshot.TargetPointerSize.get -> CheatEngine.SDK.Engine.Runtime.PointerSize
+CheatEngine.Client.Runtime.CheatEngineRuntimeSnapshot.Platform.get -> CheatEngine.Client.Runtime.CheatEngineRuntimePlatformInfo
+CheatEngine.Client.Runtime.CheatEngineRuntimeSnapshot.Version.get -> CheatEngine.Client.Runtime.CheatEngineRuntimeVersionInfo
+CheatEngine.Client.Runtime.CheatEngineRuntimeVersionInfo
+CheatEngine.Client.Runtime.CheatEngineRuntimeVersionInfo.CheatEngineRuntimeVersionInfo() -> void
+CheatEngine.Client.Runtime.CheatEngineRuntimeVersionInfo.CheatEngineRuntimeVersionInfo(double? observedCheatEngineVersion, CheatEngine.SDK.Engine.Runtime.CheatEngineVersion qualifiedCheatEngineBaseline, System.Version! clientAssemblyVersion, System.Version! sdkAssemblyVersion) -> void
+CheatEngine.Client.Runtime.CheatEngineRuntimeVersionInfo.ClientAssemblyVersion.get -> System.Version!
+CheatEngine.Client.Runtime.CheatEngineRuntimeVersionInfo.Equals(CheatEngine.Client.Runtime.CheatEngineRuntimeVersionInfo other) -> bool
+CheatEngine.Client.Runtime.CheatEngineRuntimeVersionInfo.ObservedCheatEngineVersion.get -> double?
+CheatEngine.Client.Runtime.CheatEngineRuntimeVersionInfo.QualifiedCheatEngineBaseline.get -> CheatEngine.SDK.Engine.Runtime.CheatEngineVersion
+CheatEngine.Client.Runtime.CheatEngineRuntimeVersionInfo.SdkAssemblyVersion.get -> System.Version!
+CheatEngine.Client.Runtime.ClientCapabilities
+CheatEngine.Client.Runtime.ClientCapabilities.Count.get -> int
+CheatEngine.Client.Runtime.ClientCapabilities.Entries.get -> System.ReadOnlySpan
+CheatEngine.Client.Runtime.ClientCapabilities.Equals(CheatEngine.Client.Runtime.ClientCapabilities? other) -> bool
+CheatEngine.Client.Runtime.ClientCapabilities.TryGet(CheatEngine.Client.Runtime.ClientCapabilityId capability, out CheatEngine.Client.Runtime.ClientCapabilityAvailability availability) -> bool
+CheatEngine.Client.Runtime.ClientCapabilityAvailability
+CheatEngine.Client.Runtime.ClientCapabilityAvailability.Capability.get -> CheatEngine.Client.Runtime.ClientCapabilityId
+CheatEngine.Client.Runtime.ClientCapabilityAvailability.ClientCapabilityAvailability() -> void
+CheatEngine.Client.Runtime.ClientCapabilityAvailability.ClientCapabilityAvailability(CheatEngine.Client.Runtime.ClientCapabilityId capability, CheatEngine.Client.Runtime.ClientCapabilityAvailabilityState state, string! reason) -> void
+CheatEngine.Client.Runtime.ClientCapabilityAvailability.Equals(CheatEngine.Client.Runtime.ClientCapabilityAvailability other) -> bool
+CheatEngine.Client.Runtime.ClientCapabilityAvailability.IsAvailable.get -> bool
+CheatEngine.Client.Runtime.ClientCapabilityAvailability.IsKnown.get -> bool
+CheatEngine.Client.Runtime.ClientCapabilityAvailability.Reason.get -> string!
+CheatEngine.Client.Runtime.ClientCapabilityAvailability.State.get -> CheatEngine.Client.Runtime.ClientCapabilityAvailabilityState
+CheatEngine.Client.Runtime.ClientCapabilityAvailabilityState
+CheatEngine.Client.Runtime.ClientCapabilityAvailabilityState.Available = 1 -> CheatEngine.Client.Runtime.ClientCapabilityAvailabilityState
+CheatEngine.Client.Runtime.ClientCapabilityAvailabilityState.Unavailable = 2 -> CheatEngine.Client.Runtime.ClientCapabilityAvailabilityState
+CheatEngine.Client.Runtime.ClientCapabilityAvailabilityState.Unknown = 0 -> CheatEngine.Client.Runtime.ClientCapabilityAvailabilityState
+CheatEngine.Client.Runtime.ClientCapabilityId
+CheatEngine.Client.Runtime.ClientCapabilityId.ClientCapabilityId() -> void
+CheatEngine.Client.Runtime.ClientCapabilityId.ClientCapabilityId(string! value) -> void
+CheatEngine.Client.Runtime.ClientCapabilityId.Equals(CheatEngine.Client.Runtime.ClientCapabilityId other) -> bool
+CheatEngine.Client.Runtime.ClientCapabilityId.IsEmpty.get -> bool
+CheatEngine.Client.Runtime.ClientCapabilityId.Value.get -> string!
+CheatEngine.Client.Runtime.ICheatEngineRuntime
+CheatEngine.Client.Runtime.ICheatEngineRuntime.Epoch.get -> long
+CheatEngine.Client.Runtime.ICheatEngineRuntime.GetClientCapability(CheatEngine.Client.Runtime.ClientCapabilityId capability, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Runtime.ClientCapabilityAvailability
+CheatEngine.Client.Runtime.ICheatEngineRuntime.GetSdkCapability(CheatEngine.SDK.Engine.Runtime.RuntimeCapabilityId capability, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.SDK.Engine.Runtime.RuntimeCapabilityAvailability
+CheatEngine.Client.Runtime.ICheatEngineRuntime.GetSnapshot(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Runtime.CheatEngineRuntimeSnapshot
+CheatEngine.Client.Runtime.ICheatEngineRuntime.TryGetClientCapability(CheatEngine.Client.Runtime.ClientCapabilityId capability, out CheatEngine.Client.Runtime.ClientCapabilityAvailability availability, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Runtime.ICheatEngineRuntime.TryGetSdkCapability(CheatEngine.SDK.Engine.Runtime.RuntimeCapabilityId capability, out CheatEngine.SDK.Engine.Runtime.RuntimeCapabilityAvailability availability, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Runtime.ICheatEngineRuntime.TryGetSnapshot(out CheatEngine.Client.Runtime.CheatEngineRuntimeSnapshot snapshot, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Scanning.AobPattern
+CheatEngine.Client.Scanning.AobPattern.AobPattern() -> void
+CheatEngine.Client.Scanning.AobPattern.AobPattern(string! value) -> void
+CheatEngine.Client.Scanning.AobPattern.ByteLength.get -> int
+CheatEngine.Client.Scanning.AobPattern.Equals(CheatEngine.Client.Scanning.AobPattern other) -> bool
+CheatEngine.Client.Scanning.AobPattern.IsWildcardOnly.get -> bool
+CheatEngine.Client.Scanning.AobPattern.Value.get -> string!
+CheatEngine.Client.Scanning.AobScanRange
+CheatEngine.Client.Scanning.AobScanRange.AobScanRange() -> void
+CheatEngine.Client.Scanning.AobScanRange.AobScanRange(CheatEngine.SDK.Engine.Values.Address start, CheatEngine.SDK.Engine.Values.Address end) -> void
+CheatEngine.Client.Scanning.AobScanRange.Contains(CheatEngine.SDK.Engine.Values.Address address) -> bool
+CheatEngine.Client.Scanning.AobScanRange.End.get -> CheatEngine.SDK.Engine.Values.Address
+CheatEngine.Client.Scanning.AobScanRange.Equals(CheatEngine.Client.Scanning.AobScanRange other) -> bool
+CheatEngine.Client.Scanning.AobScanRange.Start.get -> CheatEngine.SDK.Engine.Values.Address
+CheatEngine.Client.Scanning.AobScanRequest
+CheatEngine.Client.Scanning.AobScanRequest.AobScanRequest() -> void
+CheatEngine.Client.Scanning.AobScanRequest.AobScanRequest(CheatEngine.Client.Scanning.AobPattern pattern, CheatEngine.SDK.Engine.Scanning.Aob.AobScanOptions options, int maximumResults, CheatEngine.SDK.Engine.Inspection.ModuleName? module = null, CheatEngine.Client.Scanning.AobScanRange? range = null) -> void
+CheatEngine.Client.Scanning.AobScanRequest.Equals(CheatEngine.Client.Scanning.AobScanRequest other) -> bool
+CheatEngine.Client.Scanning.AobScanRequest.MaximumResults.get -> int
+CheatEngine.Client.Scanning.AobScanRequest.Module.get -> CheatEngine.SDK.Engine.Inspection.ModuleName?
+CheatEngine.Client.Scanning.AobScanRequest.Options.get -> CheatEngine.SDK.Engine.Scanning.Aob.AobScanOptions
+CheatEngine.Client.Scanning.AobScanRequest.Pattern.get -> CheatEngine.Client.Scanning.AobPattern
+CheatEngine.Client.Scanning.AobScanRequest.Range.get -> CheatEngine.Client.Scanning.AobScanRange?
+CheatEngine.Client.Scanning.AobScanResult
+CheatEngine.Client.Scanning.AobScanResult.AobScanResult() -> void
+CheatEngine.Client.Scanning.AobScanResult.AobScanResult(System.Collections.Immutable.ImmutableArray matches, bool isTruncated) -> void
+CheatEngine.Client.Scanning.AobScanResult.Equals(CheatEngine.Client.Scanning.AobScanResult other) -> bool
+CheatEngine.Client.Scanning.AobScanResult.IsTruncated.get -> bool
+CheatEngine.Client.Scanning.AobScanResult.Matches.get -> System.Collections.Immutable.ImmutableArray
+CheatEngine.Client.Scanning.IPatternScanner
+CheatEngine.Client.Scanning.IPatternScanner.Scan(CheatEngine.Client.Scanning.AobScanRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Scanning.AobScanResult
+CheatEngine.Client.Scanning.IPatternScanner.TryScan(CheatEngine.Client.Scanning.AobScanRequest request, out CheatEngine.Client.Scanning.AobScanResult result, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Scanning.IValueScanner
+CheatEngine.Client.Scanning.IValueScanner.CreateSession(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Scanning.IValueScanSession!
+CheatEngine.Client.Scanning.IValueScanner.TryCreateSession(out CheatEngine.Client.Scanning.IValueScanSession? session, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Scanning.IValueScanSession
+CheatEngine.Client.Scanning.IValueScanSession.GetResultCount(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> ulong
+CheatEngine.Client.Scanning.IValueScanSession.Read(CheatEngine.Client.Scanning.ValueScanReadRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Scanning.ValueScanPage
+CheatEngine.Client.Scanning.IValueScanSession.Reset(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> void
+CheatEngine.Client.Scanning.IValueScanSession.RunNextScan(CheatEngine.SDK.Engine.Scanning.Values.NextScanRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> void
+CheatEngine.Client.Scanning.IValueScanSession.Start(CheatEngine.SDK.Engine.Scanning.Values.FirstScanRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> void
+CheatEngine.Client.Scanning.IValueScanSession.State.get -> CheatEngine.Client.Scanning.ValueScanSessionState
+CheatEngine.Client.Scanning.IValueScanSession.TryGetResultCount(out ulong resultCount, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Scanning.IValueScanSession.TryRead(CheatEngine.Client.Scanning.ValueScanReadRequest request, out CheatEngine.Client.Scanning.ValueScanPage page, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Scanning.IValueScanSession.TryReset(out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Scanning.IValueScanSession.TryRunNextScan(CheatEngine.SDK.Engine.Scanning.Values.NextScanRequest request, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Scanning.IValueScanSession.TryStart(CheatEngine.SDK.Engine.Scanning.Values.FirstScanRequest request, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Scanning.ValueScanMatch
+CheatEngine.Client.Scanning.ValueScanMatch.Address.get -> CheatEngine.SDK.Engine.Values.Address
+CheatEngine.Client.Scanning.ValueScanMatch.Equals(CheatEngine.Client.Scanning.ValueScanMatch other) -> bool
+CheatEngine.Client.Scanning.ValueScanMatch.Index.get -> int
+CheatEngine.Client.Scanning.ValueScanMatch.Value.get -> string!
+CheatEngine.Client.Scanning.ValueScanMatch.ValueScanMatch() -> void
+CheatEngine.Client.Scanning.ValueScanMatch.ValueScanMatch(int index, CheatEngine.SDK.Engine.Values.Address address, string! value) -> void
+CheatEngine.Client.Scanning.ValueScanPage
+CheatEngine.Client.Scanning.ValueScanPage.Equals(CheatEngine.Client.Scanning.ValueScanPage other) -> bool
+CheatEngine.Client.Scanning.ValueScanPage.Matches.get -> System.Collections.Immutable.ImmutableArray
+CheatEngine.Client.Scanning.ValueScanPage.TotalCount.get -> ulong
+CheatEngine.Client.Scanning.ValueScanPage.ValueScanPage() -> void
+CheatEngine.Client.Scanning.ValueScanPage.ValueScanPage(ulong totalCount, System.Collections.Immutable.ImmutableArray matches) -> void
+CheatEngine.Client.Scanning.ValueScanReadRequest
+CheatEngine.Client.Scanning.ValueScanReadRequest.Equals(CheatEngine.Client.Scanning.ValueScanReadRequest other) -> bool
+CheatEngine.Client.Scanning.ValueScanReadRequest.MaximumCount.get -> int
+CheatEngine.Client.Scanning.ValueScanReadRequest.StartIndex.get -> int
+CheatEngine.Client.Scanning.ValueScanReadRequest.ValueScanReadRequest() -> void
+CheatEngine.Client.Scanning.ValueScanReadRequest.ValueScanReadRequest(int startIndex, int maximumCount) -> void
+CheatEngine.Client.Scanning.ValueScanSessionState
+CheatEngine.Client.Scanning.ValueScanSessionState.Created = 0 -> CheatEngine.Client.Scanning.ValueScanSessionState
+CheatEngine.Client.Scanning.ValueScanSessionState.Disposed = 4 -> CheatEngine.Client.Scanning.ValueScanSessionState
+CheatEngine.Client.Scanning.ValueScanSessionState.Invalidated = 3 -> CheatEngine.Client.Scanning.ValueScanSessionState
+CheatEngine.Client.Scanning.ValueScanSessionState.ResultsReady = 2 -> CheatEngine.Client.Scanning.ValueScanSessionState
+CheatEngine.Client.Scanning.ValueScanSessionState.Scanning = 1 -> CheatEngine.Client.Scanning.ValueScanSessionState
+CheatEngine.Client.Tables.AddressTableSnapshot
+CheatEngine.Client.Tables.AddressTableSnapshot.AddressTableSnapshot() -> void
+CheatEngine.Client.Tables.AddressTableSnapshot.AddressTableSnapshot(int recordCount) -> void
+CheatEngine.Client.Tables.AddressTableSnapshot.AddressTableSnapshot(System.Collections.Immutable.ImmutableArray records) -> void
+CheatEngine.Client.Tables.AddressTableSnapshot.Equals(CheatEngine.Client.Tables.AddressTableSnapshot other) -> bool
+CheatEngine.Client.Tables.AddressTableSnapshot.RecordCount.get -> int
+CheatEngine.Client.Tables.AddressTableSnapshot.Records.get -> System.Collections.Immutable.ImmutableArray
+CheatEngine.Client.Tables.ITableClient
+CheatEngine.Client.Tables.ITableClient.Create(CheatEngine.Client.Tables.MemoryRecordDefinition definition, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Tables.MemoryRecordSnapshot
+CheatEngine.Client.Tables.ITableClient.Delete(CheatEngine.SDK.Engine.AddressList.MemoryRecordId id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> void
+CheatEngine.Client.Tables.ITableClient.Find(CheatEngine.Client.Tables.MemoryRecordSearch search, CheatEngine.Client.Tables.MemoryRecordCollectionRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Collections.Immutable.ImmutableArray
+CheatEngine.Client.Tables.ITableClient.GetCurrent(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Tables.AddressTableSnapshot
+CheatEngine.Client.Tables.ITableClient.GetHierarchy(CheatEngine.SDK.Engine.AddressList.MemoryRecordId rootId, CheatEngine.Client.Tables.MemoryRecordHierarchyRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Tables.MemoryRecordHierarchySnapshot
+CheatEngine.Client.Tables.ITableClient.GetRecord(CheatEngine.SDK.Engine.AddressList.MemoryRecordId id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Tables.MemoryRecordSnapshot
+CheatEngine.Client.Tables.ITableClient.GetRecord(int index, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Tables.MemoryRecordSnapshot
+CheatEngine.Client.Tables.ITableClient.GetSelected(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Tables.MemoryRecordSnapshot
+CheatEngine.Client.Tables.ITableClient.GetSnapshot(CheatEngine.Client.Tables.MemoryRecordCollectionRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Tables.AddressTableSnapshot
+CheatEngine.Client.Tables.ITableClient.LoadTrustedTable(CheatEngine.Client.Tables.TableLoadRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> void
+CheatEngine.Client.Tables.ITableClient.SaveTable(CheatEngine.Client.Tables.TableSaveRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> void
+CheatEngine.Client.Tables.ITableClient.SelectRecord(CheatEngine.SDK.Engine.AddressList.MemoryRecordId id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Tables.MemoryRecordSnapshot
+CheatEngine.Client.Tables.ITableClient.SetActive(CheatEngine.SDK.Engine.AddressList.MemoryRecordId id, bool isActive, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Tables.MemoryRecordSnapshot
+CheatEngine.Client.Tables.ITableClient.SetParent(CheatEngine.SDK.Engine.AddressList.MemoryRecordId childId, CheatEngine.SDK.Engine.AddressList.MemoryRecordId? parentId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Tables.MemoryRecordSnapshot
+CheatEngine.Client.Tables.ITableClient.TryCreate(CheatEngine.Client.Tables.MemoryRecordDefinition definition, out CheatEngine.Client.Tables.MemoryRecordSnapshot record, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Tables.ITableClient.TryDelete(CheatEngine.SDK.Engine.AddressList.MemoryRecordId id, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Tables.ITableClient.TryFind(CheatEngine.Client.Tables.MemoryRecordSearch search, CheatEngine.Client.Tables.MemoryRecordCollectionRequest request, out System.Collections.Immutable.ImmutableArray records, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Tables.ITableClient.TryGetCurrent(out CheatEngine.Client.Tables.AddressTableSnapshot table, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Tables.ITableClient.TryGetHierarchy(CheatEngine.SDK.Engine.AddressList.MemoryRecordId rootId, CheatEngine.Client.Tables.MemoryRecordHierarchyRequest request, out CheatEngine.Client.Tables.MemoryRecordHierarchySnapshot hierarchy, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Tables.ITableClient.TryGetRecord(CheatEngine.SDK.Engine.AddressList.MemoryRecordId id, out CheatEngine.Client.Tables.MemoryRecordSnapshot record, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Tables.ITableClient.TryGetRecord(int index, out CheatEngine.Client.Tables.MemoryRecordSnapshot record, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Tables.ITableClient.TryGetSelected(out CheatEngine.Client.Tables.MemoryRecordSnapshot record, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Tables.ITableClient.TryGetSnapshot(CheatEngine.Client.Tables.MemoryRecordCollectionRequest request, out CheatEngine.Client.Tables.AddressTableSnapshot table, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Tables.ITableClient.TryLoadTrustedTable(CheatEngine.Client.Tables.TableLoadRequest request, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Tables.ITableClient.TrySaveTable(CheatEngine.Client.Tables.TableSaveRequest request, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Tables.ITableClient.TrySelect(CheatEngine.SDK.Engine.AddressList.MemoryRecordId id, out CheatEngine.Client.Tables.MemoryRecordSnapshot record, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Tables.ITableClient.TrySetActive(CheatEngine.SDK.Engine.AddressList.MemoryRecordId id, bool isActive, out CheatEngine.Client.Tables.MemoryRecordSnapshot record, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Tables.ITableClient.TrySetParent(CheatEngine.SDK.Engine.AddressList.MemoryRecordId childId, CheatEngine.SDK.Engine.AddressList.MemoryRecordId? parentId, out CheatEngine.Client.Tables.MemoryRecordSnapshot record, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Tables.ITableClient.TryUpdate(CheatEngine.Client.Tables.MemoryRecordUpdate update, out CheatEngine.Client.Tables.MemoryRecordSnapshot record, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool
+CheatEngine.Client.Tables.ITableClient.Update(CheatEngine.Client.Tables.MemoryRecordUpdate update, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Tables.MemoryRecordSnapshot
+CheatEngine.Client.Tables.MemoryRecordCollectionRequest
+CheatEngine.Client.Tables.MemoryRecordCollectionRequest.Equals(CheatEngine.Client.Tables.MemoryRecordCollectionRequest other) -> bool
+CheatEngine.Client.Tables.MemoryRecordCollectionRequest.MaximumItems.get -> int
+CheatEngine.Client.Tables.MemoryRecordCollectionRequest.MemoryRecordCollectionRequest() -> void
+CheatEngine.Client.Tables.MemoryRecordCollectionRequest.MemoryRecordCollectionRequest(int maximumItems) -> void
+CheatEngine.Client.Tables.MemoryRecordDefinition
+CheatEngine.Client.Tables.MemoryRecordDefinition.AddressExpression.get -> string!
+CheatEngine.Client.Tables.MemoryRecordDefinition.Description.get -> string!
+CheatEngine.Client.Tables.MemoryRecordDefinition.Equals(CheatEngine.Client.Tables.MemoryRecordDefinition other) -> bool
+CheatEngine.Client.Tables.MemoryRecordDefinition.MemoryRecordDefinition() -> void
+CheatEngine.Client.Tables.MemoryRecordDefinition.MemoryRecordDefinition(string! description, string! addressExpression, string! value, CheatEngine.SDK.Engine.Enums.VariableType variableType, CheatEngine.SDK.Engine.AddressList.MemoryRecordId? parentId = null) -> void
+CheatEngine.Client.Tables.MemoryRecordDefinition.ParentId.get -> CheatEngine.SDK.Engine.AddressList.MemoryRecordId?
+CheatEngine.Client.Tables.MemoryRecordDefinition.Value.get -> string!
+CheatEngine.Client.Tables.MemoryRecordDefinition.VariableType.get -> CheatEngine.SDK.Engine.Enums.VariableType
+CheatEngine.Client.Tables.MemoryRecordHierarchyRequest
+CheatEngine.Client.Tables.MemoryRecordHierarchyRequest.Equals(CheatEngine.Client.Tables.MemoryRecordHierarchyRequest other) -> bool
+CheatEngine.Client.Tables.MemoryRecordHierarchyRequest.MaximumDepth.get -> int
+CheatEngine.Client.Tables.MemoryRecordHierarchyRequest.MaximumItems.get -> int
+CheatEngine.Client.Tables.MemoryRecordHierarchyRequest.MemoryRecordHierarchyRequest() -> void
+CheatEngine.Client.Tables.MemoryRecordHierarchyRequest.MemoryRecordHierarchyRequest(int maximumItems, int maximumDepth) -> void
+CheatEngine.Client.Tables.MemoryRecordHierarchySnapshot
+CheatEngine.Client.Tables.MemoryRecordHierarchySnapshot.Children.get -> System.Collections.Immutable.ImmutableArray
+CheatEngine.Client.Tables.MemoryRecordHierarchySnapshot.Children.init -> void
+CheatEngine.Client.Tables.MemoryRecordHierarchySnapshot.Deconstruct(out CheatEngine.Client.Tables.MemoryRecordSnapshot Record, out System.Collections.Immutable.ImmutableArray Children) -> void
+CheatEngine.Client.Tables.MemoryRecordHierarchySnapshot.Equals(CheatEngine.Client.Tables.MemoryRecordHierarchySnapshot other) -> bool
+CheatEngine.Client.Tables.MemoryRecordHierarchySnapshot.MemoryRecordHierarchySnapshot() -> void
+CheatEngine.Client.Tables.MemoryRecordHierarchySnapshot.MemoryRecordHierarchySnapshot(CheatEngine.Client.Tables.MemoryRecordSnapshot Record, System.Collections.Immutable.ImmutableArray Children) -> void
+CheatEngine.Client.Tables.MemoryRecordHierarchySnapshot.Record.get -> CheatEngine.Client.Tables.MemoryRecordSnapshot
+CheatEngine.Client.Tables.MemoryRecordHierarchySnapshot.Record.init -> void
+CheatEngine.Client.Tables.MemoryRecordSearch
+CheatEngine.Client.Tables.MemoryRecordSearch.AddressExpression.get -> string?
+CheatEngine.Client.Tables.MemoryRecordSearch.DescriptionContains.get -> string?
+CheatEngine.Client.Tables.MemoryRecordSearch.Equals(CheatEngine.Client.Tables.MemoryRecordSearch other) -> bool
+CheatEngine.Client.Tables.MemoryRecordSearch.IsActive.get -> bool?
+CheatEngine.Client.Tables.MemoryRecordSearch.MemoryRecordSearch() -> void
+CheatEngine.Client.Tables.MemoryRecordSearch.MemoryRecordSearch(string? descriptionContains = null, string? addressExpression = null, CheatEngine.SDK.Engine.Enums.VariableType? variableType = null, bool? isActive = null) -> void
+CheatEngine.Client.Tables.MemoryRecordSearch.VariableType.get -> CheatEngine.SDK.Engine.Enums.VariableType?
+CheatEngine.Client.Tables.MemoryRecordContentSnapshot
+CheatEngine.Client.Tables.MemoryRecordContentSnapshot.AddressExpression.get -> string!
+CheatEngine.Client.Tables.MemoryRecordContentSnapshot.Description.get -> string!
+CheatEngine.Client.Tables.MemoryRecordContentSnapshot.Equals(CheatEngine.Client.Tables.MemoryRecordContentSnapshot other) -> bool
+CheatEngine.Client.Tables.MemoryRecordContentSnapshot.MemoryRecordContentSnapshot() -> void
+CheatEngine.Client.Tables.MemoryRecordContentSnapshot.MemoryRecordContentSnapshot(string! description, string! addressExpression, string! value, CheatEngine.SDK.Engine.Enums.VariableType variableType) -> void
+CheatEngine.Client.Tables.MemoryRecordContentSnapshot.Value.get -> string!
+CheatEngine.Client.Tables.MemoryRecordContentSnapshot.VariableType.get -> CheatEngine.SDK.Engine.Enums.VariableType
+CheatEngine.Client.Tables.MemoryRecordSnapshot
+CheatEngine.Client.Tables.MemoryRecordSnapshot.AddressExpression.get -> string!
+CheatEngine.Client.Tables.MemoryRecordSnapshot.ChildCount.get -> int
+CheatEngine.Client.Tables.MemoryRecordSnapshot.CurrentAddress.get -> CheatEngine.SDK.Engine.Values.Address?
+CheatEngine.Client.Tables.MemoryRecordSnapshot.Description.get -> string!
+CheatEngine.Client.Tables.MemoryRecordSnapshot.Equals(CheatEngine.Client.Tables.MemoryRecordSnapshot other) -> bool
+CheatEngine.Client.Tables.MemoryRecordSnapshot.Id.get -> CheatEngine.SDK.Engine.AddressList.MemoryRecordId
+CheatEngine.Client.Tables.MemoryRecordSnapshot.Index.get -> int
+CheatEngine.Client.Tables.MemoryRecordSnapshot.IsActive.get -> bool
+CheatEngine.Client.Tables.MemoryRecordSnapshot.MemoryRecordSnapshot() -> void
+CheatEngine.Client.Tables.MemoryRecordSnapshot.MemoryRecordSnapshot(CheatEngine.SDK.Engine.AddressList.MemoryRecordId id, int index, CheatEngine.Client.Tables.MemoryRecordContentSnapshot content, CheatEngine.Client.Tables.MemoryRecordStateSnapshot state) -> void
+CheatEngine.Client.Tables.MemoryRecordSnapshot.Content.get -> CheatEngine.Client.Tables.MemoryRecordContentSnapshot
+CheatEngine.Client.Tables.MemoryRecordSnapshot.State.get -> CheatEngine.Client.Tables.MemoryRecordStateSnapshot
+CheatEngine.Client.Tables.MemoryRecordSnapshot.Value.get -> string!
+CheatEngine.Client.Tables.MemoryRecordSnapshot.VariableType.get -> CheatEngine.SDK.Engine.Enums.VariableType
+CheatEngine.Client.Tables.MemoryRecordStateSnapshot
+CheatEngine.Client.Tables.MemoryRecordStateSnapshot.ChildCount.get -> int
+CheatEngine.Client.Tables.MemoryRecordStateSnapshot.CurrentAddress.get -> CheatEngine.SDK.Engine.Values.Address?
+CheatEngine.Client.Tables.MemoryRecordStateSnapshot.Equals(CheatEngine.Client.Tables.MemoryRecordStateSnapshot other) -> bool
+CheatEngine.Client.Tables.MemoryRecordStateSnapshot.IsActive.get -> bool
+CheatEngine.Client.Tables.MemoryRecordStateSnapshot.MemoryRecordStateSnapshot() -> void
+CheatEngine.Client.Tables.MemoryRecordStateSnapshot.MemoryRecordStateSnapshot(CheatEngine.SDK.Engine.Values.Address? currentAddress, bool isActive = false, int childCount = 0) -> void
+CheatEngine.Client.Tables.MemoryRecordUpdate
+CheatEngine.Client.Tables.MemoryRecordUpdate.AddressExpression.get -> string?
+CheatEngine.Client.Tables.MemoryRecordUpdate.Description.get -> string?
+CheatEngine.Client.Tables.MemoryRecordUpdate.Equals(CheatEngine.Client.Tables.MemoryRecordUpdate other) -> bool
+CheatEngine.Client.Tables.MemoryRecordUpdate.Id.get -> CheatEngine.SDK.Engine.AddressList.MemoryRecordId
+CheatEngine.Client.Tables.MemoryRecordUpdate.MemoryRecordUpdate() -> void
+CheatEngine.Client.Tables.MemoryRecordUpdate.MemoryRecordUpdate(CheatEngine.SDK.Engine.AddressList.MemoryRecordId id, string? description = null, string? addressExpression = null, string? value = null, CheatEngine.SDK.Engine.Enums.VariableType? variableType = null) -> void
+CheatEngine.Client.Tables.MemoryRecordUpdate.Value.get -> string?
+CheatEngine.Client.Tables.MemoryRecordUpdate.VariableType.get -> CheatEngine.SDK.Engine.Enums.VariableType?
+CheatEngine.Client.Tables.TableLoadRequest
+CheatEngine.Client.Tables.TableLoadRequest.Deconstruct(out CheatEngine.Client.Tables.TrustedTableFile File, out bool Merge) -> void
+CheatEngine.Client.Tables.TableLoadRequest.Equals(CheatEngine.Client.Tables.TableLoadRequest other) -> bool
+CheatEngine.Client.Tables.TableLoadRequest.File.get -> CheatEngine.Client.Tables.TrustedTableFile
+CheatEngine.Client.Tables.TableLoadRequest.File.init -> void
+CheatEngine.Client.Tables.TableLoadRequest.Merge.get -> bool
+CheatEngine.Client.Tables.TableLoadRequest.Merge.init -> void
+CheatEngine.Client.Tables.TableLoadRequest.TableLoadRequest() -> void
+CheatEngine.Client.Tables.TableLoadRequest.TableLoadRequest(CheatEngine.Client.Tables.TrustedTableFile File, bool Merge = false) -> void
+CheatEngine.Client.Tables.TableSaveRequest
+CheatEngine.Client.Tables.TableSaveRequest.Deconstruct(out CheatEngine.Client.Tables.TrustedTableFile File) -> void
+CheatEngine.Client.Tables.TableSaveRequest.Equals(CheatEngine.Client.Tables.TableSaveRequest other) -> bool
+CheatEngine.Client.Tables.TableSaveRequest.File.get -> CheatEngine.Client.Tables.TrustedTableFile
+CheatEngine.Client.Tables.TableSaveRequest.File.init -> void
+CheatEngine.Client.Tables.TableSaveRequest.TableSaveRequest() -> void
+CheatEngine.Client.Tables.TableSaveRequest.TableSaveRequest(CheatEngine.Client.Tables.TrustedTableFile File) -> void
+CheatEngine.Client.Tables.TrustedTableFile
+CheatEngine.Client.Tables.TrustedTableFile.Equals(CheatEngine.Client.Tables.TrustedTableFile other) -> bool
+CheatEngine.Client.Tables.TrustedTableFile.FullPath.get -> string!
+CheatEngine.Client.Tables.TrustedTableFile.TrustedTableFile() -> void
+CheatEngine.Client.Tables.TrustedTableFile.TrustedTableFile(string! path) -> void
+override CheatEngine.Client.Inspection.InspectionCollectionRequest.GetHashCode() -> int
+override CheatEngine.Client.Inspection.SymbolRegistration.GetHashCode() -> int
+override CheatEngine.Client.Lua.LuaScript.GetHashCode() -> int
+override CheatEngine.Client.Memory.MemoryBytesReadRequest.GetHashCode() -> int
+override CheatEngine.Client.Memory.MemoryBytesWriteRequest.GetHashCode() -> int
+override CheatEngine.Client.Memory.MemoryReadRequest.GetHashCode() -> int
+override CheatEngine.Client.Memory.MemoryStringReadRequest.GetHashCode() -> int
+override CheatEngine.Client.Memory.MemoryStringWriteRequest.GetHashCode() -> int
+override CheatEngine.Client.Memory.MemoryWriteRequest.GetHashCode() -> int
+override CheatEngine.Client.Memory.PointerChainRequest.GetHashCode() -> int
+override CheatEngine.Client.Processes.ProcessSnapshot.GetHashCode() -> int
+override CheatEngine.Client.Results.CheatEngineFailure.GetHashCode() -> int
+override CheatEngine.Client.Runtime.CheatEngineRuntimePlatformInfo.GetHashCode() -> int
+override CheatEngine.Client.Runtime.CheatEngineRuntimeSnapshot.GetHashCode() -> int
+override CheatEngine.Client.Runtime.CheatEngineRuntimeVersionInfo.GetHashCode() -> int
+override CheatEngine.Client.Runtime.ClientCapabilities.Equals(object? obj) -> bool
+override CheatEngine.Client.Runtime.ClientCapabilities.GetHashCode() -> int
+override CheatEngine.Client.Runtime.ClientCapabilityAvailability.GetHashCode() -> int
+override CheatEngine.Client.Runtime.ClientCapabilityId.Equals(object? obj) -> bool
+override CheatEngine.Client.Runtime.ClientCapabilityId.GetHashCode() -> int
+override CheatEngine.Client.Runtime.ClientCapabilityId.ToString() -> string!
+override CheatEngine.Client.Scanning.AobPattern.GetHashCode() -> int
+override CheatEngine.Client.Scanning.AobPattern.ToString() -> string!
+override CheatEngine.Client.Scanning.AobScanRange.GetHashCode() -> int
+override CheatEngine.Client.Scanning.AobScanRequest.GetHashCode() -> int
+override CheatEngine.Client.Scanning.AobScanResult.GetHashCode() -> int
+override CheatEngine.Client.Scanning.ValueScanMatch.GetHashCode() -> int
+override CheatEngine.Client.Scanning.ValueScanPage.GetHashCode() -> int
+override CheatEngine.Client.Scanning.ValueScanReadRequest.GetHashCode() -> int
+override CheatEngine.Client.Tables.AddressTableSnapshot.GetHashCode() -> int
+override CheatEngine.Client.Tables.MemoryRecordCollectionRequest.GetHashCode() -> int
+override CheatEngine.Client.Tables.MemoryRecordDefinition.GetHashCode() -> int
+override CheatEngine.Client.Tables.MemoryRecordHierarchyRequest.GetHashCode() -> int
+override CheatEngine.Client.Tables.MemoryRecordHierarchySnapshot.GetHashCode() -> int
+override CheatEngine.Client.Tables.MemoryRecordSearch.GetHashCode() -> int
+override CheatEngine.Client.Tables.MemoryRecordContentSnapshot.GetHashCode() -> int
+override CheatEngine.Client.Tables.MemoryRecordSnapshot.GetHashCode() -> int
+override CheatEngine.Client.Tables.MemoryRecordStateSnapshot.GetHashCode() -> int
+override CheatEngine.Client.Tables.MemoryRecordUpdate.GetHashCode() -> int
+override CheatEngine.Client.Tables.TableLoadRequest.GetHashCode() -> int
+override CheatEngine.Client.Tables.TableSaveRequest.GetHashCode() -> int
+override CheatEngine.Client.Tables.TrustedTableFile.GetHashCode() -> int
+static CheatEngine.Client.Inspection.InspectionCollectionRequest.operator !=(CheatEngine.Client.Inspection.InspectionCollectionRequest left, CheatEngine.Client.Inspection.InspectionCollectionRequest right) -> bool
+static CheatEngine.Client.Inspection.InspectionCollectionRequest.operator ==(CheatEngine.Client.Inspection.InspectionCollectionRequest left, CheatEngine.Client.Inspection.InspectionCollectionRequest right) -> bool
+static CheatEngine.Client.Inspection.SymbolRegistration.operator !=(CheatEngine.Client.Inspection.SymbolRegistration left, CheatEngine.Client.Inspection.SymbolRegistration right) -> bool
+static CheatEngine.Client.Inspection.SymbolRegistration.operator ==(CheatEngine.Client.Inspection.SymbolRegistration left, CheatEngine.Client.Inspection.SymbolRegistration right) -> bool
+static CheatEngine.Client.Lua.LuaScript.operator !=(CheatEngine.Client.Lua.LuaScript left, CheatEngine.Client.Lua.LuaScript right) -> bool
+static CheatEngine.Client.Lua.LuaScript.operator ==(CheatEngine.Client.Lua.LuaScript left, CheatEngine.Client.Lua.LuaScript right) -> bool
+static CheatEngine.Client.Memory.MemoryBytesReadRequest.operator !=(CheatEngine.Client.Memory.MemoryBytesReadRequest left, CheatEngine.Client.Memory.MemoryBytesReadRequest right) -> bool
+static CheatEngine.Client.Memory.MemoryBytesReadRequest.operator ==(CheatEngine.Client.Memory.MemoryBytesReadRequest left, CheatEngine.Client.Memory.MemoryBytesReadRequest right) -> bool
+static CheatEngine.Client.Memory.MemoryBytesWriteRequest.operator !=(CheatEngine.Client.Memory.MemoryBytesWriteRequest left, CheatEngine.Client.Memory.MemoryBytesWriteRequest right) -> bool
+static CheatEngine.Client.Memory.MemoryBytesWriteRequest.operator ==(CheatEngine.Client.Memory.MemoryBytesWriteRequest left, CheatEngine.Client.Memory.MemoryBytesWriteRequest right) -> bool
+static CheatEngine.Client.Memory.MemoryReadRequest.operator !=(CheatEngine.Client.Memory.MemoryReadRequest left, CheatEngine.Client.Memory.MemoryReadRequest right) -> bool
+static CheatEngine.Client.Memory.MemoryReadRequest.operator ==(CheatEngine.Client.Memory.MemoryReadRequest left, CheatEngine.Client.Memory.MemoryReadRequest right) -> bool
+static CheatEngine.Client.Memory.MemoryStringReadRequest.operator !=(CheatEngine.Client.Memory.MemoryStringReadRequest left, CheatEngine.Client.Memory.MemoryStringReadRequest right) -> bool
+static CheatEngine.Client.Memory.MemoryStringReadRequest.operator ==(CheatEngine.Client.Memory.MemoryStringReadRequest left, CheatEngine.Client.Memory.MemoryStringReadRequest right) -> bool
+static CheatEngine.Client.Memory.MemoryStringWriteRequest.operator !=(CheatEngine.Client.Memory.MemoryStringWriteRequest left, CheatEngine.Client.Memory.MemoryStringWriteRequest right) -> bool
+static CheatEngine.Client.Memory.MemoryStringWriteRequest.operator ==(CheatEngine.Client.Memory.MemoryStringWriteRequest left, CheatEngine.Client.Memory.MemoryStringWriteRequest right) -> bool
+static CheatEngine.Client.Memory.MemoryWriteRequest.operator !=(CheatEngine.Client.Memory.MemoryWriteRequest left, CheatEngine.Client.Memory.MemoryWriteRequest right) -> bool
+static CheatEngine.Client.Memory.MemoryWriteRequest.operator ==(CheatEngine.Client.Memory.MemoryWriteRequest left, CheatEngine.Client.Memory.MemoryWriteRequest right) -> bool
+static CheatEngine.Client.Memory.PointerChainRequest.operator !=(CheatEngine.Client.Memory.PointerChainRequest left, CheatEngine.Client.Memory.PointerChainRequest right) -> bool
+static CheatEngine.Client.Memory.PointerChainRequest.operator ==(CheatEngine.Client.Memory.PointerChainRequest left, CheatEngine.Client.Memory.PointerChainRequest right) -> bool
+static CheatEngine.Client.Processes.ProcessSnapshot.operator !=(CheatEngine.Client.Processes.ProcessSnapshot left, CheatEngine.Client.Processes.ProcessSnapshot right) -> bool
+static CheatEngine.Client.Processes.ProcessSnapshot.operator ==(CheatEngine.Client.Processes.ProcessSnapshot left, CheatEngine.Client.Processes.ProcessSnapshot right) -> bool
+static CheatEngine.Client.Results.CheatEngineFailure.operator !=(CheatEngine.Client.Results.CheatEngineFailure left, CheatEngine.Client.Results.CheatEngineFailure right) -> bool
+static CheatEngine.Client.Results.CheatEngineFailure.operator ==(CheatEngine.Client.Results.CheatEngineFailure left, CheatEngine.Client.Results.CheatEngineFailure right) -> bool
+static CheatEngine.Client.Runtime.CheatEngineRuntimePlatformInfo.operator !=(CheatEngine.Client.Runtime.CheatEngineRuntimePlatformInfo left, CheatEngine.Client.Runtime.CheatEngineRuntimePlatformInfo right) -> bool
+static CheatEngine.Client.Runtime.CheatEngineRuntimePlatformInfo.operator ==(CheatEngine.Client.Runtime.CheatEngineRuntimePlatformInfo left, CheatEngine.Client.Runtime.CheatEngineRuntimePlatformInfo right) -> bool
+static CheatEngine.Client.Runtime.CheatEngineRuntimeSnapshot.operator !=(CheatEngine.Client.Runtime.CheatEngineRuntimeSnapshot left, CheatEngine.Client.Runtime.CheatEngineRuntimeSnapshot right) -> bool
+static CheatEngine.Client.Runtime.CheatEngineRuntimeSnapshot.operator ==(CheatEngine.Client.Runtime.CheatEngineRuntimeSnapshot left, CheatEngine.Client.Runtime.CheatEngineRuntimeSnapshot right) -> bool
+static CheatEngine.Client.Runtime.CheatEngineRuntimeVersionInfo.operator !=(CheatEngine.Client.Runtime.CheatEngineRuntimeVersionInfo left, CheatEngine.Client.Runtime.CheatEngineRuntimeVersionInfo right) -> bool
+static CheatEngine.Client.Runtime.CheatEngineRuntimeVersionInfo.operator ==(CheatEngine.Client.Runtime.CheatEngineRuntimeVersionInfo left, CheatEngine.Client.Runtime.CheatEngineRuntimeVersionInfo right) -> bool
+static CheatEngine.Client.Runtime.ClientCapabilities.Create(System.ReadOnlySpan entries) -> CheatEngine.Client.Runtime.ClientCapabilities!
+static CheatEngine.Client.Runtime.ClientCapabilities.Empty.get -> CheatEngine.Client.Runtime.ClientCapabilities!
+static CheatEngine.Client.Runtime.ClientCapabilityAvailability.operator !=(CheatEngine.Client.Runtime.ClientCapabilityAvailability left, CheatEngine.Client.Runtime.ClientCapabilityAvailability right) -> bool
+static CheatEngine.Client.Runtime.ClientCapabilityAvailability.operator ==(CheatEngine.Client.Runtime.ClientCapabilityAvailability left, CheatEngine.Client.Runtime.ClientCapabilityAvailability right) -> bool
+static CheatEngine.Client.Runtime.ClientCapabilityId.Inspection.get -> CheatEngine.Client.Runtime.ClientCapabilityId
+static CheatEngine.Client.Runtime.ClientCapabilityId.operator !=(CheatEngine.Client.Runtime.ClientCapabilityId left, CheatEngine.Client.Runtime.ClientCapabilityId right) -> bool
+static CheatEngine.Client.Runtime.ClientCapabilityId.operator ==(CheatEngine.Client.Runtime.ClientCapabilityId left, CheatEngine.Client.Runtime.ClientCapabilityId right) -> bool
+static CheatEngine.Client.Runtime.ClientCapabilityId.PatternScanning.get -> CheatEngine.Client.Runtime.ClientCapabilityId
+static CheatEngine.Client.Runtime.ClientCapabilityId.ProcessSelection.get -> CheatEngine.Client.Runtime.ClientCapabilityId
+static CheatEngine.Client.Runtime.ClientCapabilityId.ProtectedLua.get -> CheatEngine.Client.Runtime.ClientCapabilityId
+static CheatEngine.Client.Runtime.ClientCapabilityId.Tables.get -> CheatEngine.Client.Runtime.ClientCapabilityId
+static CheatEngine.Client.Runtime.ClientCapabilityId.TypedMemory.get -> CheatEngine.Client.Runtime.ClientCapabilityId
+static CheatEngine.Client.Runtime.ClientCapabilityId.UnsafeLuaExecution.get -> CheatEngine.Client.Runtime.ClientCapabilityId
+static CheatEngine.Client.Runtime.ClientCapabilityId.ValueScanning.get -> CheatEngine.Client.Runtime.ClientCapabilityId
+static CheatEngine.Client.Scanning.AobPattern.operator !=(CheatEngine.Client.Scanning.AobPattern left, CheatEngine.Client.Scanning.AobPattern right) -> bool
+static CheatEngine.Client.Scanning.AobPattern.operator ==(CheatEngine.Client.Scanning.AobPattern left, CheatEngine.Client.Scanning.AobPattern right) -> bool
+static CheatEngine.Client.Scanning.AobPattern.TryParse(string? value, out CheatEngine.Client.Scanning.AobPattern pattern) -> bool
+static CheatEngine.Client.Scanning.AobScanRange.operator !=(CheatEngine.Client.Scanning.AobScanRange left, CheatEngine.Client.Scanning.AobScanRange right) -> bool
+static CheatEngine.Client.Scanning.AobScanRange.operator ==(CheatEngine.Client.Scanning.AobScanRange left, CheatEngine.Client.Scanning.AobScanRange right) -> bool
+static CheatEngine.Client.Scanning.AobScanRequest.operator !=(CheatEngine.Client.Scanning.AobScanRequest left, CheatEngine.Client.Scanning.AobScanRequest right) -> bool
+static CheatEngine.Client.Scanning.AobScanRequest.operator ==(CheatEngine.Client.Scanning.AobScanRequest left, CheatEngine.Client.Scanning.AobScanRequest right) -> bool
+static CheatEngine.Client.Scanning.AobScanResult.operator !=(CheatEngine.Client.Scanning.AobScanResult left, CheatEngine.Client.Scanning.AobScanResult right) -> bool
+static CheatEngine.Client.Scanning.AobScanResult.operator ==(CheatEngine.Client.Scanning.AobScanResult left, CheatEngine.Client.Scanning.AobScanResult right) -> bool
+static CheatEngine.Client.Scanning.ValueScanMatch.operator !=(CheatEngine.Client.Scanning.ValueScanMatch left, CheatEngine.Client.Scanning.ValueScanMatch right) -> bool
+static CheatEngine.Client.Scanning.ValueScanMatch.operator ==(CheatEngine.Client.Scanning.ValueScanMatch left, CheatEngine.Client.Scanning.ValueScanMatch right) -> bool
+static CheatEngine.Client.Scanning.ValueScanPage.operator !=(CheatEngine.Client.Scanning.ValueScanPage left, CheatEngine.Client.Scanning.ValueScanPage right) -> bool
+static CheatEngine.Client.Scanning.ValueScanPage.operator ==(CheatEngine.Client.Scanning.ValueScanPage left, CheatEngine.Client.Scanning.ValueScanPage right) -> bool
+static CheatEngine.Client.Scanning.ValueScanReadRequest.operator !=(CheatEngine.Client.Scanning.ValueScanReadRequest left, CheatEngine.Client.Scanning.ValueScanReadRequest right) -> bool
+static CheatEngine.Client.Scanning.ValueScanReadRequest.operator ==(CheatEngine.Client.Scanning.ValueScanReadRequest left, CheatEngine.Client.Scanning.ValueScanReadRequest right) -> bool
+static CheatEngine.Client.Tables.AddressTableSnapshot.operator !=(CheatEngine.Client.Tables.AddressTableSnapshot left, CheatEngine.Client.Tables.AddressTableSnapshot right) -> bool
+static CheatEngine.Client.Tables.AddressTableSnapshot.operator ==(CheatEngine.Client.Tables.AddressTableSnapshot left, CheatEngine.Client.Tables.AddressTableSnapshot right) -> bool
+static CheatEngine.Client.Tables.MemoryRecordCollectionRequest.operator !=(CheatEngine.Client.Tables.MemoryRecordCollectionRequest left, CheatEngine.Client.Tables.MemoryRecordCollectionRequest right) -> bool
+static CheatEngine.Client.Tables.MemoryRecordCollectionRequest.operator ==(CheatEngine.Client.Tables.MemoryRecordCollectionRequest left, CheatEngine.Client.Tables.MemoryRecordCollectionRequest right) -> bool
+static CheatEngine.Client.Tables.MemoryRecordDefinition.operator !=(CheatEngine.Client.Tables.MemoryRecordDefinition left, CheatEngine.Client.Tables.MemoryRecordDefinition right) -> bool
+static CheatEngine.Client.Tables.MemoryRecordDefinition.operator ==(CheatEngine.Client.Tables.MemoryRecordDefinition left, CheatEngine.Client.Tables.MemoryRecordDefinition right) -> bool
+static CheatEngine.Client.Tables.MemoryRecordHierarchyRequest.operator !=(CheatEngine.Client.Tables.MemoryRecordHierarchyRequest left, CheatEngine.Client.Tables.MemoryRecordHierarchyRequest right) -> bool
+static CheatEngine.Client.Tables.MemoryRecordHierarchyRequest.operator ==(CheatEngine.Client.Tables.MemoryRecordHierarchyRequest left, CheatEngine.Client.Tables.MemoryRecordHierarchyRequest right) -> bool
+static CheatEngine.Client.Tables.MemoryRecordHierarchySnapshot.operator !=(CheatEngine.Client.Tables.MemoryRecordHierarchySnapshot left, CheatEngine.Client.Tables.MemoryRecordHierarchySnapshot right) -> bool
+static CheatEngine.Client.Tables.MemoryRecordHierarchySnapshot.operator ==(CheatEngine.Client.Tables.MemoryRecordHierarchySnapshot left, CheatEngine.Client.Tables.MemoryRecordHierarchySnapshot right) -> bool
+static CheatEngine.Client.Tables.MemoryRecordSearch.operator !=(CheatEngine.Client.Tables.MemoryRecordSearch left, CheatEngine.Client.Tables.MemoryRecordSearch right) -> bool
+static CheatEngine.Client.Tables.MemoryRecordSearch.operator ==(CheatEngine.Client.Tables.MemoryRecordSearch left, CheatEngine.Client.Tables.MemoryRecordSearch right) -> bool
+static CheatEngine.Client.Tables.MemoryRecordContentSnapshot.operator !=(CheatEngine.Client.Tables.MemoryRecordContentSnapshot left, CheatEngine.Client.Tables.MemoryRecordContentSnapshot right) -> bool
+static CheatEngine.Client.Tables.MemoryRecordContentSnapshot.operator ==(CheatEngine.Client.Tables.MemoryRecordContentSnapshot left, CheatEngine.Client.Tables.MemoryRecordContentSnapshot right) -> bool
+static CheatEngine.Client.Tables.MemoryRecordSnapshot.operator !=(CheatEngine.Client.Tables.MemoryRecordSnapshot left, CheatEngine.Client.Tables.MemoryRecordSnapshot right) -> bool
+static CheatEngine.Client.Tables.MemoryRecordSnapshot.operator ==(CheatEngine.Client.Tables.MemoryRecordSnapshot left, CheatEngine.Client.Tables.MemoryRecordSnapshot right) -> bool
+static CheatEngine.Client.Tables.MemoryRecordStateSnapshot.operator !=(CheatEngine.Client.Tables.MemoryRecordStateSnapshot left, CheatEngine.Client.Tables.MemoryRecordStateSnapshot right) -> bool
+static CheatEngine.Client.Tables.MemoryRecordStateSnapshot.operator ==(CheatEngine.Client.Tables.MemoryRecordStateSnapshot left, CheatEngine.Client.Tables.MemoryRecordStateSnapshot right) -> bool
+static CheatEngine.Client.Tables.MemoryRecordUpdate.operator !=(CheatEngine.Client.Tables.MemoryRecordUpdate left, CheatEngine.Client.Tables.MemoryRecordUpdate right) -> bool
+static CheatEngine.Client.Tables.MemoryRecordUpdate.operator ==(CheatEngine.Client.Tables.MemoryRecordUpdate left, CheatEngine.Client.Tables.MemoryRecordUpdate right) -> bool
+static CheatEngine.Client.Tables.TableLoadRequest.operator !=(CheatEngine.Client.Tables.TableLoadRequest left, CheatEngine.Client.Tables.TableLoadRequest right) -> bool
+static CheatEngine.Client.Tables.TableLoadRequest.operator ==(CheatEngine.Client.Tables.TableLoadRequest left, CheatEngine.Client.Tables.TableLoadRequest right) -> bool
+static CheatEngine.Client.Tables.TableSaveRequest.operator !=(CheatEngine.Client.Tables.TableSaveRequest left, CheatEngine.Client.Tables.TableSaveRequest right) -> bool
+static CheatEngine.Client.Tables.TableSaveRequest.operator ==(CheatEngine.Client.Tables.TableSaveRequest left, CheatEngine.Client.Tables.TableSaveRequest right) -> bool
+static CheatEngine.Client.Tables.TrustedTableFile.operator !=(CheatEngine.Client.Tables.TrustedTableFile left, CheatEngine.Client.Tables.TrustedTableFile right) -> bool
+static CheatEngine.Client.Tables.TrustedTableFile.operator ==(CheatEngine.Client.Tables.TrustedTableFile left, CheatEngine.Client.Tables.TrustedTableFile right) -> bool
diff --git a/libs/CheatEngine.Client.Abstractions/PublicAPI.Unshipped.txt b/libs/CheatEngine.Client.Abstractions/PublicAPI.Unshipped.txt
new file mode 100644
index 0000000..7dc5c58
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/PublicAPI.Unshipped.txt
@@ -0,0 +1 @@
+#nullable enable
diff --git a/libs/CheatEngine.Client.Abstractions/README.md b/libs/CheatEngine.Client.Abstractions/README.md
index 1cac69f..499cbcc 100644
--- a/libs/CheatEngine.Client.Abstractions/README.md
+++ b/libs/CheatEngine.Client.Abstractions/README.md
@@ -1,9 +1,93 @@
# CheatEngine.Client.Abstractions
-The base layer of CheatEngine.Client: the strongly-typed public vocabulary (contracts, value types, enums, options,
-error types) and the contracts that separate the fluent surface from the binding layer.
+## Context
-## Rules
+`CheatEngine.Client.Abstractions` is the stable, handle-free vocabulary of the in-process
+`CheatEngine.Client` plugin API. It targets C# 14 and .NET 10 on top of
+[CheatEngine.SDK](https://www.nuget.org/packages/CheatEngine.SDK), and is valid only for the
+current Cheat Engine plugin activation.
-- References no other project. Everything above depends on it, nothing below it exists.
-- Public API: what lives here is what consumers and the other layers agree on.
+It defines the public contracts used by the product facade, implementations, fluent helpers,
+hosting, extensions, and application code. It is intentionally synchronous: an attached Cheat
+Engine Lua runtime and its main-thread work must not be retained across an `await` boundary.
+
+## Why This Project Exists
+
+The SDK correctly exposes the Cheat Engine runtime, Lua bridge, ownership wrappers, and ABI-level
+concepts. Application code should not need to carry those handles through its own architecture.
+This project establishes a smaller product boundary with explicit failure, ownership, lifetime,
+and materialization rules.
+
+It is the bottom of the Client project graph: it has no Client project reference. Its only package
+dependency is the `CheatEngine.SDK` compile/runtime surface required for stable value and query
+types such as `Address`, target identifiers, inspection metadata, scan requests, and address-list
+identifiers. SDK build assets, generators, native assets, and analyzers deliberately do not flow
+through this package.
+
+```text
+CheatEngine.Client.Abstractions
+ ↑ ↑
+ Fluent Core
+ ↑
+ DependencyInjection / Hosting
+```
+
+## How It Improves CheatEngine.Client
+
+- Makes domain behavior testable against interfaces instead of Cheat Engine statics.
+- Keeps expected failures explicit through `Try...(..., out CheatEngineFailure)` and throws
+ `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.
+- Makes lifecycle constraints visible: `ICheatEngineClient.Epoch`, `Stopping`, leases, and scan
+ sessions are activation-scoped; stale resources report `CheatEngineActivationExpiredException`.
+
+## Public Surface
+
+Package and assembly names are not consumer namespaces. Public code belongs to functional
+namespaces only:
+
+| Namespace | Responsibility |
+|------------------------------|-----------------------------------------------------------------------|
+| `CheatEngine.Client` | `ICheatEngineClient`, the activation-scoped facade |
+| `.Dispatching` / `.Runtime` | main-thread dispatch and runtime/capability observations |
+| `.Processes` / `.Inspection` | target selection, copied process/module/region/symbol data |
+| `.Memory` | bounded primitive, byte, string, codec, and pointer-chain operations |
+| `.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 |
+| `.Results` | classified expected failures and lifecycle exceptions |
+
+No public consumer should use `CheatEngine.Client.Abstractions` as a namespace.
+
+### Capability Boundary
+
+The contracts describe runtime, process, memory, inspection, AOB scanning, tables, protected Lua,
+and explicitly disposable value-scan sessions. A contract is not an availability promise: callers
+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.
+
+`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.
+
+## Contribution and Validation
+
+Changes here are public API changes. Keep request/value types immutable, preserve functional
+namespaces, add XML documentation, update `PublicAPI.Unshipped.txt`, and add focused contract
+tests in `tests/CheatEngine.Client.Abstractions.Tests` before moving an entry to
+`PublicAPI.Shipped.txt`.
+
+From the repository root, validate the complete package graph:
+
+```powershell
+dotnet restore CheatEngine.Client.slnx --locked-mode
+dotnet build CheatEngine.Client.slnx --configuration Release --no-restore
+dotnet test --solution CheatEngine.Client.slnx --configuration Release --no-build --no-restore
+```
diff --git a/libs/CheatEngine.Client.Abstractions/Results/CheatEngineActivationExpiredException.cs b/libs/CheatEngine.Client.Abstractions/Results/CheatEngineActivationExpiredException.cs
new file mode 100644
index 0000000..8466e60
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Results/CheatEngineActivationExpiredException.cs
@@ -0,0 +1,11 @@
+namespace CheatEngine.Client.Results;
+
+/// Thrown when a resource is used after its plugin activation epoch has ended.
+public sealed class CheatEngineActivationExpiredException : CheatEngineClientException
+{
+ /// Creates an expired-activation exception for the attempted operation.
+ public CheatEngineActivationExpiredException(string operation, string message, Exception? innerException = null)
+ : base(new CheatEngineFailure(CheatEngineFailureKind.ActivationExpired, operation, message, innerException))
+ {
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Results/CheatEngineClientException.cs b/libs/CheatEngine.Client.Abstractions/Results/CheatEngineClientException.cs
new file mode 100644
index 0000000..777cb35
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Results/CheatEngineClientException.cs
@@ -0,0 +1,18 @@
+namespace CheatEngine.Client.Results;
+
+/// Base exception for an invalid use of the Cheat Engine client contract.
+public class CheatEngineClientException : Exception
+{
+ /// Creates an exception from a classified client failure.
+ public CheatEngineClientException(CheatEngineFailure failure)
+ : base(failure.Message, failure.Exception)
+ {
+ Failure = failure;
+ }
+
+ /// Gets the failure that caused the exception.
+ public CheatEngineFailure Failure
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Results/CheatEngineClientLifecycleException.cs b/libs/CheatEngine.Client.Abstractions/Results/CheatEngineClientLifecycleException.cs
new file mode 100644
index 0000000..a935b8b
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Results/CheatEngineClientLifecycleException.cs
@@ -0,0 +1,11 @@
+namespace CheatEngine.Client.Results;
+
+/// Thrown when code uses a client, session, or handle outside its active plugin lifecycle.
+public sealed class CheatEngineClientLifecycleException : CheatEngineClientException
+{
+ /// Creates a lifecycle exception with the operation that was attempted.
+ public CheatEngineClientLifecycleException(string operation, string message, Exception? innerException = null)
+ : base(new CheatEngineFailure(CheatEngineFailureKind.InvalidState, operation, message, innerException))
+ {
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Results/CheatEngineFailure.cs b/libs/CheatEngine.Client.Abstractions/Results/CheatEngineFailure.cs
new file mode 100644
index 0000000..1dfe257
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Results/CheatEngineFailure.cs
@@ -0,0 +1,57 @@
+namespace CheatEngine.Client.Results;
+
+/// An immutable description of an expected Cheat Engine operation failure.
+public readonly record struct CheatEngineFailure
+{
+ /// Creates a failure while retaining an optional SDK exception for diagnostics.
+ public CheatEngineFailure(CheatEngineFailureKind kind, string operation, string message,
+ Exception? exception = null)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(operation);
+ ArgumentException.ThrowIfNullOrWhiteSpace(message);
+ Kind = kind;
+ Operation = operation;
+ Message = message;
+ Exception = exception;
+ }
+
+ /// Gets the stable failure category.
+ public CheatEngineFailureKind Kind
+ {
+ get;
+ }
+
+ /// Gets the client operation that failed.
+ public string Operation
+ {
+ get;
+ }
+
+ /// Gets a human-readable diagnostic message.
+ public string Message
+ {
+ get;
+ }
+
+ /// Gets the originating SDK exception when one exists.
+ public Exception? Exception
+ {
+ get;
+ }
+
+ /// Throws this failure as an operation exception.
+ public readonly void Throw()
+ {
+ if (Kind == CheatEngineFailureKind.ActivationExpired)
+ {
+ throw new CheatEngineActivationExpiredException(Operation, Message, Exception);
+ }
+
+ if (Kind == CheatEngineFailureKind.InvalidState)
+ {
+ throw new CheatEngineClientLifecycleException(Operation, Message, Exception);
+ }
+
+ throw new CheatEngineOperationException(this);
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Results/CheatEngineFailureKind.cs b/libs/CheatEngine.Client.Abstractions/Results/CheatEngineFailureKind.cs
new file mode 100644
index 0000000..6d0bef9
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Results/CheatEngineFailureKind.cs
@@ -0,0 +1,53 @@
+namespace CheatEngine.Client.Results;
+
+/// Classifies a failure observed while executing a Cheat Engine client operation.
+public enum CheatEngineFailureKind
+{
+ /// The operation could not be classified more precisely.
+ Unknown = 0,
+
+ /// The operation was cancelled before a Cheat Engine call began.
+ Cancelled = 1,
+
+ /// The required Cheat Engine capability or Lua global is unavailable.
+ CapabilityUnavailable = 2,
+
+ /// Cheat Engine rejected the requested operation.
+ OperationRejected = 3,
+
+ /// The requested resource was not found.
+ NotFound = 4,
+
+ /// The operation expected one result but observed several.
+ AmbiguousMatch = 5,
+
+ /// The host result exceeded the caller's materialization limit.
+ ResultLimitExceeded = 6,
+
+ /// A protected Lua call failed.
+ LuaError = 7,
+
+ /// The SDK binding could not uphold its documented contract.
+ BindingError = 8,
+
+ /// The host returned a value outside its documented result shape.
+ InvalidHostResult = 9,
+
+ /// The requested feature is intentionally not supported by this client version.
+ Unsupported = 10,
+
+ /// No target process is currently attached.
+ TargetNotAttached = 11,
+
+ /// A target-memory read failed.
+ MemoryReadFailed = 12,
+
+ /// A target-memory write failed.
+ MemoryWriteFailed = 13,
+
+ /// The client resource belongs to an expired activation epoch.
+ ActivationExpired = 14,
+
+ /// The operation is not valid in the resource's current lifecycle or session state.
+ InvalidState = 15
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Results/CheatEngineOperationException.cs b/libs/CheatEngine.Client.Abstractions/Results/CheatEngineOperationException.cs
new file mode 100644
index 0000000..c1902da
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Results/CheatEngineOperationException.cs
@@ -0,0 +1,11 @@
+namespace CheatEngine.Client.Results;
+
+/// Thrown when a throwing convenience operation observes an expected Cheat Engine failure.
+public sealed class CheatEngineOperationException : CheatEngineClientException
+{
+ /// Creates an operation exception from a classified failure.
+ public CheatEngineOperationException(CheatEngineFailure failure)
+ : base(failure)
+ {
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Runtime/CheatEngineRuntimePlatformInfo.cs b/libs/CheatEngine.Client.Abstractions/Runtime/CheatEngineRuntimePlatformInfo.cs
new file mode 100644
index 0000000..f107d25
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Runtime/CheatEngineRuntimePlatformInfo.cs
@@ -0,0 +1,56 @@
+using CheatEngine.SDK.Engine.Runtime;
+
+namespace CheatEngine.Client.Runtime;
+
+/// Immutable platform observations captured during one active Cheat Engine activation.
+public readonly record struct CheatEngineRuntimePlatformInfo
+{
+ /// Creates runtime platform observations from copied host facts.
+ ///
+ /// does not match the width implied by
+ /// .
+ ///
+ public CheatEngineRuntimePlatformInfo(
+ CheatEngineArchitecture systemArchitecture,
+ CheatEngineArchitecture targetArchitecture,
+ PointerSize targetPointerSize,
+ TargetAbi targetAbi)
+ {
+ PointerSize expectedTargetPointerSize = PointerSize.FromArchitecture(targetArchitecture);
+ if (targetPointerSize != expectedTargetPointerSize)
+ {
+ throw new ArgumentException(
+ "The target pointer size must match the target architecture.",
+ nameof(targetPointerSize));
+ }
+
+ SystemArchitecture = systemArchitecture;
+ TargetArchitecture = targetArchitecture;
+ TargetPointerSize = targetPointerSize;
+ TargetAbi = targetAbi;
+ }
+
+ /// Gets the CE host architecture observed from CE's system-architecture global.
+ public CheatEngineArchitecture SystemArchitecture
+ {
+ get;
+ }
+
+ /// Gets the target architecture observed by a target-specific probe, or unknown.
+ public CheatEngineArchitecture TargetArchitecture
+ {
+ get;
+ }
+
+ /// Gets the pointer width implied by the observed target architecture, or unknown.
+ public PointerSize TargetPointerSize
+ {
+ get;
+ }
+
+ /// Gets the target ABI observed from CE's ABI global, or unknown.
+ public TargetAbi TargetAbi
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Runtime/CheatEngineRuntimeSnapshot.cs b/libs/CheatEngine.Client.Abstractions/Runtime/CheatEngineRuntimeSnapshot.cs
new file mode 100644
index 0000000..0995fee
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Runtime/CheatEngineRuntimeSnapshot.cs
@@ -0,0 +1,97 @@
+using CheatEngine.SDK.Engine.Runtime;
+
+namespace CheatEngine.Client.Runtime;
+
+/// Immutable runtime observations captured during one active Cheat Engine activation.
+///
+/// is the coarse number returned by Cheat Engine's
+/// getCEVersion global. It is deliberately separate from
+/// , because that global cannot establish a complete four-part file
+/// version.
+///
+public readonly record struct CheatEngineRuntimeSnapshot
+{
+ /// Creates a runtime snapshot from grouped version, platform, and capability observations.
+ /// is negative.
+ ///
+ /// is uninitialized, or a capability collection is .
+ ///
+ public CheatEngineRuntimeSnapshot(
+ long epoch,
+ CheatEngineRuntimeVersionInfo versionInfo,
+ CheatEngineRuntimePlatformInfo platformInfo,
+ RuntimeCapabilities sdkCapabilities,
+ ClientCapabilities clientCapabilities)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(epoch);
+ ArgumentNullException.ThrowIfNull(versionInfo.ClientAssemblyVersion);
+ ArgumentNullException.ThrowIfNull(versionInfo.SdkAssemblyVersion);
+
+ Epoch = epoch;
+ Version = versionInfo;
+ Platform = platformInfo;
+ SdkCapabilities = sdkCapabilities ?? throw new ArgumentNullException(nameof(sdkCapabilities));
+ ClientCapabilities = clientCapabilities ?? throw new ArgumentNullException(nameof(clientCapabilities));
+ }
+
+ /// Gets the current plugin activation epoch.
+ public long Epoch
+ {
+ get;
+ }
+
+ /// Gets the grouped version observations captured for this activation.
+ public CheatEngineRuntimeVersionInfo Version
+ {
+ get;
+ }
+
+ /// Gets the grouped platform observations captured for this activation.
+ public CheatEngineRuntimePlatformInfo Platform
+ {
+ get;
+ }
+
+ /// Gets the coarse number returned by CE's getCEVersion global, when it was callable.
+ public double? ObservedCheatEngineVersion => Version.ObservedCheatEngineVersion;
+
+ /// Gets the complete CE build against which this Client release was qualified.
+ public CheatEngineVersion QualifiedCheatEngineBaseline => Version.QualifiedCheatEngineBaseline;
+
+ /// Gets the assembly version of this Client abstraction assembly.
+ public Version ClientAssemblyVersion => Version.ClientAssemblyVersion;
+
+ /// Gets the assembly version of the SDK runtime-contract assembly.
+ public Version SdkAssemblyVersion => Version.SdkAssemblyVersion;
+
+ /// Gets the CE host architecture observed from CE's system-architecture global.
+ public CheatEngineArchitecture SystemArchitecture => Platform.SystemArchitecture;
+
+ /// Gets the target architecture observed by a target-specific probe, or unknown.
+ public CheatEngineArchitecture TargetArchitecture => Platform.TargetArchitecture;
+
+ /// Gets the pointer width implied by the observed target architecture, or unknown.
+ public PointerSize TargetPointerSize => Platform.TargetPointerSize;
+
+ /// Gets the target ABI observed from CE's ABI global, or unknown.
+ public TargetAbi TargetAbi => Platform.TargetAbi;
+
+ /// Gets the explicit availability observation for each SDK runtime capability that was probed.
+ public RuntimeCapabilities SdkCapabilities
+ {
+ get;
+ }
+
+ /// Gets the explicit availability observation for each Client high-level capability.
+ public ClientCapabilities ClientCapabilities
+ {
+ get;
+ }
+
+ /// Gets whether the observed coarse CE version belongs to the qualified major/minor line.
+ public bool IsOnQualifiedCheatEngineLine => ObservedCheatEngineVersion is { } observed &&
+ observed >= QualifiedCheatEngineBaseline.Major +
+ QualifiedCheatEngineBaseline.Minor / 10d &&
+ observed < QualifiedCheatEngineBaseline.Major +
+ (QualifiedCheatEngineBaseline.Minor + 1) / 10d;
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Runtime/CheatEngineRuntimeVersionInfo.cs b/libs/CheatEngine.Client.Abstractions/Runtime/CheatEngineRuntimeVersionInfo.cs
new file mode 100644
index 0000000..66c1791
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Runtime/CheatEngineRuntimeVersionInfo.cs
@@ -0,0 +1,51 @@
+using CheatEngine.SDK.Engine.Runtime;
+
+namespace CheatEngine.Client.Runtime;
+
+/// Immutable version observations captured during one active Cheat Engine activation.
+public readonly record struct CheatEngineRuntimeVersionInfo
+{
+ /// Creates runtime version observations from independently observed facts.
+ public CheatEngineRuntimeVersionInfo(
+ double? observedCheatEngineVersion,
+ CheatEngineVersion qualifiedCheatEngineBaseline,
+ Version clientAssemblyVersion,
+ Version sdkAssemblyVersion)
+ {
+ if (observedCheatEngineVersion is { } observed &&
+ (!double.IsFinite(observed) || observed < 0))
+ {
+ throw new ArgumentOutOfRangeException(nameof(observedCheatEngineVersion), observed,
+ "The observed Cheat Engine version must be a finite non-negative number when supplied.");
+ }
+
+ ObservedCheatEngineVersion = observedCheatEngineVersion;
+ QualifiedCheatEngineBaseline = qualifiedCheatEngineBaseline;
+ ClientAssemblyVersion = clientAssemblyVersion ?? throw new ArgumentNullException(nameof(clientAssemblyVersion));
+ SdkAssemblyVersion = sdkAssemblyVersion ?? throw new ArgumentNullException(nameof(sdkAssemblyVersion));
+ }
+
+ /// Gets the coarse number returned by CE's getCEVersion global, when it was callable.
+ public double? ObservedCheatEngineVersion
+ {
+ get;
+ }
+
+ /// Gets the complete CE build against which this Client release was qualified.
+ public CheatEngineVersion QualifiedCheatEngineBaseline
+ {
+ get;
+ }
+
+ /// Gets the assembly version of this Client abstraction assembly.
+ public Version ClientAssemblyVersion
+ {
+ get;
+ }
+
+ /// Gets the assembly version of the SDK runtime-contract assembly.
+ public Version SdkAssemblyVersion
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilities.cs b/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilities.cs
new file mode 100644
index 0000000..a3ff002
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilities.cs
@@ -0,0 +1,107 @@
+namespace CheatEngine.Client.Runtime;
+
+/// An immutable, allocation-free-to-enumerate collection of Client capability observations.
+public sealed class ClientCapabilities : IEquatable
+{
+ private readonly ClientCapabilityAvailability[] _entries;
+
+ private ClientCapabilities(ClientCapabilityAvailability[] entries)
+ {
+ _entries = entries;
+ }
+
+ /// Gets an empty capability collection.
+ public static ClientCapabilities Empty
+ {
+ get;
+ } = new([]);
+
+ /// Gets the number of explicit capability observations.
+ public int Count => _entries.Length;
+
+ /// Gets the ordered observations as a read-only span.
+ public ReadOnlySpan Entries => _entries;
+
+ ///
+ public bool Equals(ClientCapabilities? other)
+ {
+ if (ReferenceEquals(this, other))
+ {
+ return true;
+ }
+
+ if (other is null || _entries.Length != other._entries.Length)
+ {
+ return false;
+ }
+
+ for (int index = 0; index < _entries.Length; index++)
+ {
+ if (_entries[index] != other._entries[index])
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ ///
+ public override bool Equals(object? obj)
+ {
+ return obj is ClientCapabilities other && Equals(other);
+ }
+
+ /// Copies and validates a set of distinct Client capability observations.
+ public static ClientCapabilities Create(ReadOnlySpan entries)
+ {
+ if (entries.IsEmpty)
+ {
+ return Empty;
+ }
+
+ ClientCapabilityAvailability[] copy = entries.ToArray();
+ for (int index = 0; index < copy.Length; index++)
+ {
+ _ = new ClientCapabilityAvailability(copy[index].Capability, copy[index].State, copy[index].Reason);
+ for (int previous = 0; previous < index; previous++)
+ {
+ if (copy[previous].Capability == copy[index].Capability)
+ {
+ throw new ArgumentException("A Client capability identifier occurs more than once.",
+ nameof(entries));
+ }
+ }
+ }
+
+ return new ClientCapabilities(copy);
+ }
+
+ /// Tries to get one explicit capability observation.
+ public bool TryGet(ClientCapabilityId capability, out ClientCapabilityAvailability availability)
+ {
+ for (int index = 0; index < _entries.Length; index++)
+ {
+ if (_entries[index].Capability == capability)
+ {
+ availability = _entries[index];
+ return true;
+ }
+ }
+
+ availability = default;
+ return false;
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ HashCode hash = new();
+ for (int index = 0; index < _entries.Length; index++)
+ {
+ hash.Add(_entries[index]);
+ }
+
+ return hash.ToHashCode();
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityAvailability.cs b/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityAvailability.cs
new file mode 100644
index 0000000..1a75db2
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityAvailability.cs
@@ -0,0 +1,58 @@
+using System.Runtime.InteropServices;
+
+namespace CheatEngine.Client.Runtime;
+
+/// An immutable observation of a Client-owned high-level capability and its evidence.
+[StructLayout(LayoutKind.Sequential)]
+public readonly record struct ClientCapabilityAvailability
+{
+ /// Creates an observation after validating its stable public shape.
+ /// The stable Client-owned capability identifier.
+ /// The observed availability state.
+ /// The explicit probe, policy, or gate reason behind the state.
+ public ClientCapabilityAvailability(
+ ClientCapabilityId capability,
+ ClientCapabilityAvailabilityState state,
+ string reason)
+ {
+ if (capability.IsEmpty)
+ {
+ throw new ArgumentException("A Client capability identifier is required.", nameof(capability));
+ }
+
+ if (!Enum.IsDefined(state))
+ {
+ throw new ArgumentOutOfRangeException(nameof(state), state, "The Client capability state is not defined.");
+ }
+
+ ArgumentException.ThrowIfNullOrWhiteSpace(reason);
+
+ Capability = capability;
+ State = state;
+ Reason = reason;
+ }
+
+ /// Gets the stable Client-owned capability identifier.
+ public ClientCapabilityId Capability
+ {
+ get;
+ }
+
+ /// Gets the observed availability state.
+ public ClientCapabilityAvailabilityState State
+ {
+ get;
+ }
+
+ /// Gets the explicit probe, policy, or gate reason behind the state.
+ public string Reason
+ {
+ get;
+ }
+
+ /// Gets whether this capability was explicitly established as available.
+ public bool IsAvailable => State == ClientCapabilityAvailabilityState.Available;
+
+ /// Gets whether this capability was explicitly established as available or unavailable.
+ public bool IsKnown => State != ClientCapabilityAvailabilityState.Unknown;
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityAvailabilityState.cs b/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityAvailabilityState.cs
new file mode 100644
index 0000000..922c236
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityAvailabilityState.cs
@@ -0,0 +1,14 @@
+namespace CheatEngine.Client.Runtime;
+
+/// Describes the observed availability of a Client-owned high-level capability.
+public enum ClientCapabilityAvailabilityState : byte
+{
+ /// The Client has not established every prerequisite for the capability.
+ Unknown = 0,
+
+ /// The Client has established that the capability is available for this activation.
+ Available = 1,
+
+ /// The Client explicitly does not offer the capability for this activation.
+ Unavailable = 2
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityId.cs b/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityId.cs
new file mode 100644
index 0000000..93c4614
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityId.cs
@@ -0,0 +1,80 @@
+namespace CheatEngine.Client.Runtime;
+
+/// A stable, Client-owned identifier for one high-level Cheat Engine capability.
+public readonly struct ClientCapabilityId : IEquatable
+{
+ private readonly string? _value;
+
+ /// Creates a non-empty Client capability identifier.
+ public ClientCapabilityId(string value)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(value);
+ _value = value;
+ }
+
+ /// Gets the identifier value, or an empty string for the default value.
+ public string Value => _value ?? string.Empty;
+
+ /// Gets whether this is the default, unusable identifier.
+ public bool IsEmpty => _value is null;
+
+ /// Gets the Client capability for process selection and attachment.
+ public static ClientCapabilityId ProcessSelection => new("Client.ProcessSelection");
+
+ /// Gets the Client capability for typed target-memory operations.
+ public static ClientCapabilityId TypedMemory => new("Client.TypedMemory");
+
+ /// Gets the Client capability for AOB pattern scanning.
+ public static ClientCapabilityId PatternScanning => new("Client.PatternScanning");
+
+ /// Gets the Client capability for value scan sessions.
+ public static ClientCapabilityId ValueScanning => new("Client.ValueScanning");
+
+ /// Gets the Client capability for copied inspection snapshots and symbol operations.
+ public static ClientCapabilityId Inspection => new("Client.Inspection");
+
+ /// Gets the Client capability for Address List and trusted table operations.
+ public static ClientCapabilityId Tables => new("Client.Tables");
+
+ /// Gets the Client capability for protected typed Lua operations.
+ public static ClientCapabilityId ProtectedLua => new("Client.ProtectedLua");
+
+ /// Gets the Client capability for explicitly opted-in arbitrary Lua source execution.
+ public static ClientCapabilityId UnsafeLuaExecution => new("Client.UnsafeLuaExecution");
+
+ ///
+ public bool Equals(ClientCapabilityId other)
+ {
+ return string.Equals(_value, other._value, StringComparison.Ordinal);
+ }
+
+ ///
+ public override bool Equals(object? obj)
+ {
+ return obj is ClientCapabilityId other && Equals(other);
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ return _value is null ? 0 : StringComparer.Ordinal.GetHashCode(_value);
+ }
+
+ ///
+ public override string ToString()
+ {
+ return Value;
+ }
+
+ /// Tests two identifiers for ordinal equality.
+ public static bool operator ==(ClientCapabilityId left, ClientCapabilityId right)
+ {
+ return left.Equals(right);
+ }
+
+ /// Tests two identifiers for ordinal inequality.
+ public static bool operator !=(ClientCapabilityId left, ClientCapabilityId right)
+ {
+ return !left.Equals(right);
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Runtime/ICheatEngineRuntime.cs b/libs/CheatEngine.Client.Abstractions/Runtime/ICheatEngineRuntime.cs
new file mode 100644
index 0000000..29451d8
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Runtime/ICheatEngineRuntime.cs
@@ -0,0 +1,47 @@
+using CheatEngine.Client.Results;
+using CheatEngine.SDK.Engine.Runtime;
+
+namespace CheatEngine.Client.Runtime;
+
+/// Reads immutable runtime facts and capability observations for the active Cheat Engine activation.
+public interface ICheatEngineRuntime
+{
+ /// Gets the activation epoch for which this runtime service is valid.
+ public long Epoch
+ {
+ get;
+ }
+
+ /// Tries to capture the runtime facts available to the active plugin.
+ public bool TryGetSnapshot(
+ out CheatEngineRuntimeSnapshot snapshot,
+ out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Captures the runtime facts or throws when they are unavailable.
+ public CheatEngineRuntimeSnapshot GetSnapshot(CancellationToken cancellationToken = default);
+
+ /// Reads one SDK capability observation without exposing its backing Lua global.
+ public bool TryGetSdkCapability(
+ RuntimeCapabilityId capability,
+ out RuntimeCapabilityAvailability availability,
+ out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Reads one SDK capability observation or throws when it cannot be observed.
+ public RuntimeCapabilityAvailability GetSdkCapability(
+ RuntimeCapabilityId capability,
+ CancellationToken cancellationToken = default);
+
+ /// Reads one Client capability observation without exposing internal adapters or handles.
+ public bool TryGetClientCapability(
+ ClientCapabilityId capability,
+ out ClientCapabilityAvailability availability,
+ out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Reads one Client capability observation or throws when it cannot be observed.
+ public ClientCapabilityAvailability GetClientCapability(
+ ClientCapabilityId capability,
+ CancellationToken cancellationToken = default);
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Scanning/AobPattern.cs b/libs/CheatEngine.Client.Abstractions/Scanning/AobPattern.cs
new file mode 100644
index 0000000..c8ac7c8
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Scanning/AobPattern.cs
@@ -0,0 +1,186 @@
+using System.Text;
+
+namespace CheatEngine.Client.Scanning;
+
+/// An immutable, normalized sequence of Cheat Engine AOB byte and wildcard tokens.
+///
+///
+/// The client deliberately accepts the documented, portable subset of string-form AOBScan: hexadecimal
+/// bytes and ?? wildcards. It canonicalizes hexadecimal digits to upper case and emits exactly one ASCII
+/// space between tokens. This prevents an invalid token from reaching Cheat Engine while retaining CE as the
+/// authority for matching semantics.
+///
+///
+/// The byte-array overload of CE's Lua function has broader numeric coercion rules. It is not used by this
+/// string-based client surface, so numeric values above 255 and non-integral values are intentionally not
+/// accepted here.
+///
+///
+public readonly record struct AobPattern
+{
+ /// Creates and normalizes an AOB pattern.
+ /// Hexadecimal byte tokens and ?? wildcard tokens.
+ /// is .
+ /// is empty or has an invalid token.
+ public AobPattern(string value)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+ if (!TryNormalize(value.AsSpan(), out string normalized, out int byteLength))
+ {
+ throw new ArgumentException(
+ "An AOB pattern must contain only two-digit hexadecimal bytes or ?? wildcard tokens.", nameof(value));
+ }
+
+ Value = normalized;
+ ByteLength = byteLength;
+ }
+
+ private AobPattern(string normalized, int byteLength)
+ {
+ Value = normalized;
+ ByteLength = byteLength;
+ }
+
+ /// Gets the normalized Cheat Engine pattern text.
+ public string Value
+ {
+ get;
+ }
+
+ /// Gets the number of byte positions represented by the pattern.
+ public int ByteLength
+ {
+ get;
+ }
+
+ /// Gets whether every byte position is a wildcard.
+ public bool IsWildcardOnly
+ {
+ get
+ {
+ if (string.IsNullOrEmpty(Value))
+ {
+ return false;
+ }
+
+ for (int index = 0; index < Value.Length; index += 3)
+ {
+ if (Value[index] != '?' || Value[index + 1] != '?')
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+
+ /// Attempts to parse and normalize an AOB pattern without throwing for malformed text.
+ /// The candidate pattern text, or .
+ /// The normalized pattern when this method returns .
+ /// when is a valid documented string-form AOB pattern.
+ public static bool TryParse(string? value, out AobPattern pattern)
+ {
+ if (value is not null && TryNormalize(value.AsSpan(), out string normalized, out int byteLength))
+ {
+ pattern = new AobPattern(normalized, byteLength);
+ return true;
+ }
+
+ pattern = default;
+ return false;
+ }
+
+ /// Formats the pattern as normalized Cheat Engine text.
+ public override string ToString()
+ {
+ return Value ?? string.Empty;
+ }
+
+ private static bool TryNormalize(ReadOnlySpan value, out string normalized, out int byteLength)
+ {
+ StringBuilder builder = new(value.Length);
+ int index = 0;
+ byteLength = 0;
+
+ while (TryMoveToNextToken(value, ref index))
+ {
+ if (!TryReadNormalizedToken(value, ref index, out char high, out char low))
+ {
+ normalized = string.Empty;
+ byteLength = 0;
+ return false;
+ }
+
+ AppendToken(builder, high, low);
+ byteLength++;
+ }
+
+ if (byteLength != 0)
+ {
+ normalized = builder.ToString();
+ return true;
+ }
+
+ normalized = string.Empty;
+ return false;
+ }
+
+ private static bool TryMoveToNextToken(ReadOnlySpan value, ref int index)
+ {
+ while (index < value.Length && char.IsWhiteSpace(value[index]))
+ {
+ index++;
+ }
+
+ return index < value.Length;
+ }
+
+ private static bool TryReadNormalizedToken(ReadOnlySpan value, ref int index, out char high, out char low)
+ {
+ if (index + 1 >= value.Length || char.IsWhiteSpace(value[index + 1]))
+ {
+ high = default;
+ low = default;
+ return false;
+ }
+
+ high = value[index++];
+ low = value[index++];
+ if (high == '?' && low == '?')
+ {
+ return true;
+ }
+
+ return TryGetHexDigit(high, out high) && TryGetHexDigit(low, out low);
+ }
+
+ private static void AppendToken(StringBuilder builder, char high, char low)
+ {
+ if (builder.Length != 0)
+ {
+ builder.Append(' ');
+ }
+
+ builder.Append(high);
+ builder.Append(low);
+ }
+
+ private static bool TryGetHexDigit(char value, out char normalized)
+ {
+ if (value is >= '0' and <= '9' or >= 'A' and <= 'F')
+ {
+ normalized = value;
+ return true;
+ }
+
+ if (value is >= 'a' and <= 'f')
+ {
+ normalized = (char) (value - ('a' - 'A'));
+ return true;
+ }
+
+ normalized = default;
+ return false;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Scanning/AobScanOptionsNormalizer.cs b/libs/CheatEngine.Client.Abstractions/Scanning/AobScanOptionsNormalizer.cs
new file mode 100644
index 0000000..0364356
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Scanning/AobScanOptionsNormalizer.cs
@@ -0,0 +1,155 @@
+using System.Globalization;
+
+using CheatEngine.SDK.Engine.Enums;
+using CheatEngine.SDK.Engine.Scanning.Aob;
+
+namespace CheatEngine.Client.Scanning;
+
+/// Validates the documented CE AOB protection and alignment text before an SDK call.
+internal static class AobScanOptionsNormalizer
+{
+ internal static AobScanOptions Normalize(AobScanOptions options)
+ {
+ string? protection = NormalizeProtection(options.ProtectionFlags);
+ string? alignment = NormalizeAlignmentParameter(options.AlignmentMethod, options.AlignmentParameter);
+ return new AobScanOptions(protection, options.AlignmentMethod, alignment);
+ }
+
+ private static string? NormalizeProtection(string? value)
+ {
+ if (value is null || value.Length == 0)
+ {
+ return value;
+ }
+
+ Span modes = stackalloc char[3];
+ int seen = 0;
+ for (int index = 0; index < value.Length;)
+ {
+ AddProtectionClause(value, ref index, modes, ref seen);
+ }
+
+ return FormatProtection(modes, seen);
+ }
+
+ private static void AddProtectionClause(string value, ref int index, Span modes, ref int seen)
+ {
+ if (index + 1 >= value.Length)
+ {
+ throw new ArgumentException(
+ "An AOB protection expression consists of +, -, or * followed by X, C, or W.", nameof(value));
+ }
+
+ char mode = value[index++];
+ char flag = value[index++];
+ if (mode is not ('+' or '-' or '*'))
+ {
+ throw new ArgumentException(
+ "An AOB protection expression consists of unique +, -, or * X/C/W clauses.", nameof(value));
+ }
+
+ int slot = GetProtectionSlot(flag);
+ if (slot < 0 || IsProtectionSlotPresent(seen, slot))
+ {
+ throw new ArgumentException(
+ "An AOB protection expression consists of unique +, -, or * X/C/W clauses.", nameof(value));
+ }
+
+ modes[slot] = mode;
+ seen |= 1 << slot;
+ }
+
+ private static string FormatProtection(ReadOnlySpan modes, int seen)
+ {
+ Span normalized = stackalloc char[6];
+ int written = 0;
+ for (int slot = 0; slot < modes.Length; slot++)
+ {
+ if (!IsProtectionSlotPresent(seen, slot))
+ {
+ continue;
+ }
+
+ normalized[written++] = modes[slot];
+ normalized[written++] = GetProtectionFlag(slot);
+ }
+
+ return new string(normalized[..written]);
+ }
+
+ private static int GetProtectionSlot(char flag)
+ {
+ return flag switch
+ {
+ 'X' or 'x' => 0,
+ 'C' or 'c' => 1,
+ 'W' or 'w' => 2,
+ _ => -1
+ };
+ }
+
+ private static char GetProtectionFlag(int slot)
+ {
+ return slot switch { 0 => 'X', 1 => 'C', _ => 'W' };
+ }
+
+ private static bool IsProtectionSlotPresent(int seen, int slot)
+ {
+ return (seen & (1 << slot)) != 0;
+ }
+
+ private static string? NormalizeAlignmentParameter(FastScanMethod method, string? value)
+ {
+ return method switch
+ {
+ FastScanMethod.NotAligned => value,
+ FastScanMethod.Aligned => NormalizeDivisor(value),
+ FastScanMethod.LastDigits => NormalizeHexSuffix(value),
+ _ => throw new ArgumentOutOfRangeException(nameof(method), method,
+ "AOB alignment must use a documented Cheat Engine fast-scan method.")
+ };
+ }
+
+ private static string NormalizeDivisor(string? value)
+ {
+ if (string.IsNullOrEmpty(value) ||
+ !ulong.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out ulong divisor) || divisor == 0)
+ {
+ throw new ArgumentException(
+ "An aligned AOB scan requires a positive decimal alignment divisor.", nameof(value));
+ }
+
+ return divisor.ToString(CultureInfo.InvariantCulture);
+ }
+
+ private static string NormalizeHexSuffix(string? value)
+ {
+ if (string.IsNullOrEmpty(value) || value.Length > sizeof(ulong) * 2)
+ {
+ throw new ArgumentException(
+ "A last-digits AOB scan requires one to sixteen hexadecimal digits.", nameof(value));
+ }
+
+ Span normalized = stackalloc char[value.Length];
+ for (int index = 0; index < value.Length; index++)
+ {
+ char character = value[index];
+ if (character is >= '0' and <= '9' or >= 'A' and <= 'F')
+ {
+ normalized[index] = character;
+ continue;
+ }
+
+ if (character is >= 'a' and <= 'f')
+ {
+ normalized[index] = (char) (character - ('a' - 'A'));
+ continue;
+ }
+
+ throw new ArgumentException(
+ "A last-digits AOB scan requires one to sixteen hexadecimal digits.", nameof(value));
+ }
+
+ return new string(normalized);
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Scanning/AobScanRange.cs b/libs/CheatEngine.Client.Abstractions/Scanning/AobScanRange.cs
new file mode 100644
index 0000000..6690fed
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Scanning/AobScanRange.cs
@@ -0,0 +1,46 @@
+using CheatEngine.SDK.Engine.Values;
+
+namespace CheatEngine.Client.Scanning;
+
+/// An inclusive target-address range used to filter copied AOB match addresses.
+///
+/// String-form AOBScan does not accept start and stop address arguments. Core therefore applies this filter
+/// after copying each matching address from the SDK-owned result list and before it contributes to the caller's
+/// materialization limit.
+///
+public readonly record struct AobScanRange
+{
+ /// Creates an inclusive target-address range.
+ /// The first included address.
+ /// The last included address.
+ /// precedes .
+ public AobScanRange(Address start, Address end)
+ {
+ if (end < start)
+ {
+ throw new ArgumentOutOfRangeException(nameof(end), end,
+ "An AOB range end address must not precede its start address.");
+ }
+
+ Start = start;
+ End = end;
+ }
+
+ /// Gets the first included target address.
+ public Address Start
+ {
+ get;
+ }
+
+ /// Gets the last included target address.
+ public Address End
+ {
+ get;
+ }
+
+ /// Gets whether is inside this inclusive range.
+ public bool Contains(Address address)
+ {
+ return address >= Start && address <= End;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Scanning/AobScanRequest.cs b/libs/CheatEngine.Client.Abstractions/Scanning/AobScanRequest.cs
new file mode 100644
index 0000000..fe0cce5
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Scanning/AobScanRequest.cs
@@ -0,0 +1,60 @@
+using CheatEngine.SDK.Engine.Inspection;
+using CheatEngine.SDK.Engine.Scanning.Aob;
+
+namespace CheatEngine.Client.Scanning;
+
+/// An immutable AOB scan request with an explicit managed materialization limit.
+public readonly record struct AobScanRequest
+{
+ /// Creates an AOB scan request.
+ public AobScanRequest(AobPattern pattern, AobScanOptions options, int maximumResults, ModuleName? module = null,
+ AobScanRange? range = null)
+ {
+ if (string.IsNullOrWhiteSpace(pattern.Value))
+ {
+ throw new ArgumentException("An AOB pattern must be non-empty.", nameof(pattern));
+ }
+
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maximumResults);
+ if (module.HasValue && string.IsNullOrWhiteSpace(module.Value.Value))
+ {
+ throw new ArgumentException("An AOB module filter must be non-empty.", nameof(module));
+ }
+
+ Pattern = pattern;
+ Options = AobScanOptionsNormalizer.Normalize(options);
+ MaximumResults = maximumResults;
+ Module = module;
+ Range = range;
+ }
+
+ /// Gets the scan pattern.
+ public AobPattern Pattern
+ {
+ get;
+ }
+
+ /// Gets the SDK's evidence-backed optional scan arguments.
+ public AobScanOptions Options
+ {
+ get;
+ }
+
+ /// Gets the maximum number of managed matches the caller permits.
+ public int MaximumResults
+ {
+ get;
+ }
+
+ /// Gets the optional module whose copied address range filters the results.
+ public ModuleName? Module
+ {
+ get;
+ }
+
+ /// Gets the optional inclusive range that filters copied address results.
+ public AobScanRange? Range
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Scanning/AobScanResult.cs b/libs/CheatEngine.Client.Abstractions/Scanning/AobScanResult.cs
new file mode 100644
index 0000000..09826fc
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Scanning/AobScanResult.cs
@@ -0,0 +1,34 @@
+using System.Collections.Immutable;
+
+using CheatEngine.SDK.Engine.Values;
+
+namespace CheatEngine.Client.Scanning;
+
+/// A copied, handle-free AOB scan result.
+public readonly record struct AobScanResult
+{
+ /// Creates a copied AOB scan result.
+ public AobScanResult(ImmutableArray matches, bool isTruncated)
+ {
+ Matches = matches.IsDefault ? ImmutableArray.Empty : matches;
+ if (isTruncated && Matches.IsEmpty)
+ {
+ throw new ArgumentException("A truncated AOB result must retain at least one copied match.",
+ nameof(matches));
+ }
+
+ IsTruncated = isTruncated;
+ }
+
+ /// Gets the materialized target addresses.
+ public ImmutableArray Matches
+ {
+ get;
+ }
+
+ /// Gets whether additional CE matches were omitted because the request limit was reached.
+ public bool IsTruncated
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Scanning/IPatternScanner.cs b/libs/CheatEngine.Client.Abstractions/Scanning/IPatternScanner.cs
new file mode 100644
index 0000000..23da161
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Scanning/IPatternScanner.cs
@@ -0,0 +1,14 @@
+using CheatEngine.Client.Results;
+
+namespace CheatEngine.Client.Scanning;
+
+/// Runs bounded AOB scans and copies all returned addresses before releasing SDK-owned objects.
+public interface IPatternScanner
+{
+ /// Tries to run one bounded pattern scan.
+ public bool TryScan(AobScanRequest request, out AobScanResult result, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Runs one bounded pattern scan or throws when the operation fails.
+ public AobScanResult Scan(AobScanRequest request, CancellationToken cancellationToken = default);
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Scanning/IValueScanSession.cs b/libs/CheatEngine.Client.Abstractions/Scanning/IValueScanSession.cs
new file mode 100644
index 0000000..e7f60ae
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Scanning/IValueScanSession.cs
@@ -0,0 +1,54 @@
+using CheatEngine.Client.Results;
+using CheatEngine.SDK.Engine.Scanning.Values;
+
+namespace CheatEngine.Client.Scanning;
+
+/// A main-thread-bound, explicitly disposable high-level value-scan session.
+///
+/// The session exposes a Client-managed rather than the SDK's raw scan state. Its
+/// operations complete Cheat Engine's required wait-and-initialize sequence before publishing copied results, and
+/// never
+/// expose MemScan, FoundList, Lua, or ownership wrappers.
+///
+public interface IValueScanSession : IDisposable
+{
+ /// Gets the session's current conservative scan state.
+ public ValueScanSessionState State
+ {
+ get;
+ }
+
+ /// Runs a first scan and prepares its results for reading.
+ public bool TryStart(FirstScanRequest request, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Runs a first scan or throws when it fails.
+ public void Start(FirstScanRequest request, CancellationToken cancellationToken = default);
+
+ /// Runs a next scan and prepares its results for reading.
+ public bool TryRunNextScan(NextScanRequest request, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Runs a next scan or throws when it fails.
+ public void RunNextScan(NextScanRequest request, CancellationToken cancellationToken = default);
+
+ /// Resets the scan session to its initial state.
+ public bool TryReset(out CheatEngineFailure failure, CancellationToken cancellationToken = default);
+
+ /// Resets the scan session or throws when it fails.
+ public void Reset(CancellationToken cancellationToken = default);
+
+ /// Gets the current result count after results are ready.
+ public bool TryGetResultCount(out ulong resultCount, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Gets the current result count or throws when results are not ready.
+ public ulong GetResultCount(CancellationToken cancellationToken = default);
+
+ /// Copies a bounded page of current results after results are ready.
+ public bool TryRead(ValueScanReadRequest request, out ValueScanPage page, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Copies a bounded page or throws when the session is not ready.
+ public ValueScanPage Read(ValueScanReadRequest request, CancellationToken cancellationToken = default);
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Scanning/IValueScanner.cs b/libs/CheatEngine.Client.Abstractions/Scanning/IValueScanner.cs
new file mode 100644
index 0000000..0960dd0
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Scanning/IValueScanner.cs
@@ -0,0 +1,14 @@
+using CheatEngine.Client.Results;
+
+namespace CheatEngine.Client.Scanning;
+
+/// Creates explicitly owned value-scan sessions.
+public interface IValueScanner
+{
+ /// Tries to create a value-scan session for the current activation.
+ public bool TryCreateSession(out IValueScanSession? session, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Creates a value-scan session or throws when the host capability is unavailable.
+ public IValueScanSession CreateSession(CancellationToken cancellationToken = default);
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Scanning/ValueScanMatch.cs b/libs/CheatEngine.Client.Abstractions/Scanning/ValueScanMatch.cs
new file mode 100644
index 0000000..b87698b
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Scanning/ValueScanMatch.cs
@@ -0,0 +1,35 @@
+using CheatEngine.SDK.Engine.Values;
+
+namespace CheatEngine.Client.Scanning;
+
+/// A copied value-scan match; it owns no FoundList or other CE resource.
+public readonly record struct ValueScanMatch
+{
+ /// Creates a copied value-scan match.
+ public ValueScanMatch(int index, Address address, string value)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(index);
+ ArgumentNullException.ThrowIfNull(value);
+ Index = index;
+ Address = address;
+ Value = value;
+ }
+
+ /// Gets the zero-based index in the CE found list.
+ public int Index
+ {
+ get;
+ }
+
+ /// Gets the parsed target address.
+ public Address Address
+ {
+ get;
+ }
+
+ /// Gets the verbatim value text returned by Cheat Engine.
+ public string Value
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Scanning/ValueScanPage.cs b/libs/CheatEngine.Client.Abstractions/Scanning/ValueScanPage.cs
new file mode 100644
index 0000000..402ffdd
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Scanning/ValueScanPage.cs
@@ -0,0 +1,26 @@
+using System.Collections.Immutable;
+
+namespace CheatEngine.Client.Scanning;
+
+/// An immutable page of copied value-scan results.
+public readonly record struct ValueScanPage
+{
+ /// Creates a value-scan result page.
+ public ValueScanPage(ulong totalCount, ImmutableArray matches)
+ {
+ TotalCount = totalCount;
+ Matches = matches.IsDefault ? ImmutableArray.Empty : matches;
+ }
+
+ /// Gets the total CE result count observed while reading this page.
+ public ulong TotalCount
+ {
+ get;
+ }
+
+ /// Gets the copied result matches.
+ public ImmutableArray Matches
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Scanning/ValueScanReadRequest.cs b/libs/CheatEngine.Client.Abstractions/Scanning/ValueScanReadRequest.cs
new file mode 100644
index 0000000..aa76e9e
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Scanning/ValueScanReadRequest.cs
@@ -0,0 +1,26 @@
+namespace CheatEngine.Client.Scanning;
+
+/// Describes a bounded, zero-based value-scan result read.
+public readonly record struct ValueScanReadRequest
+{
+ /// Creates a bounded result-read request.
+ public ValueScanReadRequest(int startIndex, int maximumCount)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(startIndex);
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maximumCount);
+ StartIndex = startIndex;
+ MaximumCount = maximumCount;
+ }
+
+ /// Gets the first zero-based result index to read.
+ public int StartIndex
+ {
+ get;
+ }
+
+ /// Gets the maximum number of copied matches to materialize.
+ public int MaximumCount
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Scanning/ValueScanSessionState.cs b/libs/CheatEngine.Client.Abstractions/Scanning/ValueScanSessionState.cs
new file mode 100644
index 0000000..97535e1
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Scanning/ValueScanSessionState.cs
@@ -0,0 +1,33 @@
+namespace CheatEngine.Client.Scanning;
+
+/// Describes the conservative Client-managed lifecycle of one value-scan session.
+///
+///
+/// The successful path is , , then
+/// . A subsequent scan transitions from through
+/// again. No SDK object or Lua handle is represented by this value.
+///
+///
+/// is deliberately conservative. It means a Cheat Engine operation began but failed
+/// before
+/// the Client could establish a safe next state; the session must be reset or disposed rather than reused
+/// speculatively.
+///
+///
+public enum ValueScanSessionState
+{
+ /// The session was created and has no readable result set.
+ Created = 0,
+
+ /// A first or subsequent scan was accepted and has not yet completed.
+ Scanning = 1,
+
+ /// The scan completed and its copied-result view is available for bounded reads.
+ ResultsReady = 2,
+
+ /// A started operation did not establish a safe continuation state.
+ Invalidated = 3,
+
+ /// The session has released its Client-owned resources and accepts no further operation.
+ Disposed = 4
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Tables/AddressTableSnapshot.cs b/libs/CheatEngine.Client.Abstractions/Tables/AddressTableSnapshot.cs
new file mode 100644
index 0000000..2a6e124
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Tables/AddressTableSnapshot.cs
@@ -0,0 +1,38 @@
+using System.Collections.Immutable;
+
+namespace CheatEngine.Client.Tables;
+
+/// A copied snapshot of Cheat Engine's current address-list cardinality.
+public readonly record struct AddressTableSnapshot
+{
+ /// Creates an address-table snapshot.
+ public AddressTableSnapshot(int recordCount)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(recordCount);
+ RecordCount = recordCount;
+ Records = ImmutableArray.Empty;
+ }
+
+ /// Creates a bounded copied snapshot of every top-level record in the address list.
+ public AddressTableSnapshot(ImmutableArray records)
+ {
+ Records = records.IsDefault ? ImmutableArray.Empty : records;
+ RecordCount = Records.Length;
+ }
+
+ /// Gets the number of top-level records observed in the current address list.
+ public int RecordCount
+ {
+ get;
+ }
+
+ /// Gets the copied top-level records when this snapshot was explicitly materialized.
+ ///
+ /// A cardinality-only value returned by has an empty collection. Use
+ /// to request records with an explicit materialization limit.
+ ///
+ public ImmutableArray Records
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Tables/ITableClient.cs b/libs/CheatEngine.Client.Abstractions/Tables/ITableClient.cs
new file mode 100644
index 0000000..b8849f9
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Tables/ITableClient.cs
@@ -0,0 +1,125 @@
+using System.Collections.Immutable;
+
+using CheatEngine.Client.Results;
+using CheatEngine.SDK.Engine.AddressList;
+
+namespace CheatEngine.Client.Tables;
+
+/// Reads and changes the current Cheat Engine address list through copied record snapshots.
+public interface ITableClient
+{
+ /// Tries to get a snapshot of the current address list.
+ public bool TryGetCurrent(out AddressTableSnapshot table, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Gets the current table or throws when it is unavailable.
+ public AddressTableSnapshot GetCurrent(CancellationToken cancellationToken = default);
+
+ /// Copies every top-level record only when the caller supplies a materialization limit.
+ public bool TryGetSnapshot(MemoryRecordCollectionRequest request, out AddressTableSnapshot table,
+ out CheatEngineFailure failure, CancellationToken cancellationToken = default);
+
+ /// Copies a bounded top-level record snapshot or throws when the table exceeds the limit.
+ public AddressTableSnapshot GetSnapshot(MemoryRecordCollectionRequest request,
+ CancellationToken cancellationToken = default);
+
+ /// Searches a bounded copied top-level record snapshot with conjunctive managed predicates.
+ public bool TryFind(MemoryRecordSearch search, MemoryRecordCollectionRequest request,
+ out ImmutableArray records, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Searches a bounded top-level record snapshot or throws when it cannot be materialized.
+ public ImmutableArray Find(MemoryRecordSearch search, MemoryRecordCollectionRequest request,
+ CancellationToken cancellationToken = default);
+
+ /// Gets one record by its current zero-based address-list index.
+ public bool TryGetRecord(int index, out MemoryRecordSnapshot record, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Gets one record by its stable Cheat Engine identifier.
+ public bool TryGetRecord(MemoryRecordId id, out MemoryRecordSnapshot record, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Gets one record by index or throws when it is unavailable.
+ public MemoryRecordSnapshot GetRecord(int index, CancellationToken cancellationToken = default);
+
+ /// Gets one record by identifier or throws when it is unavailable.
+ public MemoryRecordSnapshot GetRecord(MemoryRecordId id, CancellationToken cancellationToken = default);
+
+ /// Copies the selected address-list record when Cheat Engine has one.
+ public bool TryGetSelected(out MemoryRecordSnapshot record, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Gets the selected record or throws when Cheat Engine has no selection.
+ public MemoryRecordSnapshot GetSelected(CancellationToken cancellationToken = default);
+
+ /// Selects one record by its stable identifier and returns its copied snapshot.
+ public bool TrySelect(MemoryRecordId id, out MemoryRecordSnapshot record, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Selects one record or throws when the record is unavailable.
+ public MemoryRecordSnapshot SelectRecord(MemoryRecordId id, CancellationToken cancellationToken = default);
+
+ /// Creates a memory record, assigns its defined fields, and returns a copied snapshot.
+ public bool TryCreate(MemoryRecordDefinition definition, out MemoryRecordSnapshot record,
+ out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Creates a record or throws when the host rejects it.
+ public MemoryRecordSnapshot Create(MemoryRecordDefinition definition,
+ CancellationToken cancellationToken = default);
+
+ /// Applies a partial update and returns a copied snapshot of the changed record.
+ public bool TryUpdate(MemoryRecordUpdate update, out MemoryRecordSnapshot record,
+ out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Updates a record or throws when the host rejects the change.
+ public MemoryRecordSnapshot Update(MemoryRecordUpdate update, CancellationToken cancellationToken = default);
+
+ /// Deletes one memory record from the current Cheat Engine address list.
+ public bool TryDelete(MemoryRecordId id, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Deletes one memory record or throws when Cheat Engine rejects the operation.
+ public void Delete(MemoryRecordId id, CancellationToken cancellationToken = default);
+
+ /// Activates or deactivates one memory record and returns its copied post-change snapshot.
+ public bool TrySetActive(MemoryRecordId id, bool isActive, out MemoryRecordSnapshot record,
+ out CheatEngineFailure failure, CancellationToken cancellationToken = default);
+
+ /// Activates or deactivates one memory record or throws when Cheat Engine rejects the change.
+ public MemoryRecordSnapshot SetActive(MemoryRecordId id, bool isActive,
+ CancellationToken cancellationToken = default);
+
+ /// Moves one record under a parent, or passes to restore it to the root.
+ public bool TrySetParent(MemoryRecordId childId, MemoryRecordId? parentId, out MemoryRecordSnapshot record,
+ out CheatEngineFailure failure, CancellationToken cancellationToken = default);
+
+ /// Moves one record under a parent, or restores it to the root, and returns its copied post-change state.
+ public MemoryRecordSnapshot SetParent(MemoryRecordId childId, MemoryRecordId? parentId,
+ CancellationToken cancellationToken = default);
+
+ /// Copies a bounded hierarchy rooted at one memory record.
+ public bool TryGetHierarchy(MemoryRecordId rootId, MemoryRecordHierarchyRequest request,
+ out MemoryRecordHierarchySnapshot hierarchy, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Copies a bounded hierarchy or throws when its depth, cardinality, or host shape is invalid.
+ public MemoryRecordHierarchySnapshot GetHierarchy(MemoryRecordId rootId, MemoryRecordHierarchyRequest request,
+ CancellationToken cancellationToken = default);
+
+ /// Tries to load a trusted table through Cheat Engine's native table loader.
+ public bool TryLoadTrustedTable(TableLoadRequest request, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Loads a trusted table or throws when policy or host execution rejects it.
+ public void LoadTrustedTable(TableLoadRequest request, CancellationToken cancellationToken = default);
+
+ /// Tries to save the current table to an explicitly allowed path.
+ public bool TrySaveTable(TableSaveRequest request, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default);
+
+ /// Saves the current table or throws when policy or host execution rejects it.
+ public void SaveTable(TableSaveRequest request, CancellationToken cancellationToken = default);
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordCollectionRequest.cs b/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordCollectionRequest.cs
new file mode 100644
index 0000000..98b57f8
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordCollectionRequest.cs
@@ -0,0 +1,18 @@
+namespace CheatEngine.Client.Tables;
+
+/// Bounds copied top-level or child memory records materialized from the Cheat Engine address list.
+public readonly record struct MemoryRecordCollectionRequest
+{
+ /// Creates a bounded record materialization request.
+ public MemoryRecordCollectionRequest(int maximumItems)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maximumItems);
+ MaximumItems = maximumItems;
+ }
+
+ /// Gets the maximum number of record snapshots the caller permits.
+ public int MaximumItems
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordContentSnapshot.cs b/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordContentSnapshot.cs
new file mode 100644
index 0000000..afced03
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordContentSnapshot.cs
@@ -0,0 +1,45 @@
+using CheatEngine.SDK.Engine.Enums;
+
+namespace CheatEngine.Client.Tables;
+
+/// Copied content fields of a Cheat Engine memory record.
+public readonly record struct MemoryRecordContentSnapshot
+{
+ /// Creates copied content fields for a memory-record snapshot.
+ public MemoryRecordContentSnapshot(string description, string addressExpression, string value,
+ VariableType variableType)
+ {
+ ArgumentNullException.ThrowIfNull(description);
+ ArgumentNullException.ThrowIfNull(addressExpression);
+ ArgumentNullException.ThrowIfNull(value);
+
+ Description = description;
+ AddressExpression = addressExpression;
+ Value = value;
+ VariableType = variableType;
+ }
+
+ /// Gets the record display description.
+ public string Description
+ {
+ get;
+ }
+
+ /// Gets the record's unresolved Cheat Engine address expression.
+ public string AddressExpression
+ {
+ get;
+ }
+
+ /// Gets the record's verbatim value text.
+ public string Value
+ {
+ get;
+ }
+
+ /// Gets the record's Cheat Engine value type.
+ public VariableType VariableType
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordDefinition.cs b/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordDefinition.cs
new file mode 100644
index 0000000..ecd7aff
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordDefinition.cs
@@ -0,0 +1,54 @@
+using CheatEngine.SDK.Engine.AddressList;
+using CheatEngine.SDK.Engine.Enums;
+
+namespace CheatEngine.Client.Tables;
+
+/// The initial managed fields for a newly created Cheat Engine memory record.
+public readonly record struct MemoryRecordDefinition
+{
+ /// Creates a memory-record definition.
+ public MemoryRecordDefinition(string description, string addressExpression, string value, VariableType variableType,
+ MemoryRecordId? parentId = null)
+ {
+ ArgumentNullException.ThrowIfNull(description);
+ ArgumentException.ThrowIfNullOrWhiteSpace(addressExpression);
+ ArgumentNullException.ThrowIfNull(value);
+
+ Description = description;
+ AddressExpression = addressExpression;
+ Value = value;
+ VariableType = variableType;
+ ParentId = parentId;
+ }
+
+ /// Gets the display description to assign.
+ public string Description
+ {
+ get;
+ }
+
+ /// Gets the CE address expression to assign.
+ public string AddressExpression
+ {
+ get;
+ }
+
+ /// Gets the CE string value to assign.
+ public string Value
+ {
+ get;
+ }
+
+ /// Gets the CE value type to assign.
+ public VariableType VariableType
+ {
+ get;
+ }
+
+ /// Gets the optional parent record to assign after the record fields have been initialized.
+ /// A value creates the record at the address-list root.
+ public MemoryRecordId? ParentId
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordHierarchyRequest.cs b/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordHierarchyRequest.cs
new file mode 100644
index 0000000..875e991
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordHierarchyRequest.cs
@@ -0,0 +1,27 @@
+namespace CheatEngine.Client.Tables;
+
+/// Bounds a copied memory-record hierarchy traversal.
+public readonly record struct MemoryRecordHierarchyRequest
+{
+ /// Creates a bounded hierarchy materialization request.
+ public MemoryRecordHierarchyRequest(int maximumItems, int maximumDepth)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maximumItems);
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maximumDepth);
+
+ MaximumItems = maximumItems;
+ MaximumDepth = maximumDepth;
+ }
+
+ /// Gets the maximum number of snapshots, including the requested root.
+ public int MaximumItems
+ {
+ get;
+ }
+
+ /// Gets the maximum depth, where the requested root has depth one.
+ public int MaximumDepth
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordHierarchySnapshot.cs b/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordHierarchySnapshot.cs
new file mode 100644
index 0000000..0787f9f
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordHierarchySnapshot.cs
@@ -0,0 +1,39 @@
+using System.Collections.Immutable;
+
+namespace CheatEngine.Client.Tables;
+
+/// A copied, recursively bounded memory-record tree.
+public readonly record struct MemoryRecordHierarchySnapshot
+{
+ /// Creates a copied memory-record tree and normalizes unavailable child storage to empty.
+ public MemoryRecordHierarchySnapshot(
+ MemoryRecordSnapshot Record,
+ ImmutableArray Children)
+ {
+ this.Record = Record;
+ this.Children = Children;
+ }
+
+ /// Gets the copied root record.
+ public MemoryRecordSnapshot Record
+ {
+ get;
+ init;
+ }
+
+ /// Gets the copied child records; a default array is exposed as empty.
+ public ImmutableArray Children
+ {
+ get => field.IsDefault ? ImmutableArray.Empty : field;
+ init => field = value.IsDefault ? ImmutableArray.Empty : value;
+ }
+
+ /// Deconstructs the copied root record and normalized child records.
+ public void Deconstruct(
+ out MemoryRecordSnapshot Record,
+ out ImmutableArray Children)
+ {
+ Record = this.Record;
+ Children = this.Children;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordSearch.cs b/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordSearch.cs
new file mode 100644
index 0000000..a21e300
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordSearch.cs
@@ -0,0 +1,62 @@
+using CheatEngine.SDK.Engine.Enums;
+
+namespace CheatEngine.Client.Tables;
+
+/// Conjunctive managed predicates used to search a bounded address-list snapshot.
+public readonly record struct MemoryRecordSearch
+{
+ /// Creates a search that matches all supplied predicates.
+ public MemoryRecordSearch(
+ string? descriptionContains = null,
+ string? addressExpression = null,
+ VariableType? variableType = null,
+ bool? isActive = null)
+ {
+ if (descriptionContains is { Length: 0 })
+ {
+ throw new ArgumentException("A description search value must be null or non-empty.",
+ nameof(descriptionContains));
+ }
+
+ if (addressExpression is { Length: 0 })
+ {
+ throw new ArgumentException("An address expression search value must be null or non-empty.",
+ nameof(addressExpression));
+ }
+
+ if (descriptionContains is null && addressExpression is null && variableType is null && isActive is null)
+ {
+ throw new ArgumentException("A memory-record search must specify at least one predicate.",
+ nameof(descriptionContains));
+ }
+
+ DescriptionContains = descriptionContains;
+ AddressExpression = addressExpression;
+ VariableType = variableType;
+ IsActive = isActive;
+ }
+
+ /// Gets the case-insensitive description substring predicate.
+ public string? DescriptionContains
+ {
+ get;
+ }
+
+ /// Gets the case-insensitive exact address-expression predicate.
+ public string? AddressExpression
+ {
+ get;
+ }
+
+ /// Gets the optional exact Cheat Engine value-type predicate.
+ public VariableType? VariableType
+ {
+ get;
+ }
+
+ /// Gets the optional active/frozen state predicate.
+ public bool? IsActive
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordSnapshot.cs b/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordSnapshot.cs
new file mode 100644
index 0000000..e9ca221
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordSnapshot.cs
@@ -0,0 +1,75 @@
+using CheatEngine.SDK.Engine.AddressList;
+using CheatEngine.SDK.Engine.Enums;
+using CheatEngine.SDK.Engine.Values;
+
+namespace CheatEngine.Client.Tables;
+
+/// A copied, handle-free snapshot of a Cheat Engine memory record.
+public readonly record struct MemoryRecordSnapshot
+{
+ /// Creates a copied memory-record snapshot from grouped content and state fields.
+ /// is negative.
+ /// is uninitialized.
+ public MemoryRecordSnapshot(
+ MemoryRecordId id,
+ int index,
+ MemoryRecordContentSnapshot content,
+ MemoryRecordStateSnapshot state)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(index);
+ if (content.Description is null || content.AddressExpression is null || content.Value is null)
+ {
+ throw new ArgumentException("Content requires non-null text fields.", nameof(content));
+ }
+
+ Id = id;
+ Index = index;
+ Content = content;
+ State = state;
+ }
+
+ /// Gets the stable Cheat Engine record identifier.
+ public MemoryRecordId Id
+ {
+ get;
+ }
+
+ /// Gets the current zero-based address-list position.
+ public int Index
+ {
+ get;
+ }
+
+ /// Gets the copied content fields for this memory record.
+ public MemoryRecordContentSnapshot Content
+ {
+ get;
+ }
+
+ /// Gets the copied runtime state fields for this memory record.
+ public MemoryRecordStateSnapshot State
+ {
+ get;
+ }
+
+ /// Gets the record display description.
+ public string Description => Content.Description;
+
+ /// Gets the record's unresolved Cheat Engine address expression.
+ public string AddressExpression => Content.AddressExpression;
+
+ /// Gets the record's verbatim value text.
+ public string Value => Content.Value;
+
+ /// Gets the record's Cheat Engine value type.
+ public VariableType VariableType => Content.VariableType;
+
+ /// Gets the currently resolved target address when it could be obtained.
+ public Address? CurrentAddress => State.CurrentAddress;
+
+ /// Gets whether Cheat Engine reports this record as active or frozen.
+ public bool IsActive => State.IsActive;
+
+ /// Gets the number of immediate child records reported by Cheat Engine.
+ public int ChildCount => State.ChildCount;
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordStateSnapshot.cs b/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordStateSnapshot.cs
new file mode 100644
index 0000000..40d7768
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordStateSnapshot.cs
@@ -0,0 +1,35 @@
+using CheatEngine.SDK.Engine.Values;
+
+namespace CheatEngine.Client.Tables;
+
+/// Copied runtime state fields of a Cheat Engine memory record.
+public readonly record struct MemoryRecordStateSnapshot
+{
+ /// Creates copied runtime state fields for a memory-record snapshot.
+ public MemoryRecordStateSnapshot(Address? currentAddress, bool isActive = false, int childCount = 0)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(childCount);
+
+ CurrentAddress = currentAddress;
+ IsActive = isActive;
+ ChildCount = childCount;
+ }
+
+ /// Gets the currently resolved target address when it could be obtained.
+ public Address? CurrentAddress
+ {
+ get;
+ }
+
+ /// Gets whether Cheat Engine reports this record as active or frozen.
+ public bool IsActive
+ {
+ get;
+ }
+
+ /// Gets the number of immediate child records reported by Cheat Engine.
+ public int ChildCount
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordUpdate.cs b/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordUpdate.cs
new file mode 100644
index 0000000..2959c0e
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordUpdate.cs
@@ -0,0 +1,63 @@
+using CheatEngine.SDK.Engine.AddressList;
+using CheatEngine.SDK.Engine.Enums;
+
+namespace CheatEngine.Client.Tables;
+
+/// A partial change set for an existing Cheat Engine memory record.
+public readonly record struct MemoryRecordUpdate
+{
+ /// Creates a memory-record update containing at least one changed field.
+ public MemoryRecordUpdate(
+ MemoryRecordId id,
+ string? description = null,
+ string? addressExpression = null,
+ string? value = null,
+ VariableType? variableType = null)
+ {
+ if (description is null && addressExpression is null && value is null && variableType is null)
+ {
+ throw new ArgumentException("A memory-record update must change at least one field.", nameof(description));
+ }
+
+ if (addressExpression is { Length: 0 })
+ {
+ throw new ArgumentException("An address expression must be null or non-empty.", nameof(addressExpression));
+ }
+
+ Id = id;
+ Description = description;
+ AddressExpression = addressExpression;
+ Value = value;
+ VariableType = variableType;
+ }
+
+ /// Gets the identifier of the record to change.
+ public MemoryRecordId Id
+ {
+ get;
+ }
+
+ /// Gets the optional replacement description.
+ public string? Description
+ {
+ get;
+ }
+
+ /// Gets the optional replacement address expression.
+ public string? AddressExpression
+ {
+ get;
+ }
+
+ /// Gets the optional replacement value text.
+ public string? Value
+ {
+ get;
+ }
+
+ /// Gets the optional replacement value type.
+ public VariableType? VariableType
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/Tables/TableLoadRequest.cs b/libs/CheatEngine.Client.Abstractions/Tables/TableLoadRequest.cs
new file mode 100644
index 0000000..3b7192b
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Tables/TableLoadRequest.cs
@@ -0,0 +1,4 @@
+namespace CheatEngine.Client.Tables;
+
+/// Options for loading one explicitly trusted Cheat Engine table.
+public readonly record struct TableLoadRequest(TrustedTableFile File, bool Merge = false);
diff --git a/libs/CheatEngine.Client.Abstractions/Tables/TableSaveRequest.cs b/libs/CheatEngine.Client.Abstractions/Tables/TableSaveRequest.cs
new file mode 100644
index 0000000..3e0d459
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Tables/TableSaveRequest.cs
@@ -0,0 +1,4 @@
+namespace CheatEngine.Client.Tables;
+
+/// Options for saving the current Cheat Engine table without advanced signing or protection.
+public readonly record struct TableSaveRequest(TrustedTableFile File);
diff --git a/libs/CheatEngine.Client.Abstractions/Tables/TrustedTableFile.cs b/libs/CheatEngine.Client.Abstractions/Tables/TrustedTableFile.cs
new file mode 100644
index 0000000..b90aa54
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Tables/TrustedTableFile.cs
@@ -0,0 +1,23 @@
+namespace CheatEngine.Client.Tables;
+
+/// An explicitly trusted, normalized Cheat Engine table path.
+public readonly record struct TrustedTableFile
+{
+ /// Creates a trusted table path. Policy validation is still performed by the configured table client.
+ public TrustedTableFile(string path)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(path);
+ if (!Path.IsPathFullyQualified(path))
+ {
+ throw new ArgumentException("A trusted Cheat Engine table path must be absolute.", nameof(path));
+ }
+
+ FullPath = Path.GetFullPath(path);
+ }
+
+ /// Gets the normalized absolute path.
+ public string FullPath
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Abstractions/packages.lock.json b/libs/CheatEngine.Client.Abstractions/packages.lock.json
new file mode 100644
index 0000000..a256510
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/packages.lock.json
@@ -0,0 +1,54 @@
+{
+ "version": 2,
+ "dependencies": {
+ "net10.0": {
+ "CheatEngine.SDK": {
+ "type": "Direct",
+ "requested": "[1.0.0, 2.0.0)",
+ "resolved": "1.0.0",
+ "contentHash": "n7nHqZ8vzo7Vf20jF0fkh/jUtR3yo1TwRGpXE7ERxZeJ4C5S/Nsft4lqOg7zGwfsD5Nh9tTVgdw4PrybJRF0gA=="
+ },
+ "Microsoft.CodeAnalysis.PublicApiAnalyzers": {
+ "type": "Direct",
+ "requested": "[5.6.0, )",
+ "resolved": "5.6.0",
+ "contentHash": "W4kJGezNIKLzo0Ak5FAQDFvkMf2U7DtGL4THmHyRSApfKsKt5V+eX/bU0ZLKAt/uf9Bb2o1bi0YDKj/GRB/vYQ=="
+ },
+ "Microsoft.NET.ILLink.Tasks": {
+ "type": "Direct",
+ "requested": "[10.0.12, )",
+ "resolved": "10.0.12",
+ "contentHash": "xi+BDjFpW+Sb+MHFHaH6Y/gV9I8BluFwRXc1QyCdoZbIK26eNiBeFuMTe/FMwc33G1wdHCyDg7CVTmb8OdQrMQ=="
+ },
+ "Microsoft.SourceLink.GitHub": {
+ "type": "Direct",
+ "requested": "[10.0.401, )",
+ "resolved": "10.0.401",
+ "contentHash": "LGmlwgP1Cx37JEWzyjS0o1/+xs/s/e3E2TBSuogI5ePA/9L0pfIfeYX0k5in7Bfcw8Nn2y2sG9jXxydTjiR2Fg==",
+ "dependencies": {
+ "Microsoft.Build.Tasks.Git": "10.0.401",
+ "Microsoft.SourceLink.Common": "10.0.401",
+ "System.IO.Hashing": "10.0.12"
+ }
+ },
+ "Microsoft.Build.Tasks.Git": {
+ "type": "Transitive",
+ "resolved": "10.0.401",
+ "contentHash": "ZYctNuT10V9IYyCFydy63DXx0ggZQuynuzQOdLvW62dPgzjIz7f0ISEP75RGiq1jFQh8p6TmGSqxeQZQ87LCig==",
+ "dependencies": {
+ "System.IO.Hashing": "10.0.12"
+ }
+ },
+ "Microsoft.SourceLink.Common": {
+ "type": "Transitive",
+ "resolved": "10.0.401",
+ "contentHash": "u3rLxIwi/9MqDFaWGE/QQgLR1NBEzLOW2lv5+9OrZPDBYIAmFdYSWCWrR1ufpXWOqFn+x02TgKropl/oDuHmgA=="
+ },
+ "System.IO.Hashing": {
+ "type": "Transitive",
+ "resolved": "10.0.12",
+ "contentHash": "jDix4bBMYnpZdSPcnY+KDV6ik3SRMzpMKby/bZl/XUwIiflwRNAFZ0oOl61R/pSaveIJ8t1gs2BUlrGsPs/bcg=="
+ }
+ }
+ }
+}
diff --git a/libs/CheatEngine.Client.Binding/CheatEngine.Client.Binding.csproj b/libs/CheatEngine.Client.Binding/CheatEngine.Client.Binding.csproj
deleted file mode 100644
index 9a9b672..0000000
--- a/libs/CheatEngine.Client.Binding/CheatEngine.Client.Binding.csproj
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
diff --git a/libs/CheatEngine.Client.Binding/README.md b/libs/CheatEngine.Client.Binding/README.md
deleted file mode 100644
index 0480aa2..0000000
--- a/libs/CheatEngine.Client.Binding/README.md
+++ /dev/null
@@ -1,11 +0,0 @@
-# CheatEngine.Client.Binding
-
-The mapper/binder layer of CheatEngine.Client: it implements the contracts declared in `CheatEngine.Client.Abstractions`
-by mapping them onto CheatEngine.SDK.
-
-## Rules
-
-- References `CheatEngine.Client.Abstractions` only. It never references `CheatEngine.Client.Fluent`.
-- The only project meant to depend on CheatEngine.SDK. This is the default layering rule: revisit it if
- `CheatEngine.Client.Abstractions` ever reuses CheatEngine.SDK vocabulary.
-- Internal by default: only what `CheatEngine.Client` composes is public.
diff --git a/libs/CheatEngine.Client.Core/CheatEngine.Client.Core.csproj b/libs/CheatEngine.Client.Core/CheatEngine.Client.Core.csproj
new file mode 100644
index 0000000..cb7032f
--- /dev/null
+++ b/libs/CheatEngine.Client.Core/CheatEngine.Client.Core.csproj
@@ -0,0 +1,19 @@
+
+
+
+ true
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/libs/CheatEngine.Client.Core/CheatEngineClient.cs b/libs/CheatEngine.Client.Core/CheatEngineClient.cs
new file mode 100644
index 0000000..4ce4fc2
--- /dev/null
+++ b/libs/CheatEngine.Client.Core/CheatEngineClient.cs
@@ -0,0 +1,84 @@
+using CheatEngine.Client.Core.Infrastructure;
+using CheatEngine.Client.Dispatching;
+using CheatEngine.Client.Inspection;
+using CheatEngine.Client.Lua;
+using CheatEngine.Client.Memory;
+using CheatEngine.Client.Processes;
+using CheatEngine.Client.Runtime;
+using CheatEngine.Client.Scanning;
+using CheatEngine.Client.Tables;
+
+namespace CheatEngine.Client.Core;
+
+internal sealed class CheatEngineClient : ICheatEngineClient
+{
+ private readonly CoreLifetime _lifetime;
+
+ internal CheatEngineClient(
+ CoreLifetime lifetime,
+ CheatEngineClientRuntimeServices runtimeServices,
+ CheatEngineClientDomainServices domainServices)
+ {
+ _lifetime = lifetime ?? throw new ArgumentNullException(nameof(lifetime));
+ ArgumentNullException.ThrowIfNull(runtimeServices);
+ ArgumentNullException.ThrowIfNull(domainServices);
+
+ Runtime = runtimeServices.Runtime ?? throw new ArgumentNullException(nameof(runtimeServices));
+ Dispatcher = runtimeServices.Dispatcher ?? throw new ArgumentNullException(nameof(runtimeServices));
+ Processes = domainServices.Processes ?? throw new ArgumentNullException(nameof(domainServices));
+ Memory = domainServices.Memory ?? throw new ArgumentNullException(nameof(domainServices));
+ Patterns = domainServices.Patterns ?? throw new ArgumentNullException(nameof(domainServices));
+ Scans = domainServices.Scans ?? throw new ArgumentNullException(nameof(domainServices));
+ Inspection = domainServices.Inspection ?? throw new ArgumentNullException(nameof(domainServices));
+ Tables = domainServices.Tables ?? throw new ArgumentNullException(nameof(domainServices));
+ Lua = domainServices.Lua ?? throw new ArgumentNullException(nameof(domainServices));
+ }
+
+ public long Epoch => _lifetime.Epoch;
+ public CancellationToken Stopping => _lifetime.Stopping;
+
+ public ICheatEngineRuntime Runtime
+ {
+ get;
+ }
+
+ public ICheatEngineDispatcher Dispatcher
+ {
+ get;
+ }
+
+ public IProcessClient Processes
+ {
+ get;
+ }
+
+ public IMemoryClient Memory
+ {
+ get;
+ }
+
+ public IPatternScanner Patterns
+ {
+ get;
+ }
+
+ public IValueScanner Scans
+ {
+ get;
+ }
+
+ public IInspectionClient Inspection
+ {
+ get;
+ }
+
+ public ITableClient Tables
+ {
+ get;
+ }
+
+ public ILuaClient Lua
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Core/CheatEngineClientDomainServices.cs b/libs/CheatEngine.Client.Core/CheatEngineClientDomainServices.cs
new file mode 100644
index 0000000..7ce2156
--- /dev/null
+++ b/libs/CheatEngine.Client.Core/CheatEngineClientDomainServices.cs
@@ -0,0 +1,18 @@
+using CheatEngine.Client.Inspection;
+using CheatEngine.Client.Lua;
+using CheatEngine.Client.Memory;
+using CheatEngine.Client.Processes;
+using CheatEngine.Client.Scanning;
+using CheatEngine.Client.Tables;
+
+namespace CheatEngine.Client.Core;
+
+/// Internal grouping of the independently consumable high-level Client domains.
+internal sealed record CheatEngineClientDomainServices(
+ IProcessClient Processes,
+ IMemoryClient Memory,
+ IPatternScanner Patterns,
+ IValueScanner Scans,
+ IInspectionClient Inspection,
+ ITableClient Tables,
+ ILuaClient Lua);
diff --git a/libs/CheatEngine.Client.Core/CheatEngineClientRuntimeServices.cs b/libs/CheatEngine.Client.Core/CheatEngineClientRuntimeServices.cs
new file mode 100644
index 0000000..18bdb18
--- /dev/null
+++ b/libs/CheatEngine.Client.Core/CheatEngineClientRuntimeServices.cs
@@ -0,0 +1,9 @@
+using CheatEngine.Client.Dispatching;
+using CheatEngine.Client.Runtime;
+
+namespace CheatEngine.Client.Core;
+
+/// Internal grouping of the façade services that describe the active runtime and its dispatch boundary.
+internal sealed record CheatEngineClientRuntimeServices(
+ ICheatEngineRuntime Runtime,
+ ICheatEngineDispatcher Dispatcher);
diff --git a/libs/CheatEngine.Client.Core/Dispatching/IMainThreadInvoker.cs b/libs/CheatEngine.Client.Core/Dispatching/IMainThreadInvoker.cs
new file mode 100644
index 0000000..db33040
--- /dev/null
+++ b/libs/CheatEngine.Client.Core/Dispatching/IMainThreadInvoker.cs
@@ -0,0 +1,8 @@
+namespace CheatEngine.Client.Core.Dispatching;
+
+internal interface IMainThreadInvoker
+{
+ public Exception? Invoke(Action callback);
+
+ public MainThreadInvocationResult Invoke(Func callback);
+}
diff --git a/libs/CheatEngine.Client.Core/Dispatching/MainThreadInvocationResult.cs b/libs/CheatEngine.Client.Core/Dispatching/MainThreadInvocationResult.cs
new file mode 100644
index 0000000..dd31ef8
--- /dev/null
+++ b/libs/CheatEngine.Client.Core/Dispatching/MainThreadInvocationResult.cs
@@ -0,0 +1,3 @@
+namespace CheatEngine.Client.Core.Dispatching;
+
+internal readonly record struct MainThreadInvocationResult(T Result, Exception? Exception);
diff --git a/libs/CheatEngine.Client.Core/Dispatching/SdkMainThreadDispatcher.cs b/libs/CheatEngine.Client.Core/Dispatching/SdkMainThreadDispatcher.cs
new file mode 100644
index 0000000..b4395f5
--- /dev/null
+++ b/libs/CheatEngine.Client.Core/Dispatching/SdkMainThreadDispatcher.cs
@@ -0,0 +1,189 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.ExceptionServices;
+
+using CheatEngine.Client.Core.Infrastructure;
+using CheatEngine.Client.Dispatching;
+using CheatEngine.Client.Results;
+using CheatEngine.SDK.Hosting.Threading;
+
+namespace CheatEngine.Client.Core.Dispatching;
+
+/// Adapts the SDK's synchronous main-thread dispatcher without retaining Lua state.
+internal sealed class SdkMainThreadDispatcher : ICheatEngineDispatcher
+{
+ private const string _invokeOperation = "Dispatcher.Invoke";
+
+ private readonly CoreLifetime _lifetime;
+ private readonly IMainThreadInvoker _mainThread;
+
+ internal SdkMainThreadDispatcher(CoreLifetime lifetime)
+ : this(lifetime, SdkMainThreadInvoker.Instance)
+ {
+ }
+
+ internal SdkMainThreadDispatcher(CoreLifetime lifetime, IMainThreadInvoker mainThread)
+ {
+ _lifetime = lifetime ?? throw new ArgumentNullException(nameof(lifetime));
+ _mainThread = mainThread ?? throw new ArgumentNullException(nameof(mainThread));
+ }
+
+ public bool IsMainThread => _lifetime.CanDispatch && MainThread.IsMainThread;
+
+ public bool TryInvoke(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);
+ }
+ 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;
+ }
+
+ public bool TryInvoke(Func callback, [MaybeNullWhen(false)] out T 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);
+ }
+ catch (CheatEngineActivationExpiredException)
+ {
+ 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)
+ {
+ RethrowCallback(callbackResult.Exception);
+ }
+
+ result = callbackResult.Result;
+ failure = default;
+ return true;
+ }
+
+ public void Invoke(Action callback, CancellationToken cancellationToken = default)
+ {
+ if (!TryInvoke(callback, out CheatEngineFailure failure, cancellationToken))
+ {
+ _ = ThrowFailure