+
# 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.Abstractions/README.md b/libs/CheatEngine.Client.Abstractions/README.md
index fa5b960..499cbcc 100644
--- a/libs/CheatEngine.Client.Abstractions/README.md
+++ b/libs/CheatEngine.Client.Abstractions/README.md
@@ -49,16 +49,16 @@ CheatEngine.Client.Abstractions
Package and assembly names are not consumer namespaces. Public code belongs to functional
namespaces only:
-| Namespace | Responsibility |
-|---|---|
-| `CheatEngine.Client` | `ICheatEngineClient`, the activation-scoped facade |
-| `.Dispatching` / `.Runtime` | main-thread dispatch and runtime/capability observations |
-| `.Processes` / `.Inspection` | target selection, copied process/module/region/symbol data |
-| `.Memory` | bounded primitive, byte, string, codec, and pointer-chain operations |
-| `.Scanning` | AOB contracts and the value-scan session contract |
-| `.Tables` | copied Address List records and explicitly trusted table I/O requests |
-| `.Lua` / `.Modules` | typed protected Lua operations, explicit modules, and leases |
-| `.Results` | classified expected failures and lifecycle exceptions |
+| Namespace | Responsibility |
+|------------------------------|-----------------------------------------------------------------------|
+| `CheatEngine.Client` | `ICheatEngineClient`, the activation-scoped facade |
+| `.Dispatching` / `.Runtime` | main-thread dispatch and runtime/capability observations |
+| `.Processes` / `.Inspection` | target selection, copied process/module/region/symbol data |
+| `.Memory` | bounded primitive, byte, string, codec, and pointer-chain operations |
+| `.Scanning` | AOB contracts and the value-scan session contract |
+| `.Tables` | copied Address List records and explicitly trusted table I/O requests |
+| `.Lua` / `.Modules` | typed protected Lua operations, explicit modules, and leases |
+| `.Results` | classified expected failures and lifecycle exceptions |
No public consumer should use `CheatEngine.Client.Abstractions` as a namespace.
diff --git a/libs/CheatEngine.Client.Abstractions/Runtime/CheatEngineRuntimeSnapshot.cs b/libs/CheatEngine.Client.Abstractions/Runtime/CheatEngineRuntimeSnapshot.cs
index 30e6f6b..0995fee 100644
--- a/libs/CheatEngine.Client.Abstractions/Runtime/CheatEngineRuntimeSnapshot.cs
+++ b/libs/CheatEngine.Client.Abstractions/Runtime/CheatEngineRuntimeSnapshot.cs
@@ -53,52 +53,28 @@ public CheatEngineRuntimePlatformInfo Platform
}
/// Gets the coarse number returned by CE's getCEVersion global, when it was callable.
- public double? ObservedCheatEngineVersion
- {
- get => Version.ObservedCheatEngineVersion;
- }
+ public double? ObservedCheatEngineVersion => Version.ObservedCheatEngineVersion;
/// Gets the complete CE build against which this Client release was qualified.
- public CheatEngineVersion QualifiedCheatEngineBaseline
- {
- get => Version.QualifiedCheatEngineBaseline;
- }
+ public CheatEngineVersion QualifiedCheatEngineBaseline => Version.QualifiedCheatEngineBaseline;
/// Gets the assembly version of this Client abstraction assembly.
- public Version ClientAssemblyVersion
- {
- get => Version.ClientAssemblyVersion;
- }
+ public Version ClientAssemblyVersion => Version.ClientAssemblyVersion;
/// Gets the assembly version of the SDK runtime-contract assembly.
- public Version SdkAssemblyVersion
- {
- get => Version.SdkAssemblyVersion;
- }
+ public Version SdkAssemblyVersion => Version.SdkAssemblyVersion;
/// Gets the CE host architecture observed from CE's system-architecture global.
- public CheatEngineArchitecture SystemArchitecture
- {
- get => Platform.SystemArchitecture;
- }
+ public CheatEngineArchitecture SystemArchitecture => Platform.SystemArchitecture;
/// Gets the target architecture observed by a target-specific probe, or unknown.
- public CheatEngineArchitecture TargetArchitecture
- {
- get => Platform.TargetArchitecture;
- }
+ public CheatEngineArchitecture TargetArchitecture => Platform.TargetArchitecture;
/// Gets the pointer width implied by the observed target architecture, or unknown.
- public PointerSize TargetPointerSize
- {
- get => Platform.TargetPointerSize;
- }
+ public PointerSize TargetPointerSize => Platform.TargetPointerSize;
/// Gets the target ABI observed from CE's ABI global, or unknown.
- public TargetAbi TargetAbi
- {
- get => Platform.TargetAbi;
- }
+ public TargetAbi TargetAbi => Platform.TargetAbi;
/// Gets the explicit availability observation for each SDK runtime capability that was probed.
public RuntimeCapabilities SdkCapabilities
@@ -114,8 +90,8 @@ public ClientCapabilities ClientCapabilities
/// Gets whether the observed coarse CE version belongs to the qualified major/minor line.
public bool IsOnQualifiedCheatEngineLine => ObservedCheatEngineVersion is { } observed &&
- observed >= QualifiedCheatEngineBaseline.Major +
- QualifiedCheatEngineBaseline.Minor / 10d &&
- observed < QualifiedCheatEngineBaseline.Major +
- (QualifiedCheatEngineBaseline.Minor + 1) / 10d;
+ observed >= QualifiedCheatEngineBaseline.Major +
+ QualifiedCheatEngineBaseline.Minor / 10d &&
+ observed < QualifiedCheatEngineBaseline.Major +
+ (QualifiedCheatEngineBaseline.Minor + 1) / 10d;
}
diff --git a/libs/CheatEngine.Client.Abstractions/Runtime/CheatEngineRuntimeVersionInfo.cs b/libs/CheatEngine.Client.Abstractions/Runtime/CheatEngineRuntimeVersionInfo.cs
index f879e54..66c1791 100644
--- a/libs/CheatEngine.Client.Abstractions/Runtime/CheatEngineRuntimeVersionInfo.cs
+++ b/libs/CheatEngine.Client.Abstractions/Runtime/CheatEngineRuntimeVersionInfo.cs
@@ -13,7 +13,7 @@ public CheatEngineRuntimeVersionInfo(
Version sdkAssemblyVersion)
{
if (observedCheatEngineVersion is { } observed &&
- (!double.IsFinite(observed) || observed < 0))
+ (!double.IsFinite(observed) || observed < 0))
{
throw new ArgumentOutOfRangeException(nameof(observedCheatEngineVersion), observed,
"The observed Cheat Engine version must be a finite non-negative number when supplied.");
diff --git a/libs/CheatEngine.Client.Abstractions/Scanning/AobPattern.cs b/libs/CheatEngine.Client.Abstractions/Scanning/AobPattern.cs
index aa3e46c..c8ac7c8 100644
--- a/libs/CheatEngine.Client.Abstractions/Scanning/AobPattern.cs
+++ b/libs/CheatEngine.Client.Abstractions/Scanning/AobPattern.cs
@@ -42,10 +42,16 @@ private AobPattern(string normalized, int byteLength)
}
/// Gets the normalized Cheat Engine pattern text.
- public string Value { get; }
+ public string Value
+ {
+ get;
+ }
/// Gets the number of byte positions represented by the pattern.
- public int ByteLength { get; }
+ public int ByteLength
+ {
+ get;
+ }
/// Gets whether every byte position is a wildcard.
public bool IsWildcardOnly
diff --git a/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordContentSnapshot.cs b/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordContentSnapshot.cs
index 7c59637..afced03 100644
--- a/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordContentSnapshot.cs
+++ b/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordContentSnapshot.cs
@@ -6,7 +6,8 @@ namespace CheatEngine.Client.Tables;
public readonly record struct MemoryRecordContentSnapshot
{
/// Creates copied content fields for a memory-record snapshot.
- public MemoryRecordContentSnapshot(string description, string addressExpression, string value, VariableType variableType)
+ public MemoryRecordContentSnapshot(string description, string addressExpression, string value,
+ VariableType variableType)
{
ArgumentNullException.ThrowIfNull(description);
ArgumentNullException.ThrowIfNull(addressExpression);
diff --git a/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordSearch.cs b/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordSearch.cs
index 6e9c5b9..a21e300 100644
--- a/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordSearch.cs
+++ b/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordSearch.cs
@@ -37,14 +37,26 @@ public MemoryRecordSearch(
}
/// Gets the case-insensitive description substring predicate.
- public string? DescriptionContains { get; }
+ public string? DescriptionContains
+ {
+ get;
+ }
/// Gets the case-insensitive exact address-expression predicate.
- public string? AddressExpression { get; }
+ public string? AddressExpression
+ {
+ get;
+ }
/// Gets the optional exact Cheat Engine value-type predicate.
- public VariableType? VariableType { get; }
+ public VariableType? VariableType
+ {
+ get;
+ }
/// Gets the optional active/frozen state predicate.
- public bool? IsActive { get; }
+ public bool? IsActive
+ {
+ get;
+ }
}
diff --git a/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordSnapshot.cs b/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordSnapshot.cs
index 2d0b6f7..e9ca221 100644
--- a/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordSnapshot.cs
+++ b/libs/CheatEngine.Client.Abstractions/Tables/MemoryRecordSnapshot.cs
@@ -53,44 +53,23 @@ public MemoryRecordStateSnapshot State
}
/// Gets the record display description.
- public string Description
- {
- get => Content.Description;
- }
+ public string Description => Content.Description;
/// Gets the record's unresolved Cheat Engine address expression.
- public string AddressExpression
- {
- get => Content.AddressExpression;
- }
+ public string AddressExpression => Content.AddressExpression;
/// Gets the record's verbatim value text.
- public string Value
- {
- get => Content.Value;
- }
+ public string Value => Content.Value;
/// Gets the record's Cheat Engine value type.
- public VariableType VariableType
- {
- get => Content.VariableType;
- }
+ public VariableType VariableType => Content.VariableType;
/// Gets the currently resolved target address when it could be obtained.
- public Address? CurrentAddress
- {
- get => State.CurrentAddress;
- }
+ public Address? CurrentAddress => State.CurrentAddress;
/// Gets whether Cheat Engine reports this record as active or frozen.
- public bool IsActive
- {
- get => State.IsActive;
- }
+ public bool IsActive => State.IsActive;
/// Gets the number of immediate child records reported by Cheat Engine.
- public int ChildCount
- {
- get => State.ChildCount;
- }
+ public int ChildCount => State.ChildCount;
}
diff --git a/libs/CheatEngine.Client.Binding/CheatEngine.Client.Binding.csproj b/libs/CheatEngine.Client.Binding/CheatEngine.Client.Binding.csproj
deleted file mode 100644
index 9a9b672..0000000
--- a/libs/CheatEngine.Client.Binding/CheatEngine.Client.Binding.csproj
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
diff --git a/libs/CheatEngine.Client.Binding/README.md b/libs/CheatEngine.Client.Binding/README.md
deleted file mode 100644
index 0480aa2..0000000
--- a/libs/CheatEngine.Client.Binding/README.md
+++ /dev/null
@@ -1,11 +0,0 @@
-# CheatEngine.Client.Binding
-
-The mapper/binder layer of CheatEngine.Client: it implements the contracts declared in `CheatEngine.Client.Abstractions`
-by mapping them onto CheatEngine.SDK.
-
-## Rules
-
-- References `CheatEngine.Client.Abstractions` only. It never references `CheatEngine.Client.Fluent`.
-- The only project meant to depend on CheatEngine.SDK. This is the default layering rule: revisit it if
- `CheatEngine.Client.Abstractions` ever reuses CheatEngine.SDK vocabulary.
-- Internal by default: only what `CheatEngine.Client` composes is public.
diff --git a/libs/CheatEngine.Client.Core/CheatEngine.Client.Core.csproj b/libs/CheatEngine.Client.Core/CheatEngine.Client.Core.csproj
new file mode 100644
index 0000000..cb7032f
--- /dev/null
+++ b/libs/CheatEngine.Client.Core/CheatEngine.Client.Core.csproj
@@ -0,0 +1,19 @@
+
+
+
+ true
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/libs/CheatEngine.Client.Core/CheatEngineClient.cs b/libs/CheatEngine.Client.Core/CheatEngineClient.cs
new file mode 100644
index 0000000..4ce4fc2
--- /dev/null
+++ b/libs/CheatEngine.Client.Core/CheatEngineClient.cs
@@ -0,0 +1,84 @@
+using CheatEngine.Client.Core.Infrastructure;
+using CheatEngine.Client.Dispatching;
+using CheatEngine.Client.Inspection;
+using CheatEngine.Client.Lua;
+using CheatEngine.Client.Memory;
+using CheatEngine.Client.Processes;
+using CheatEngine.Client.Runtime;
+using CheatEngine.Client.Scanning;
+using CheatEngine.Client.Tables;
+
+namespace CheatEngine.Client.Core;
+
+internal sealed class CheatEngineClient : ICheatEngineClient
+{
+ private readonly CoreLifetime _lifetime;
+
+ internal CheatEngineClient(
+ CoreLifetime lifetime,
+ CheatEngineClientRuntimeServices runtimeServices,
+ CheatEngineClientDomainServices domainServices)
+ {
+ _lifetime = lifetime ?? throw new ArgumentNullException(nameof(lifetime));
+ ArgumentNullException.ThrowIfNull(runtimeServices);
+ ArgumentNullException.ThrowIfNull(domainServices);
+
+ Runtime = runtimeServices.Runtime ?? throw new ArgumentNullException(nameof(runtimeServices));
+ Dispatcher = runtimeServices.Dispatcher ?? throw new ArgumentNullException(nameof(runtimeServices));
+ Processes = domainServices.Processes ?? throw new ArgumentNullException(nameof(domainServices));
+ Memory = domainServices.Memory ?? throw new ArgumentNullException(nameof(domainServices));
+ Patterns = domainServices.Patterns ?? throw new ArgumentNullException(nameof(domainServices));
+ Scans = domainServices.Scans ?? throw new ArgumentNullException(nameof(domainServices));
+ Inspection = domainServices.Inspection ?? throw new ArgumentNullException(nameof(domainServices));
+ Tables = domainServices.Tables ?? throw new ArgumentNullException(nameof(domainServices));
+ Lua = domainServices.Lua ?? throw new ArgumentNullException(nameof(domainServices));
+ }
+
+ public long Epoch => _lifetime.Epoch;
+ public CancellationToken Stopping => _lifetime.Stopping;
+
+ public ICheatEngineRuntime Runtime
+ {
+ get;
+ }
+
+ public ICheatEngineDispatcher Dispatcher
+ {
+ get;
+ }
+
+ public IProcessClient Processes
+ {
+ get;
+ }
+
+ public IMemoryClient Memory
+ {
+ get;
+ }
+
+ public IPatternScanner Patterns
+ {
+ get;
+ }
+
+ public IValueScanner Scans
+ {
+ get;
+ }
+
+ public IInspectionClient Inspection
+ {
+ get;
+ }
+
+ public ITableClient Tables
+ {
+ get;
+ }
+
+ public ILuaClient Lua
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.Client.Core/CheatEngineClientDomainServices.cs b/libs/CheatEngine.Client.Core/CheatEngineClientDomainServices.cs
new file mode 100644
index 0000000..7ce2156
--- /dev/null
+++ b/libs/CheatEngine.Client.Core/CheatEngineClientDomainServices.cs
@@ -0,0 +1,18 @@
+using CheatEngine.Client.Inspection;
+using CheatEngine.Client.Lua;
+using CheatEngine.Client.Memory;
+using CheatEngine.Client.Processes;
+using CheatEngine.Client.Scanning;
+using CheatEngine.Client.Tables;
+
+namespace CheatEngine.Client.Core;
+
+/// Internal grouping of the independently consumable high-level Client domains.
+internal sealed record CheatEngineClientDomainServices(
+ IProcessClient Processes,
+ IMemoryClient Memory,
+ IPatternScanner Patterns,
+ IValueScanner Scans,
+ IInspectionClient Inspection,
+ ITableClient Tables,
+ ILuaClient Lua);
diff --git a/libs/CheatEngine.Client.Core/CheatEngineClientRuntimeServices.cs b/libs/CheatEngine.Client.Core/CheatEngineClientRuntimeServices.cs
new file mode 100644
index 0000000..18bdb18
--- /dev/null
+++ b/libs/CheatEngine.Client.Core/CheatEngineClientRuntimeServices.cs
@@ -0,0 +1,9 @@
+using CheatEngine.Client.Dispatching;
+using CheatEngine.Client.Runtime;
+
+namespace CheatEngine.Client.Core;
+
+/// Internal grouping of the façade services that describe the active runtime and its dispatch boundary.
+internal sealed record CheatEngineClientRuntimeServices(
+ ICheatEngineRuntime Runtime,
+ ICheatEngineDispatcher Dispatcher);
diff --git a/libs/CheatEngine.Client.Core/Dispatching/IMainThreadInvoker.cs b/libs/CheatEngine.Client.Core/Dispatching/IMainThreadInvoker.cs
new file mode 100644
index 0000000..db33040
--- /dev/null
+++ b/libs/CheatEngine.Client.Core/Dispatching/IMainThreadInvoker.cs
@@ -0,0 +1,8 @@
+namespace CheatEngine.Client.Core.Dispatching;
+
+internal interface IMainThreadInvoker
+{
+ public Exception? Invoke(Action callback);
+
+ public MainThreadInvocationResult Invoke(Func callback);
+}
diff --git a/libs/CheatEngine.Client.Core/Dispatching/MainThreadInvocationResult.cs b/libs/CheatEngine.Client.Core/Dispatching/MainThreadInvocationResult.cs
new file mode 100644
index 0000000..dd31ef8
--- /dev/null
+++ b/libs/CheatEngine.Client.Core/Dispatching/MainThreadInvocationResult.cs
@@ -0,0 +1,3 @@
+namespace CheatEngine.Client.Core.Dispatching;
+
+internal readonly record struct MainThreadInvocationResult(T Result, Exception? Exception);
diff --git a/libs/CheatEngine.Client.Core/Dispatching/SdkMainThreadDispatcher.cs b/libs/CheatEngine.Client.Core/Dispatching/SdkMainThreadDispatcher.cs
new file mode 100644
index 0000000..b4395f5
--- /dev/null
+++ b/libs/CheatEngine.Client.Core/Dispatching/SdkMainThreadDispatcher.cs
@@ -0,0 +1,189 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.ExceptionServices;
+
+using CheatEngine.Client.Core.Infrastructure;
+using CheatEngine.Client.Dispatching;
+using CheatEngine.Client.Results;
+using CheatEngine.SDK.Hosting.Threading;
+
+namespace CheatEngine.Client.Core.Dispatching;
+
+/// Adapts the SDK's synchronous main-thread dispatcher without retaining Lua state.
+internal sealed class SdkMainThreadDispatcher : ICheatEngineDispatcher
+{
+ private const string _invokeOperation = "Dispatcher.Invoke";
+
+ private readonly CoreLifetime _lifetime;
+ private readonly IMainThreadInvoker _mainThread;
+
+ internal SdkMainThreadDispatcher(CoreLifetime lifetime)
+ : this(lifetime, SdkMainThreadInvoker.Instance)
+ {
+ }
+
+ internal SdkMainThreadDispatcher(CoreLifetime lifetime, IMainThreadInvoker mainThread)
+ {
+ _lifetime = lifetime ?? throw new ArgumentNullException(nameof(lifetime));
+ _mainThread = mainThread ?? throw new ArgumentNullException(nameof(mainThread));
+ }
+
+ public bool IsMainThread => _lifetime.CanDispatch && MainThread.IsMainThread;
+
+ public bool TryInvoke(Action callback, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(callback);
+ _lifetime.ThrowIfDispatchAllowed(_invokeOperation);
+ if (cancellationToken.IsCancellationRequested)
+ {
+ failure = CoreFailureFactory.Cancelled(_invokeOperation);
+ return false;
+ }
+
+ Exception? callbackException;
+ try
+ {
+ callbackException = _mainThread.Invoke(callback);
+ }
+ catch (CheatEngineActivationExpiredException)
+ {
+ throw;
+ }
+ catch (Exception exception) when (!_lifetime.IsActivationCurrent)
+ {
+ throw new CheatEngineActivationExpiredException(_invokeOperation,
+ "The Cheat Engine plugin lifecycle changed while dispatching work.", exception);
+ }
+ catch (Exception exception)
+ {
+ failure = CoreFailureFactory.FromException(_invokeOperation, exception);
+ return false;
+ }
+
+ if (callbackException is not null)
+ {
+ RethrowCallback(callbackException);
+ }
+
+ failure = default;
+ return true;
+ }
+
+ public bool TryInvoke(Func callback, [MaybeNullWhen(false)] out T result, out CheatEngineFailure failure,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(callback);
+ _lifetime.ThrowIfDispatchAllowed(_invokeOperation);
+ if (cancellationToken.IsCancellationRequested)
+ {
+ result = default;
+ failure = CoreFailureFactory.Cancelled(_invokeOperation);
+ return false;
+ }
+
+ MainThreadInvocationResult callbackResult;
+ try
+ {
+ callbackResult = _mainThread.Invoke(callback);
+ }
+ catch (CheatEngineActivationExpiredException)
+ {
+ throw;
+ }
+ catch (Exception exception) when (!_lifetime.IsActivationCurrent)
+ {
+ result = default;
+ throw new CheatEngineActivationExpiredException(_invokeOperation,
+ "The Cheat Engine plugin lifecycle changed while dispatching work.", exception);
+ }
+ catch (Exception exception)
+ {
+ result = default;
+ failure = CoreFailureFactory.FromException(_invokeOperation, exception);
+ return false;
+ }
+
+ if (callbackResult.Exception is not null)
+ {
+ RethrowCallback(callbackResult.Exception);
+ }
+
+ result = callbackResult.Result;
+ failure = default;
+ return true;
+ }
+
+ public void Invoke(Action callback, CancellationToken cancellationToken = default)
+ {
+ if (!TryInvoke(callback, out CheatEngineFailure failure, cancellationToken))
+ {
+ _ = ThrowFailure