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.** + +[![CI](https://img.shields.io/github/actions/workflow/status/CheatEngineNet/CheatEngine.Client/main-ci.yml?branch=main&style=flat-square&logo=githubactions&logoColor=white&labelColor=24292f)](https://github.com/CheatEngineNet/CheatEngine.Client/actions/workflows/main-ci.yml) +[![NuGet](https://img.shields.io/nuget/vpre/CheatEngine.Client?style=flat-square&logo=nuget&logoColor=white&labelColor=24292f&color=004880)](https://www.nuget.org/packages/CheatEngine.Client) +[![.NET 10](https://img.shields.io/badge/.NET-10.0-512BD4?style=flat-square&logo=dotnet&logoColor=white&labelColor=24292f)](https://dotnet.microsoft.com/download/dotnet/10.0) +[![Windows x64](https://img.shields.io/badge/platform-Windows%20x64-0078D4?style=flat-square&labelColor=24292f)](#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.