diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
deleted file mode 100644
index 4f66b91..0000000
--- a/.github/workflows/build.yml
+++ /dev/null
@@ -1,81 +0,0 @@
-name: SonarQube
-on:
- push:
- branches:
- - main
- pull_request:
- types: [opened, synchronize, reopened]
-
-permissions:
- contents: read
-
-jobs:
- build:
- name: Build and analyze
- runs-on: windows-latest
- steps:
- - name: Set up JDK 21
- uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
- with:
- java-version: 21
- distribution: "zulu" # Alternative distribution options are available.
- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.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@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
- with:
- dotnet-version: 10.0.401
- - name: Cache SonarQube Cloud packages
- if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
- with:
- path: ~\sonar\cache
- key: ${{ runner.os }}-sonar
- restore-keys: ${{ runner.os }}-sonar
- - name: Cache SonarQube Cloud scanner
- id: cache-sonar-scanner
- if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
- with:
- path: ${{ runner.temp }}\scanner
- key: ${{ runner.os }}-sonar-scanner
- restore-keys: ${{ runner.os }}-sonar-scanner
- - name: Install SonarQube Cloud scanner
- if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false
- 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: Begin SonarQube Cloud analysis
- id: sonar_begin
- if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false
- env:
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- shell: powershell
- run: |
- & "$env:RUNNER_TEMP\scanner\dotnet-sonarscanner.exe" begin /k:"CheatEngineNet_CheatEngine.Client" /o:"cheatenginenet" "/d:sonar.token=$env:SONAR_TOKEN" /d:sonar.cs.cobertura.reportsPaths="artifacts/sonar-test-results/**/*.cobertura.xml"
-
- - name: Build
- shell: powershell
- run: |
- dotnet build CheatEngine.Client.slnx --configuration Release
-
- - name: Test
- shell: powershell
- run: |
- # The stacked branches deliberately introduce some test projects before their first tests. MTP reports exit
- # code 8 for those empty intermediate projects; ignore only that code while genuine test failures still fail.
- dotnet test --solution CheatEngine.Client.slnx --configuration Release --no-build --coverage --coverage-output-format cobertura --results-directory artifacts/sonar-test-results --ignore-exit-code 8
-
- - name: End SonarQube Cloud analysis
- if: ${{ always() && steps.sonar_begin.outcome == 'success' }}
- env:
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- shell: powershell
- run: |
- & "$env:RUNNER_TEMP\scanner\dotnet-sonarscanner.exe" end "/d:sonar.token=$env:SONAR_TOKEN"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..47c1247
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,136 @@
+name: Client validation
+
+on:
+ workflow_call:
+
+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
+ run: >-
+ dotnet test --solution CheatEngine.Client.slnx --configuration Release --no-build --no-restore
+ --report-trx --results-directory artifacts/test-results --fail-skips on
+
+ - 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: 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..3d69a2d
--- /dev/null
+++ b/.github/workflows/main-ci.yml
@@ -0,0 +1,34 @@
+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
diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml
new file mode 100644
index 0000000..65a94ba
--- /dev/null
+++ b/.github/workflows/pull-request-ci.yml
@@ -0,0 +1,64 @@
+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
+
+ 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..5b34005
--- /dev/null
+++ b/.github/workflows/sonar.yml
@@ -0,0 +1,137 @@
+name: SonarQube Cloud
+
+on:
+ push:
+ branches: [main]
+ paths:
+ - '.editorconfig'
+ - '.github/**'
+ - 'CheatEngine.Client.slnx'
+ - 'Directory.Build.props'
+ - 'Directory.Build.targets'
+ - 'Directory.Packages.props'
+ - 'eng/**'
+ - 'global.json'
+ - 'libs/**'
+ - 'src/**'
+ - 'templates/**'
+ - 'tests/**'
+ pull_request:
+ types: [opened, synchronize, reopened, ready_for_review]
+ paths:
+ - '.editorconfig'
+ - '.github/**'
+ - 'CheatEngine.Client.slnx'
+ - 'Directory.Build.props'
+ - 'Directory.Build.targets'
+ - 'Directory.Packages.props'
+ - 'eng/**'
+ - 'global.json'
+ - 'libs/**'
+ - 'src/**'
+ - 'templates/**'
+ - 'tests/**'
+ workflow_dispatch:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+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: Build, test, and analyze
+ # Secrets are intentionally never exposed to pull requests from forks.
+ if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
+ runs-on: windows-latest
+ timeout-minutes: 35
+
+ steps:
+ - name: Set up JDK 21
+ uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
+ with:
+ distribution: zulu
+ java-version: '21'
+
+ - 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: Install pinned SonarScanner for .NET
+ id: scanner
+ run: |
+ $scannerDirectory = Join-Path $env:RUNNER_TEMP 'sonar-scanner'
+ New-Item -Path $scannerDirectory -ItemType Directory -Force | Out-Null
+ dotnet tool install dotnet-sonarscanner --tool-path $scannerDirectory --version $env:SONAR_SCANNER_VERSION
+ "path=$(Join-Path $scannerDirectory 'dotnet-sonarscanner.exe')" >> $env:GITHUB_OUTPUT
+
+ - name: Restore locked dependency graph
+ run: dotnet restore CheatEngine.Client.slnx --locked-mode
+
+ - name: Begin Sonar analysis
+ env:
+ SONAR_SCANNER: ${{ steps.scanner.outputs.path }}
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ run: |
+ & $env:SONAR_SCANNER begin `
+ /k:"CheatEngineNet_CheatEngine.Client" `
+ /o:"cheatenginenet" `
+ /d:sonar.token="$env:SONAR_TOKEN" `
+ /d:sonar.coverage.exclusions="tests/CheatEngine.Client.AotProbe/**" `
+ /d:sonar.cs.cobertura.reportsPaths="artifacts/sonar-test-results/**/*.cobertura.xml"
+
+ - name: Build Release
+ run: dotnet build CheatEngine.Client.slnx --configuration Release --no-restore --warnaserror
+
+ - name: Run tests with Cobertura coverage
+ run: >-
+ dotnet test --solution CheatEngine.Client.slnx --configuration Release --no-build --no-restore
+ --coverage --coverage-output-format cobertura --report-trx
+ --results-directory artifacts/sonar-test-results --fail-skips on
+
+ - name: Verify coverage reports
+ run: |
+ $reports = @(Get-ChildItem -Path artifacts/sonar-test-results -Recurse -File -Filter '*.cobertura.xml' -ErrorAction SilentlyContinue)
+ if ($reports.Count -eq 0) {
+ throw 'Microsoft Testing Platform did not produce a Cobertura coverage report.'
+ }
+
+ $reports | ForEach-Object { Write-Host "Coverage report: $($_.FullName)" }
+
+ - name: End Sonar analysis
+ env:
+ SONAR_SCANNER: ${{ steps.scanner.outputs.path }}
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ run: '& $env:SONAR_SCANNER end /d:sonar.token="$env:SONAR_TOKEN"'
+
+ - name: Upload Sonar coverage reports
+ if: ${{ !cancelled() }}
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: sonar-coverage
+ path: artifacts/sonar-test-results/**/*.cobertura.xml
+ if-no-files-found: warn
+ retention-days: 14
diff --git a/CheatEngine.Client.slnx b/CheatEngine.Client.slnx
index c8acdbc..99628c9 100644
--- a/CheatEngine.Client.slnx
+++ b/CheatEngine.Client.slnx
@@ -11,22 +11,30 @@
+
-
+
+
+
-
+
+
+
+
+
diff --git a/README.md b/README.md
index 1fe3585..1a7b6a4 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/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/src/CheatEngine.Client/README.md b/src/CheatEngine.Client/README.md
index 7b34e28..4e51357 100644
--- a/src/CheatEngine.Client/README.md
+++ b/src/CheatEngine.Client/README.md
@@ -1,22 +1,21 @@
# CheatEngine.Client
-The recommended NuGet installation point for high-level, DI-oriented Cheat Engine plugins written in C# 14 and .NET 10.
-
## Context
-`CheatEngine.Client` is a NuGet façade package. It has no operational implementation of its own; it composes the Fluent API and plugin Hosting packages, which in turn bring the public Client contracts and their implementation dependencies.
-
-The public surface is organized by function rather than by delivery assembly. Plugin code uses namespaces such as `CheatEngine.Client`, `CheatEngine.Client.Memory`, `CheatEngine.Client.Scanning`, `CheatEngine.Client.Tables`, `CheatEngine.Client.Lua`, and `CheatEngine.Client.Hosting`. It does not need to use implementation namespaces.
+`CheatEngine.Client` is the umbrella package for a normal in-process Cheat Engine plugin. It is the composition root
+of the Client delivery graph: it combines Hosting and Fluent APIs over the public Client contracts, while keeping the
+SDK-facing Core implementation behind the DI registration boundary. The package itself intentionally contains no
+Cheat Engine host logic.
## Why this project exists
-Most plugin authors should install one Client package, not reconstruct its package graph. This façade provides that stable installation point while keeping the lower-level packages separately consumable when a project needs only a focused capability.
-
-It deliberately does not hide `CheatEngine.SDK`: a real plugin must directly reference the SDK so that the SDK's source generators, build targets, and native bridge assets are active in the plugin project.
+Most plugin projects should take one Client package rather than recreate the Client package graph. This façade is that
+stable installation point: Hosting supplies the activation-scoped DI lifecycle, Fluent supplies immutable request
+builders, and Core is composed internally through Hosting's DI registration.
-## How it helps CheatEngine.Client
-
-Use this package together with an explicit SDK package reference:
+It deliberately does not replace `CheatEngine.SDK`. A plugin must reference the SDK **directly** so its build assets
+can generate the Cheat Engine entry point and copy the native Lua bridge. Those host-bound assets do not flow through
+an ordinary transitive NuGet dependency.
```xml
@@ -32,26 +31,26 @@ Use this package together with an explicit SDK package reference:
```
-The direct SDK reference is required even though Hosting has an SDK dependency. When `CheatEngineClientPluginProject` is `true`, Hosting's transitive build target reports `CECLIENT001` if the direct reference is missing.
+When `CheatEngineClientPluginProject` is enabled, the Hosting build target emits `CECLIENT001` if that direct SDK
+reference is missing.
-For a plugin, derive from `CheatEngineClientPlugin`, configure services and sources explicitly, and use `ICheatEngineClient` only within an enabled lifecycle. The Client facade exposes bounded synchronous APIs for runtime capabilities, process selection, typed memory, AOB scanning, inspection, tables, and typed Lua operations. Capability-dependent operations report Client failures when unavailable; value-scan functionality remains capability-gated.
+## How it helps improve CheatEngine.Client
-```csharp
-using CheatEngine.Client;
-using CheatEngine.Client.Hosting;
+The package gives plugin authors a small, intentional composition boundary without exposing implementation or SDK
+ownership types. It brings together:
-public sealed class Plugin : CheatEngineClientPlugin
-{
- protected override void Configure(CheatEnginePluginBuilder builder)
- {
- // Add explicit configuration, modules, and memory codecs here.
- }
+- `CheatEngine.Client.Hosting` for the enable-epoch DI container and plugin lifecycle;
+- `CheatEngine.Client.Fluent` for immutable memory and AOB request builders;
+- `CheatEngine.Client.Core`, composed through Hosting, as the only SDK mapper;
+- functional public namespaces such as `CheatEngine.Client.Memory`, `.Scanning`, `.Tables`, and `.Lua`.
- protected override void OnClientEnabled(ICheatEngineClient client)
- {
- // The Client is valid only for this activation epoch.
- }
-}
-```
+Use the generated `ceplugin` template for a complete, buildable plugin shape. The client and all Client-created
+resources are valid only for one enable epoch; do not retain them across disable/re-enable. See the repository
+[README](../../README.md) for installation and deployment guidance, [ADR 0001](../../docs/adr/0001-layered-in-process-architecture.md)
+for the package architecture, and [ADR 0002](../../docs/adr/0002-plugin-activation-lifecycle.md) for lifecycle rules.
+
+## Rules
-For the complete, SDK-annotated entry point and project configuration, install `CheatEngine.Client.Templates` and create the `ceplugin` template. The template is the executable reference for the expected plugin shape.
+- This is the only public package where Hosting, Core, and Fluent meet.
+- The assembly and root namespace are both `CheatEngine.Client`; do not declare a `CheatEngine` or `Client` type in
+ this namespace because CA1724 matches each namespace segment.
diff --git a/templates/CheatEngine.Client.Templates/CheatEngine.Client.Templates.csproj b/templates/CheatEngine.Client.Templates/CheatEngine.Client.Templates.csproj
new file mode 100644
index 0000000..cdf4ff6
--- /dev/null
+++ b/templates/CheatEngine.Client.Templates/CheatEngine.Client.Templates.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Template
+ false
+ $(NoWarn);NU5128
+
+
+
+
+
+
+
+
+
diff --git a/templates/CheatEngine.Client.Templates/README.md b/templates/CheatEngine.Client.Templates/README.md
new file mode 100644
index 0000000..838af70
--- /dev/null
+++ b/templates/CheatEngine.Client.Templates/README.md
@@ -0,0 +1,61 @@
+# CheatEngine.Client.Templates
+
+## Context
+
+`CheatEngine.Client.Templates` is the content-only NuGet package that provides the `dotnet new ceplugin` template. It
+creates a C# 14, .NET 10, x64, managed in-process Cheat Engine plugin whose composition starts from
+`CheatEngineClientPlugin`.
+
+The generated plugin is the repository's executable reference implementation. There is intentionally no separate
+`samples/` project to keep in sync.
+
+## Why this project exists
+
+Cheat Engine plugins need a direct reference to both packages below:
+
+- `CheatEngine.Client` supplies the high-level facade, fluent API, dependency-injection composition, activation
+ lifecycle, and module model.
+- `CheatEngine.SDK` supplies the plugin entry-point generator and native Lua bridge build assets. Those assets are not
+ guaranteed to flow through a transitive dependency.
+
+The template makes that deployment-critical relationship explicit. Its
+`true` marker activates Hosting's `CECLIENT001` guard,
+which fails a marked plugin build if the SDK reference stops being direct.
+
+## How it helps improve CheatEngine.Client
+
+The template turns the intended consumption model into buildable source. It exercises activation-scoped DI, generated
+SDK plugin bootstrap, explicit configuration, bounded AOB probing, typed memory access, Address List inspection, and
+an application-owned Lua module. Keeping this path executable prevents package, bootstrap, and documentation drift.
+
+It deliberately does not imply that a Native AOT binary is loadable by Cheat Engine. The project enables AOT
+compatibility analysis for the library graph, but a plugin must be deployed as the complete managed output required by
+the SDK. Value scans remain capability-gated until their full Cheat Engine 7.7 x64 lifecycle has passed the opt-in
+live gate.
+
+## Create a plugin
+
+Install the published template, then instantiate it from the directory that should contain the new project:
+
+```powershell
+dotnet new install CheatEngine.Client.Templates
+dotnet new ceplugin --name Contoso.CheatEngine.Plugin --output .\Contoso.CheatEngine.Plugin
+dotnet restore .\Contoso.CheatEngine.Plugin\Contoso.CheatEngine.Plugin.csproj
+dotnet build .\Contoso.CheatEngine.Plugin\Contoso.CheatEngine.Plugin.csproj --configuration Release --no-restore
+```
+
+The generated project's [README](content/CheatEngine.Plugin/README.md) explains the composition, configuration, and
+managed deployment requirements. Keep both direct package references when adapting the project.
+
+## Validate the template from this repository
+
+Pack the repository first so the smoke test can restore the Client packages from `artifacts/packages`, then run the
+dedicated validation script:
+
+```powershell
+dotnet pack CheatEngine.Client.slnx --configuration Release
+.\eng\Invoke-TemplateSmoke.ps1 -PackageSource .\artifacts\packages
+```
+
+The smoke test installs the locally packed template, runs `dotnet new ceplugin --dry-run`, instantiates it into a
+temporary directory, restores it against the local package source, and builds it in Release configuration.
diff --git a/templates/CheatEngine.Client.Templates/content/CheatEngine.Plugin/.template.config/template.json b/templates/CheatEngine.Client.Templates/content/CheatEngine.Plugin/.template.config/template.json
new file mode 100644
index 0000000..7fe9c26
--- /dev/null
+++ b/templates/CheatEngine.Client.Templates/content/CheatEngine.Plugin/.template.config/template.json
@@ -0,0 +1,49 @@
+{
+ "$schema": "http://json.schemastore.org/template",
+ "author": "CheatEngineNet",
+ "classifications": [
+ "Cheat Engine",
+ "Plugin"
+ ],
+ "identity": "CheatEngineNet.CheatEngine.Client.Plugin",
+ "name": "CheatEngine Client Plugin",
+ "description": "A C# 14 .NET 10 Cheat Engine plugin hosted by CheatEngine.Client.",
+ "shortName": "ceplugin",
+ "sourceName": "CheatEngine.Plugin",
+ "defaultName": "CheatEngine.Plugin",
+ "tags": {
+ "language": "C#",
+ "type": "project"
+ },
+ "constraints": {
+ "dotnet": {
+ "type": "host",
+ "args": [
+ {
+ "hostname": "dotnetcli"
+ }
+ ]
+ },
+ "net10": {
+ "type": "sdk-version",
+ "args": "[10.0.401,)"
+ }
+ },
+ "primaryOutputs": [
+ {
+ "path": "CheatEngine.Plugin.csproj"
+ }
+ ],
+ "postActions": [
+ {
+ "description": "Restore NuGet packages.",
+ "manualInstructions": [
+ {
+ "text": "Run 'dotnet restore'."
+ }
+ ],
+ "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025",
+ "continueOnError": true
+ }
+ ]
+}
diff --git a/templates/CheatEngine.Client.Templates/content/CheatEngine.Plugin/CheatEngine.Plugin.csproj b/templates/CheatEngine.Client.Templates/content/CheatEngine.Plugin/CheatEngine.Plugin.csproj
new file mode 100644
index 0000000..324736f
--- /dev/null
+++ b/templates/CheatEngine.Client.Templates/content/CheatEngine.Plugin/CheatEngine.Plugin.csproj
@@ -0,0 +1,28 @@
+
+
+
+ net10.0
+ 14.0
+ enable
+ enable
+ x64
+ true
+
+ true
+ true
+ true
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/templates/CheatEngine.Client.Templates/content/CheatEngine.Plugin/Modules/PluginClientModule.cs b/templates/CheatEngine.Client.Templates/content/CheatEngine.Plugin/Modules/PluginClientModule.cs
new file mode 100644
index 0000000..5039262
--- /dev/null
+++ b/templates/CheatEngine.Client.Templates/content/CheatEngine.Plugin/Modules/PluginClientModule.cs
@@ -0,0 +1,128 @@
+using CheatEngine.Client;
+using CheatEngine.Client.Extensions.DependencyInjection;
+using CheatEngine.Client.Lua;
+using CheatEngine.Client.Memory;
+using CheatEngine.Client.Modules;
+using CheatEngine.Client.Processes;
+using CheatEngine.Client.Results;
+using CheatEngine.Client.Scanning;
+using CheatEngine.Client.Tables;
+using CheatEngine.SDK.Engine.Values;
+
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace CheatEngine.Plugin.Modules;
+
+///
+/// Demonstrates DI, options, bounded AOB/memory access, Address List snapshots, and generated Lua exports.
+///
+internal sealed partial class PluginClientModule(
+ IMemoryCodec int32Codec,
+ IOptions options,
+ ILogger logger) : ICheatEngineClientModule
+{
+ private readonly CheatEngineClientOptions _options = options.Value;
+ private ILuaModuleLease? _luaModuleLease;
+
+ /// Registers the activation-scoped Lua module and demonstrates bounded Client operations.
+ public void OnEnabled(ICheatEngineClient client)
+ {
+ ArgumentNullException.ThrowIfNull(client);
+
+ if (_luaModuleLease is not null)
+ {
+ throw new InvalidOperationException("The Lua module is already registered for this activation.");
+ }
+
+ _luaModuleLease = client.Lua.RegisterModule(new PluginLuaModule());
+
+ int allowedTableRootCount = _options.AllowedTableRoots?.Length ?? 0;
+ LogEnabled(logger, client.Epoch, allowedTableRootCount);
+
+ if (client.Tables.TryGetCurrent(out AddressTableSnapshot table, out CheatEngineFailure tableFailure))
+ {
+ LogAddressList(logger, table.RecordCount);
+ }
+ else
+ {
+ LogSkipped("Address List", tableFailure);
+ }
+
+ if (!client.Processes.TryGetCurrent(out ProcessSnapshot process, out CheatEngineFailure processFailure))
+ {
+ LogSkipped("AOB/memory probe", processFailure);
+ return;
+ }
+
+ AobScanBuilder scan = client.Patterns.Aob("48 8B ?? ?? ?? 89");
+ if (process.Name is { } processName)
+ {
+ scan = scan.InModule(processName);
+ }
+
+ if (!scan.ReadableExecutable()
+ .FirstOrNone()
+ .TryExecute(out Address? address, out CheatEngineFailure scanFailure))
+ {
+ LogSkipped("AOB probe", scanFailure);
+ return;
+ }
+
+ if (address is not { } match)
+ {
+ return;
+ }
+
+ if (client.Memory.At(match + 0x14).TryReadWith(int32Codec, out _, out CheatEngineFailure readFailure))
+ {
+ LogMemoryReadSucceeded(logger, match);
+ }
+ else
+ {
+ LogSkipped("Memory probe", readFailure);
+ }
+ }
+
+ ///
+ /// Releases the activation-scoped Lua module so the hosting lifecycle can aggregate any cleanup failure.
+ ///
+ public void OnDisabling(ICheatEngineClient client)
+ {
+ ArgumentNullException.ThrowIfNull(client);
+
+ ILuaModuleLease? lease = _luaModuleLease;
+ _luaModuleLease = null;
+ if (lease is null)
+ {
+ return;
+ }
+
+ lease.Dispose();
+ }
+
+ /// Writes a bounded Client operation failure without exposing target-memory data.
+ private void LogSkipped(string operation, CheatEngineFailure failure)
+ {
+ LogClientFailure(logger, operation, failure);
+ }
+
+ /// Logs the activation epoch and configured count of trusted table-file roots.
+ [LoggerMessage(Level = LogLevel.Information,
+ Message = "CheatEngine.Plugin enabled at epoch {Epoch}; " +
+ "configured trusted table-file root count is {AllowedTableRootCount}.")]
+ private static partial void LogEnabled(ILogger logger, long epoch, int allowedTableRootCount);
+
+ /// Logs the number of records in the current Address List snapshot.
+ [LoggerMessage(Level = LogLevel.Information, Message = "Current Address List contains {RecordCount} record(s).")]
+ private static partial void LogAddressList(ILogger logger, int recordCount);
+
+ /// Logs a successful bounded Int32 memory probe without logging the value read.
+ [LoggerMessage(Level = LogLevel.Information,
+ Message = "A typed Int32 memory read succeeded near AOB match {Address}.")]
+ private static partial void LogMemoryReadSucceeded(ILogger logger, Address address);
+
+ /// Logs a classified Client failure for an optional demonstration operation.
+ [LoggerMessage(Level = LogLevel.Debug, Message = "Skipped {Operation}: {Reason}")]
+ private static partial void LogClientFailure(ILogger logger, string operation, CheatEngineFailure reason);
+}
diff --git a/templates/CheatEngine.Client.Templates/content/CheatEngine.Plugin/Modules/PluginLuaFunctions.cs b/templates/CheatEngine.Client.Templates/content/CheatEngine.Plugin/Modules/PluginLuaFunctions.cs
new file mode 100644
index 0000000..63a5825
--- /dev/null
+++ b/templates/CheatEngine.Client.Templates/content/CheatEngine.Plugin/Modules/PluginLuaFunctions.cs
@@ -0,0 +1,14 @@
+using CheatEngine.SDK.Annotations.Lua;
+
+namespace CheatEngine.Plugin.Modules;
+
+/// Generated SDK Lua exports available while this plugin activation is enabled.
+internal static partial class PluginLuaFunctions
+{
+ /// Returns the activation-local status text for a generated Lua export.
+ [LuaFunction("cheatengine_client_plugin_status")]
+ public static string Status()
+ {
+ return "CheatEngine.Plugin Lua module is enabled.";
+ }
+}
diff --git a/templates/CheatEngine.Client.Templates/content/CheatEngine.Plugin/Modules/PluginLuaModule.cs b/templates/CheatEngine.Client.Templates/content/CheatEngine.Plugin/Modules/PluginLuaModule.cs
new file mode 100644
index 0000000..5c550da
--- /dev/null
+++ b/templates/CheatEngine.Client.Templates/content/CheatEngine.Plugin/Modules/PluginLuaModule.cs
@@ -0,0 +1,34 @@
+using CheatEngine.Client.Lua;
+using CheatEngine.SDK.Lua.Calls;
+using CheatEngine.SDK.Lua.Runtime;
+
+namespace CheatEngine.Plugin.Modules;
+
+/// Application-owned Lua module that encapsulates the generated SDK binding calls.
+///
+/// invokes this module only while the activation is current and owns the lease that calls
+/// . The SDK state remains inside this implementation; it never crosses the public Client
+/// contract or the consuming Client module.
+///
+internal sealed class PluginLuaModule : ILuaModule
+{
+ ///
+ public void Register()
+ {
+ LuaStatus status = PluginLuaFunctions.RegisterLuaFunctions(LuaRuntime.AcquireState());
+ if (!status.IsOk)
+ {
+ throw new InvalidOperationException($"Unable to register the plugin Lua module: {status}.");
+ }
+ }
+
+ ///
+ public void Unregister()
+ {
+ LuaStatus status = PluginLuaFunctions.UnregisterLuaFunctions(LuaRuntime.AcquireState());
+ if (!status.IsOk)
+ {
+ throw new InvalidOperationException($"Unable to unregister the plugin Lua module: {status}.");
+ }
+ }
+}
diff --git a/templates/CheatEngine.Client.Templates/content/CheatEngine.Plugin/Plugin.cs b/templates/CheatEngine.Client.Templates/content/CheatEngine.Plugin/Plugin.cs
new file mode 100644
index 0000000..99700fc
--- /dev/null
+++ b/templates/CheatEngine.Client.Templates/content/CheatEngine.Plugin/Plugin.cs
@@ -0,0 +1,29 @@
+using CheatEngine.Client;
+using CheatEngine.Client.Hosting;
+using CheatEngine.Plugin.Modules;
+using CheatEngine.SDK.Annotations.Plugin;
+using Microsoft.Extensions.Configuration;
+
+namespace CheatEngine.Plugin;
+
+/// The plugin entry point generated and loaded by Cheat Engine.
+[CheatEnginePlugin("CheatEngine Client Plugin")]
+public sealed class Plugin : CheatEngineClientPlugin
+{
+ /// Adds explicit configuration sources for this activation.
+ protected override void Configure(CheatEnginePluginBuilder builder)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ builder.Configuration
+ .SetBasePath(AppContext.BaseDirectory)
+ .AddJsonFile("appsettings.json", optional: true, reloadOnChange: false);
+
+ builder.Client.AddModule();
+ }
+
+ /// Runs after the Client activation scope and its modules have started.
+ protected override void OnClientEnabled(ICheatEngineClient client)
+ {
+ ArgumentNullException.ThrowIfNull(client);
+ }
+}
diff --git a/templates/CheatEngine.Client.Templates/content/CheatEngine.Plugin/README.md b/templates/CheatEngine.Client.Templates/content/CheatEngine.Plugin/README.md
new file mode 100644
index 0000000..023eb17
--- /dev/null
+++ b/templates/CheatEngine.Client.Templates/content/CheatEngine.Plugin/README.md
@@ -0,0 +1,62 @@
+# CheatEngine.Plugin
+
+## Context
+
+This project was created by `dotnet new ceplugin`. It is a C# 14, .NET 10, x64, managed in-process plugin for Cheat
+Engine, built on the functional `CheatEngine.Client` API surface and hosted by `CheatEngineClientPlugin`.
+
+It is also the canonical executable example for the CheatEngine.Client repository. The repository intentionally keeps
+this project inside the template instead of maintaining a separate `samples/` copy.
+
+## Why this project exists
+
+The project provides a minimal but production-shaped plugin boundary:
+
+- `[CheatEnginePlugin]` is the SDK entry-point annotation recognized by the generated bootstrap.
+- `CheatEngineClientPlugin` creates a fresh DI container and Client activation for every enable cycle.
+- `Configure` explicitly loads the optional `appsettings.json` beside the plugin with `reloadOnChange: false` and
+ registers an application module.
+- `PluginClientModule` demonstrates options, logging, a bounded AOB request, typed memory access, an Address List
+ snapshot, and an activation-scoped Lua module lease.
+
+The project references `CheatEngine.Client` **and** `CheatEngine.SDK` directly. The SDK reference must remain direct:
+its plugin generator and native Lua bridge assets are build inputs, not a transitive implementation detail.
+`true` enables the `CECLIENT001` build guard to enforce
+that rule.
+
+## How it helps improve CheatEngine.Client
+
+This plugin is compiled by the template smoke test. It therefore continuously verifies the installation path that
+matters to consumers: package restore, SDK-generated bootstrap, copied bridge assets, functional Client namespaces,
+and the DI-first lifecycle. Its normal host preconditions use `Try...` APIs, so an absent process or pattern does not
+turn the example into an artificial activation failure.
+
+The Lua implementation deliberately keeps `LuaRuntime.AcquireState()` and generated SDK calls inside
+`PluginLuaModule`; no raw Lua state or SDK ownership handle crosses the Client-facing module boundary. The project does
+not demonstrate value scans because their complete Create/Scan/Destroy lifecycle is still capability-gated pending
+the opt-in Cheat Engine 7.7 x64 live validation.
+
+## Build
+
+From this project directory, restore and build the managed plugin:
+
+```powershell
+dotnet restore .\CheatEngine.Plugin.csproj
+dotnet build .\CheatEngine.Plugin.csproj --configuration Release --no-restore
+```
+
+Deploy the complete `bin\Release\net10.0` managed output produced by that build, including the plugin assembly,
+`.runtimeconfig.json`, `CheatEngine.SDK` assemblies, and the SDK Lua bridge assets. Do not publish this project as a
+Native AOT plugin binary: `IsAotCompatible` validates library compatibility only and is not a Cheat Engine plugin
+loader guarantee.
+
+## Configure and adapt
+
+`appsettings.json` is optional and is loaded only because `Plugin.Configure` explicitly adds it. Leave
+`CheatEngineClient:AllowedTableRoots` empty unless table import/export paths have been deliberately authorized;
+loading a table can execute Lua. Configuration and module registrations are rebuilt at the next plugin enable, not
+reloaded while an activation is active.
+
+Before deployment, replace the illustrative AOB pattern and offset in `Modules/PluginClientModule.cs`, and choose an
+application-specific Lua global name in `Modules/PluginLuaFunctions.cs`. Keep AOB operations bounded and avoid logging
+memory contents or Lua scripts by default.
diff --git a/templates/CheatEngine.Client.Templates/content/CheatEngine.Plugin/appsettings.json b/templates/CheatEngine.Client.Templates/content/CheatEngine.Plugin/appsettings.json
new file mode 100644
index 0000000..acd8651
--- /dev/null
+++ b/templates/CheatEngine.Client.Templates/content/CheatEngine.Plugin/appsettings.json
@@ -0,0 +1,5 @@
+{
+ "CheatEngineClient": {
+ "AllowedTableRoots": []
+ }
+}
diff --git a/templates/CheatEngine.Client.Templates/packages.lock.json b/templates/CheatEngine.Client.Templates/packages.lock.json
new file mode 100644
index 0000000..009cf0c
--- /dev/null
+++ b/templates/CheatEngine.Client.Templates/packages.lock.json
@@ -0,0 +1,36 @@
+{
+ "version": 2,
+ "dependencies": {
+ "net10.0": {
+ "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/tests/CheatEngine.Client.AotProbe/CheatEngine.Client.AotProbe.csproj b/tests/CheatEngine.Client.AotProbe/CheatEngine.Client.AotProbe.csproj
new file mode 100644
index 0000000..0cd445e
--- /dev/null
+++ b/tests/CheatEngine.Client.AotProbe/CheatEngine.Client.AotProbe.csproj
@@ -0,0 +1,14 @@
+
+
+
+ Exe
+ win-x64
+ true
+ false
+
+
+
+
+
+
+
diff --git a/tests/CheatEngine.Client.AotProbe/Program.cs b/tests/CheatEngine.Client.AotProbe/Program.cs
new file mode 100644
index 0000000..03350d8
--- /dev/null
+++ b/tests/CheatEngine.Client.AotProbe/Program.cs
@@ -0,0 +1,21 @@
+using CheatEngine.Client;
+using CheatEngine.Client.Extensions.DependencyInjection;
+using CheatEngine.Client.Hosting;
+using CheatEngine.Client.Scanning;
+
+using Microsoft.Extensions.DependencyInjection;
+
+ServiceCollection services = new();
+services.AddCheatEngineClient().EnableUnsafeLuaExecution();
+
+using ServiceProvider provider = services.BuildServiceProvider(new ServiceProviderOptions
+{
+ ValidateOnBuild = true, ValidateScopes = true
+});
+
+_ = typeof(ICheatEngineClient);
+_ = typeof(CheatEngineClientPlugin);
+_ = typeof(AobScanBuilder);
+_ = new AobPattern("90");
+
+return services.Count == 0 ? 1 : 0;
diff --git a/tests/CheatEngine.Client.AotProbe/README.md b/tests/CheatEngine.Client.AotProbe/README.md
new file mode 100644
index 0000000..ccae8e6
--- /dev/null
+++ b/tests/CheatEngine.Client.AotProbe/README.md
@@ -0,0 +1,31 @@
+# CheatEngine.Client.AotProbe
+
+## Context
+
+This executable publishes the complete public Client graph as a `win-x64` Native AOT application. It is a build-time
+compatibility probe, not a Cheat Engine plugin.
+
+## Why this project exists
+
+The shipped libraries promise trimming and Native AOT analysis compatibility. A normal library build cannot prove that
+the full dependency graph remains compatible when the AOT compiler resolves it as an application.
+
+## How it helps improve CheatEngine.Client
+
+Publishing this probe turns AOT warnings into a delivery gate. It detects reflection-dependent paths, incompatible
+metadata usage, or transitive AOT regressions while keeping the result separate from Cheat Engine's managed plugin
+loader requirements.
+
+It does **not** claim that Cheat Engine can load a Native AOT plugin DLL. Plugins generated by `ceplugin` remain managed
+and must include the SDK bootstrap and bridge assets.
+
+## Validate
+
+From the repository root:
+
+```powershell
+dotnet publish .\tests\CheatEngine.Client.AotProbe\CheatEngine.Client.AotProbe.csproj --configuration Release
+```
+
+The successful output is a Native AOT executable under the repository artifacts path for `win-x64`; it is not intended
+to be copied into a Cheat Engine plugin directory.
diff --git a/tests/CheatEngine.Client.AotProbe/packages.lock.json b/tests/CheatEngine.Client.AotProbe/packages.lock.json
new file mode 100644
index 0000000..4300e86
--- /dev/null
+++ b/tests/CheatEngine.Client.AotProbe/packages.lock.json
@@ -0,0 +1,217 @@
+{
+ "version": 2,
+ "dependencies": {
+ "net10.0": {
+ "Microsoft.DotNet.ILCompiler": {
+ "type": "Direct",
+ "requested": "[10.0.12, )",
+ "resolved": "10.0.12",
+ "contentHash": "AawF393Q+VkdrnrnI1gu612zh5iqpa1AGSvnKCQ3IkMgSKJXIQbO1sXYRgEzOU4f0cPZ6MaCCF29xZSGWlmbuQ=="
+ },
+ "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.Extensions.Configuration": {
+ "type": "Transitive",
+ "resolved": "10.0.12",
+ "contentHash": "e3IPP32CRNL031VZJAUTlCTG0YN7WFh4mN3fsSHTDQCJB+3+f0jGycv4fXk3rrftaY3B85XrQaj7sRthrOsavg==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.12",
+ "Microsoft.Extensions.Primitives": "10.0.12"
+ }
+ },
+ "Microsoft.Extensions.Options": {
+ "type": "Transitive",
+ "resolved": "10.0.12",
+ "contentHash": "TDYD33TSRpXKZWlmTXNlj5kCihxatmv2Ec1u6C+bMYLphCS7PoSLE9Pjd/nunDoE7yETk+LLKjVJX78HYtWjpA==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12",
+ "Microsoft.Extensions.Primitives": "10.0.12"
+ }
+ },
+ "Microsoft.Extensions.Primitives": {
+ "type": "Transitive",
+ "resolved": "10.0.12",
+ "contentHash": "dYfCLR52UA+3DL7C4I/pvSaRPkNqxrUAQmbFL2u0zvYKKzqgrFCJl08Df+F1aYc8leu9JvpC9bsURUdpExcBXQ=="
+ },
+ "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=="
+ },
+ "cheatengine.client": {
+ "type": "Project",
+ "dependencies": {
+ "CheatEngine.Client.Fluent": "[0.1.0, )",
+ "CheatEngine.Client.Hosting": "[0.1.0, )"
+ }
+ },
+ "cheatengine.client.abstractions": {
+ "type": "Project",
+ "dependencies": {
+ "CheatEngine.SDK": "[1.0.0, 2.0.0)"
+ }
+ },
+ "cheatengine.client.core": {
+ "type": "Project",
+ "dependencies": {
+ "CheatEngine.Client.Abstractions": "[0.1.0, )",
+ "CheatEngine.SDK": "[1.0.0, 2.0.0)"
+ }
+ },
+ "cheatengine.client.extensions.dependencyinjection": {
+ "type": "Project",
+ "dependencies": {
+ "CheatEngine.Client.Abstractions": "[0.1.0, )",
+ "CheatEngine.Client.Core": "[0.1.0, )",
+ "Microsoft.Extensions.Configuration.Abstractions": "[10.0.12, )",
+ "Microsoft.Extensions.DependencyInjection": "[10.0.12, )",
+ "Microsoft.Extensions.Logging": "[10.0.12, )",
+ "Microsoft.Extensions.Options.ConfigurationExtensions": "[10.0.12, )",
+ "Microsoft.Extensions.Options.DataAnnotations": "[10.0.12, )"
+ }
+ },
+ "cheatengine.client.fluent": {
+ "type": "Project",
+ "dependencies": {
+ "CheatEngine.Client.Abstractions": "[0.1.0, )"
+ }
+ },
+ "cheatengine.client.hosting": {
+ "type": "Project",
+ "dependencies": {
+ "CheatEngine.Client.Extensions.DependencyInjection": "[0.1.0, )",
+ "CheatEngine.SDK": "[1.0.0, 2.0.0)",
+ "Microsoft.Extensions.DependencyInjection": "[10.0.12, )",
+ "Microsoft.Extensions.Logging": "[10.0.12, )"
+ }
+ },
+ "CheatEngine.SDK": {
+ "type": "CentralTransitive",
+ "requested": "[1.0.0, )",
+ "resolved": "1.0.0",
+ "contentHash": "n7nHqZ8vzo7Vf20jF0fkh/jUtR3yo1TwRGpXE7ERxZeJ4C5S/Nsft4lqOg7zGwfsD5Nh9tTVgdw4PrybJRF0gA=="
+ },
+ "Microsoft.Extensions.Configuration.Abstractions": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.12, )",
+ "resolved": "10.0.12",
+ "contentHash": "8xaGcvS/qZ1otoxPQCEJkNva389CVL/plNcvIETZhQTETYdRkYDPEYhUMoAGONo4FU45ufdfE0j29AfWVVj0wA==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "10.0.12"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Binder": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.12, )",
+ "resolved": "10.0.12",
+ "contentHash": "dAgIf1TOr8KLs+aBRIbXUZBjHoSH4rDG8+XkX/Q6AZwkQdMA0+yPDKTHsieeXdZDfpOpZVHzOnuAb5Z2nX3KsA==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "10.0.12",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.12"
+ }
+ },
+ "Microsoft.Extensions.DependencyInjection": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.12, )",
+ "resolved": "10.0.12",
+ "contentHash": "lXyK2O5GoYvfxW8eCFcD16JFbcoSTM1sJkAM0UHS1jZyl9NYMW64Tqm6OQFT0IDBjZi+xHt95/Zg+nxZhGFhZg==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12"
+ }
+ },
+ "Microsoft.Extensions.DependencyInjection.Abstractions": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.12, )",
+ "resolved": "10.0.12",
+ "contentHash": "9/qymSh7hVDMGTGwrLz8MRp5zRyXy9adGDOs4HwRdnLil3oZGYuWeZjbmHgCQ9BL1qBroVfgUK3U/nb61617Cw=="
+ },
+ "Microsoft.Extensions.Logging": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.12, )",
+ "resolved": "10.0.12",
+ "contentHash": "6I46fTPfgYkrjRYfRXbho9WOvOelTnNjWuZws/hzGHDASH1LEJeA4VKK9k3wJvido8o7jJSB5WkMTonX7HM1bA==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection": "10.0.12",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.12",
+ "Microsoft.Extensions.Options": "10.0.12"
+ }
+ },
+ "Microsoft.Extensions.Logging.Abstractions": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.12, )",
+ "resolved": "10.0.12",
+ "contentHash": "+24lC4plfbEDNfLAdTV/SWKS7dW+16X4HdydO3R++134kSNTzcbYA4KpR1Hdh6uWisB8Za3AzwyOn+K+NxWIug==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12"
+ }
+ },
+ "Microsoft.Extensions.Options.ConfigurationExtensions": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.12, )",
+ "resolved": "10.0.12",
+ "contentHash": "rqpu4qj5WE9x1IHGXSIgHBKi7IUlQaHyp4aXCYIanG2OghlUMFZpZTgExaXwcvmLAJHsxKQWMPpc7D2WIbCVtA==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.12",
+ "Microsoft.Extensions.Configuration.Binder": "10.0.12",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12",
+ "Microsoft.Extensions.Options": "10.0.12",
+ "Microsoft.Extensions.Primitives": "10.0.12"
+ }
+ },
+ "Microsoft.Extensions.Options.DataAnnotations": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.12, )",
+ "resolved": "10.0.12",
+ "contentHash": "rPqU/cnDMmL4Gx7sglNnYwh/upWxSLTwVe0ysT88WH9HEE3OfZgaT7TyM86C4GeXwDsDQaj95fUGL5jEB3vxng==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12",
+ "Microsoft.Extensions.Options": "10.0.12"
+ }
+ }
+ },
+ "net10.0/win-x64": {
+ "Microsoft.DotNet.ILCompiler": {
+ "type": "Direct",
+ "requested": "[10.0.12, )",
+ "resolved": "10.0.12",
+ "contentHash": "AawF393Q+VkdrnrnI1gu612zh5iqpa1AGSvnKCQ3IkMgSKJXIQbO1sXYRgEzOU4f0cPZ6MaCCF29xZSGWlmbuQ==",
+ "dependencies": {
+ "runtime.win-x64.Microsoft.DotNet.ILCompiler": "10.0.12"
+ }
+ },
+ "runtime.win-x64.Microsoft.DotNet.ILCompiler": {
+ "type": "Transitive",
+ "resolved": "10.0.12",
+ "contentHash": "clsgU9GnioCJ+PBzQTCoJHxLXRU+7O/BzYrwRT8CUP7jgyYjNgJmNsQ/kbzAA7MG5kDvFoFvIBGHGzYdINh/EQ=="
+ }
+ }
+ }
+}
diff --git a/tests/CheatEngine.Client.Binding.Tests/CheatEngine.Client.Binding.Tests.csproj b/tests/CheatEngine.Client.Binding.Tests/CheatEngine.Client.Binding.Tests.csproj
deleted file mode 100644
index 9ba1298..0000000
--- a/tests/CheatEngine.Client.Binding.Tests/CheatEngine.Client.Binding.Tests.csproj
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
diff --git a/tests/CheatEngine.Client.Binding.Tests/README.md b/tests/CheatEngine.Client.Binding.Tests/README.md
deleted file mode 100644
index 1700623..0000000
--- a/tests/CheatEngine.Client.Binding.Tests/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# CheatEngine.Client.Binding.Tests
-
-Tests of [`CheatEngine.Client.Binding`](../../libs/CheatEngine.Client.Binding/README.md). References its subject only.