arch(runtime): establish presentation-independent boundary
Create the dependency-only Runtime project before moving any gameplay owner, enforce its direct, transitive, source, and load-time closure, and pin coherent lifetime-group extraction plus retryable teardown ordering. No production behavior changes in J0. Validated by the four focused Runtime boundary tests, a zero-error Release solution build, and 8,406 passing Release tests with five pre-existing skips. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
9f37d17936
commit
b632672e5c
10 changed files with 457 additions and 2 deletions
|
|
@ -8,6 +8,7 @@
|
||||||
<Project Path="src/AcDream.Core.Net/AcDream.Core.Net.csproj" />
|
<Project Path="src/AcDream.Core.Net/AcDream.Core.Net.csproj" />
|
||||||
<Project Path="src/AcDream.Plugin.Abstractions/AcDream.Plugin.Abstractions.csproj" />
|
<Project Path="src/AcDream.Plugin.Abstractions/AcDream.Plugin.Abstractions.csproj" />
|
||||||
<Project Path="src/AcDream.Plugins.Smoke/AcDream.Plugins.Smoke.csproj" />
|
<Project Path="src/AcDream.Plugins.Smoke/AcDream.Plugins.Smoke.csproj" />
|
||||||
|
<Project Path="src/AcDream.Runtime/AcDream.Runtime.csproj" />
|
||||||
<Project Path="src/AcDream.UI.Abstractions/AcDream.UI.Abstractions.csproj" />
|
<Project Path="src/AcDream.UI.Abstractions/AcDream.UI.Abstractions.csproj" />
|
||||||
<Project Path="src/AcDream.UI.ImGui/AcDream.UI.ImGui.csproj" />
|
<Project Path="src/AcDream.UI.ImGui/AcDream.UI.ImGui.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
|
|
@ -22,6 +23,7 @@
|
||||||
<Project Path="tests/AcDream.Core.Tests.Fixtures.HelloPlugin/AcDream.Core.Tests.Fixtures.HelloPlugin.csproj" />
|
<Project Path="tests/AcDream.Core.Tests.Fixtures.HelloPlugin/AcDream.Core.Tests.Fixtures.HelloPlugin.csproj" />
|
||||||
<Project Path="tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj" />
|
<Project Path="tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj" />
|
||||||
<Project Path="tests/AcDream.Core.Net.Tests/AcDream.Core.Net.Tests.csproj" />
|
<Project Path="tests/AcDream.Core.Net.Tests/AcDream.Core.Net.Tests.csproj" />
|
||||||
|
<Project Path="tests/AcDream.Runtime.Tests/AcDream.Runtime.Tests.csproj" />
|
||||||
<Project Path="tests/AcDream.UI.Abstractions.Tests/AcDream.UI.Abstractions.Tests.csproj" />
|
<Project Path="tests/AcDream.UI.Abstractions.Tests/AcDream.UI.Abstractions.Tests.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
</Solution>
|
</Solution>
|
||||||
|
|
|
||||||
|
|
@ -203,6 +203,11 @@ src/
|
||||||
NetClient.cs -> done
|
NetClient.cs -> done
|
||||||
Messages/ -> done (CreateObject, MoveToState, etc.)
|
Messages/ -> done (CreateObject, MoveToState, etc.)
|
||||||
|
|
||||||
|
AcDream.Runtime/ presentation-independent client kernel boundary
|
||||||
|
RuntimeAssemblyMarker.cs -> J0 dependency boundary; owner moves start after I6/I7
|
||||||
|
-> may reference Core, Core.Net, Content, and Plugin.Abstractions only
|
||||||
|
-> must never reference App, UI, Silk.NET, OpenAL, Arch, or ImGui
|
||||||
|
|
||||||
AcDream.Plugin.Abstractions/ Layer 5: plugin interfaces
|
AcDream.Plugin.Abstractions/ Layer 5: plugin interfaces
|
||||||
IAcDreamPlugin.cs -> done
|
IAcDreamPlugin.cs -> done
|
||||||
IPluginHost.cs -> done
|
IPluginHost.cs -> done
|
||||||
|
|
|
||||||
|
|
@ -166,6 +166,7 @@ Today:
|
||||||
|
|
||||||
- `tests/AcDream.Core.Tests/` ← `src/AcDream.Core/`
|
- `tests/AcDream.Core.Tests/` ← `src/AcDream.Core/`
|
||||||
- `tests/AcDream.Core.Net.Tests/` ← `src/AcDream.Core.Net/`
|
- `tests/AcDream.Core.Net.Tests/` ← `src/AcDream.Core.Net/`
|
||||||
|
- `tests/AcDream.Runtime.Tests/` ← `src/AcDream.Runtime/`
|
||||||
- `tests/AcDream.UI.Abstractions.Tests/` ← `src/AcDream.UI.Abstractions/`
|
- `tests/AcDream.UI.Abstractions.Tests/` ← `src/AcDream.UI.Abstractions/`
|
||||||
- `tests/AcDream.App.Tests/` ← `src/AcDream.App/`
|
- `tests/AcDream.App.Tests/` ← `src/AcDream.App/`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,11 @@
|
||||||
|
|
||||||
**Date:** 2026-07-24
|
**Date:** 2026-07-24
|
||||||
|
|
||||||
**Status:** Slices A–H and I0–I3 are complete. Slices F–L were explicitly
|
**Status:** Slices A–H are complete. Slice I6's flat-authoritative production
|
||||||
approved 2026-07-24; Slice I4 is active.
|
cutover is committed and awaits the final user collision/visual gate before
|
||||||
|
parsed-graph deletion. Slices F–L were explicitly approved 2026-07-24. Slice
|
||||||
|
J0's dependency-boundary preparation is complete without moving gameplay
|
||||||
|
owners. J1 remains behind the I6/I7 user collision/visual closeout.
|
||||||
|
|
||||||
**Scope:** Reconcile and sequence the existing Modern Pipeline (`MP`) and
|
**Scope:** Reconcile and sequence the existing Modern Pipeline (`MP`) and
|
||||||
Linux/headless (`LH`) tracks using the 2026-07-24 connected performance audit.
|
Linux/headless (`LH`) tracks using the 2026-07-24 connected performance audit.
|
||||||
|
|
@ -499,6 +502,41 @@ It exposes:
|
||||||
- Deterministic `Tick`.
|
- Deterministic `Tick`.
|
||||||
- Retryable, complete shutdown.
|
- Retryable, complete shutdown.
|
||||||
|
|
||||||
|
#### 9.2.1 Structural teardown protocol
|
||||||
|
|
||||||
|
Every graphical or headless host follows one retryable, generation-scoped
|
||||||
|
teardown transaction. New runtime owners register through the same
|
||||||
|
`CompositionAcquisitionScope` lease discipline already used by the graphical
|
||||||
|
composition root; release order is encoded by leases and acknowledgements,
|
||||||
|
never remembered by callers.
|
||||||
|
|
||||||
|
1. **Cancel scheduler generations.** Make commands inert, cancel the active
|
||||||
|
session/runtime generation, stop new receive publication, and prevent any
|
||||||
|
timer, bot, plugin, or worker callback from creating more work for it.
|
||||||
|
2. **Drain or poison journals and queues.** Detach inbound producers, drain
|
||||||
|
already-accepted ordered events through the generation boundary, then
|
||||||
|
poison every command/event/delta queue so delayed callbacks fail against
|
||||||
|
their exact generation instead of entering a replacement session.
|
||||||
|
3. **Unpublish presentation.** The graphical host consumes the final ordered
|
||||||
|
withdrawals and acknowledges that the render scene, retained gameplay UI,
|
||||||
|
audio/effect projections, selection, radar, and private viewports no longer
|
||||||
|
expose the runtime generation. Headless hosts acknowledge this stage
|
||||||
|
immediately because they construct no presentation owners.
|
||||||
|
4. **Release residency after fences.** Withdraw content owners, finish
|
||||||
|
streaming/landblock retirement, and allow GPU-backed stores to retire only
|
||||||
|
after their existing frame-flight fences signal. A failed fence or release
|
||||||
|
retains the exact unfinished lease and resumes without replaying completed
|
||||||
|
stages.
|
||||||
|
5. **Release the shared `ContentStore` last.** Session-scoped collision/content
|
||||||
|
leases go away before the runtime instance. The immutable mapped content
|
||||||
|
store outlives every runtime and presentation host and is disposed only by
|
||||||
|
the process host after all registered sessions have acknowledged teardown.
|
||||||
|
|
||||||
|
Logout, session reset, mid-portal disconnect, reconnect replacement, ordinary
|
||||||
|
window close, and construction rollback all use this protocol. No stage may
|
||||||
|
clear another generation's state, and no teardown failure may be converted
|
||||||
|
into a best-effort purge.
|
||||||
|
|
||||||
### 9.3 Hosts
|
### 9.3 Hosts
|
||||||
|
|
||||||
`GraphicalGameHost` provides input, camera, rendering, UI, audio, frame pacing,
|
`GraphicalGameHost` provides input, camera, rendering, UI, audio, frame pacing,
|
||||||
|
|
|
||||||
230
docs/plans/2026-07-25-modern-runtime-slice-j.md
Normal file
230
docs/plans/2026-07-25-modern-runtime-slice-j.md
Normal file
|
|
@ -0,0 +1,230 @@
|
||||||
|
# Modern runtime Slice J — presentation-independent runtime
|
||||||
|
|
||||||
|
**Status:** J0 COMPLETE — lifetime plan, project boundary, and dependency
|
||||||
|
guards; J1 waits for the Slice I6/I7 user collision/visual closeout
|
||||||
|
**Parent:** `2026-07-24-modern-runtime-architecture.md`, Slice J
|
||||||
|
**Authorization:** the user approved Slices F–L, including the otherwise-frozen
|
||||||
|
Slice J gameplay-owner moves, on 2026-07-24
|
||||||
|
**Purpose:** move the existing authoritative client kernel into a
|
||||||
|
presentation-independent assembly without creating a second world, changing
|
||||||
|
retail behavior, or making the graphical host a special case.
|
||||||
|
|
||||||
|
Slice J is a lifetime-group campaign, not one large move. Each sub-slice keeps
|
||||||
|
the graphical client on the same owner instance, establishes host parity, and
|
||||||
|
deletes the replaced App production path before the next group moves.
|
||||||
|
|
||||||
|
## 1. Fixed invariants
|
||||||
|
|
||||||
|
1. `LiveEntityRuntime` remains the sole server-GUID/incarnation owner. There is
|
||||||
|
never a second GUID map or synchronized gameplay world.
|
||||||
|
2. Runtime types do not reference `AcDream.App`, either UI assembly, Silk.NET,
|
||||||
|
OpenAL, Arch, ImGui, a native window, or an OpenGL resource.
|
||||||
|
3. App projection state is keyed by runtime-issued local entity identity plus
|
||||||
|
incarnation. It may not recover or mirror server GUID ownership.
|
||||||
|
4. The accepted update order, packet ordering, timestamp gates, outbound
|
||||||
|
cadence, and retail-shaped gameplay algorithms do not change during the
|
||||||
|
extraction.
|
||||||
|
5. Contracts expose borrowed/immutable views, typed commands, and ordered
|
||||||
|
deltas. They do not expose App render components or deep per-frame object
|
||||||
|
graphs.
|
||||||
|
6. All clocks, queues, random sources, diagnostics identity, plugin behavior,
|
||||||
|
and mutable caches that can differ between sessions are instance-scoped.
|
||||||
|
7. Immutable prepared content may be shared. Session state and mutable query
|
||||||
|
scratch may not.
|
||||||
|
8. Every new owner participates in the structural teardown protocol in parent
|
||||||
|
plan §9.2.1 through acquisition leases and exact acknowledgements.
|
||||||
|
9. A headless construction path is proved after every coherent move. “No
|
||||||
|
window” means no App, Silk, OpenAL, retained UI, particle, or render assembly
|
||||||
|
is loaded—not a hidden graphical process.
|
||||||
|
10. The user collision/visual gate for Slice I6 still controls parsed-graph
|
||||||
|
deletion. J0 may land independently; gameplay-owner moves begin only after
|
||||||
|
the active I6/I7 closeout has reconciled the final collision owner shape.
|
||||||
|
|
||||||
|
## 2. Target dependency direction
|
||||||
|
|
||||||
|
```text
|
||||||
|
AcDream.App ───────────────► AcDream.Runtime
|
||||||
|
AcDream.Headless (Slice K) ─► AcDream.Runtime
|
||||||
|
├──► AcDream.Core.Net
|
||||||
|
├──► AcDream.Core
|
||||||
|
├──► AcDream.Content
|
||||||
|
└──► AcDream.Plugin.Abstractions
|
||||||
|
|
||||||
|
AcDream.Runtime -X► AcDream.App / AcDream.UI.* / Silk.NET / OpenAL / Arch
|
||||||
|
```
|
||||||
|
|
||||||
|
Runtime-emitted entity events contain incarnation, transform, flags, and
|
||||||
|
property deltas only. An App-side projection adapter translates them into
|
||||||
|
`IRenderScene` journal operations. Render vocabulary never enters Runtime.
|
||||||
|
|
||||||
|
## 3. Teardown transaction
|
||||||
|
|
||||||
|
The exact order is:
|
||||||
|
|
||||||
|
1. cancel scheduler/session generations and make commands inert;
|
||||||
|
2. detach producers, drain accepted ordered events, and poison queues;
|
||||||
|
3. withdraw and acknowledge graphical presentation;
|
||||||
|
4. retire session residence and GPU-backed owners through existing fences;
|
||||||
|
5. dispose the process-owned immutable `ContentStore` only after every runtime
|
||||||
|
and host has completed.
|
||||||
|
|
||||||
|
Completed stages never replay. Failure retains the exact remaining suffix.
|
||||||
|
Logout, reset, mid-portal disconnect, reconnect replacement, construction
|
||||||
|
rollback, and native close all traverse the same transaction.
|
||||||
|
|
||||||
|
## 4. Execution slices
|
||||||
|
|
||||||
|
### J0 — boundary, plan, and dependency enforcement
|
||||||
|
|
||||||
|
**Completed 2026-07-25.**
|
||||||
|
|
||||||
|
- Add `AcDream.Runtime` and `AcDream.Runtime.Tests`.
|
||||||
|
- Permit references only to Core, Core.Net, Content, and Plugin.Abstractions.
|
||||||
|
- Add App → Runtime as the future host dependency; Runtime never references
|
||||||
|
App.
|
||||||
|
- Add direct-reference, dependency-closure, source-project, and assembly-load
|
||||||
|
guards that fail if a presentation/backend dependency enters Runtime.
|
||||||
|
- Add only an assembly boundary marker; do not introduce a facade
|
||||||
|
`GameRuntime`, mirror state, or move behavior in J0.
|
||||||
|
- Pin this lifetime-group plan and parent-plan teardown protocol.
|
||||||
|
|
||||||
|
Gate: Release build and full tests pass; the Runtime test process loads no App,
|
||||||
|
UI, Silk, OpenAL, Arch, or ImGui assembly; graphical behavior is byte-for-byte
|
||||||
|
unchanged because no runtime path changed.
|
||||||
|
|
||||||
|
Evidence: four focused dependency tests pass, the Release solution builds with
|
||||||
|
zero errors, and the complete Release suite passes 8,406 tests with five
|
||||||
|
pre-existing skips. The exact rollback commit is recorded below after landing.
|
||||||
|
|
||||||
|
### J1 — read, command, event, clock, and lifecycle contracts
|
||||||
|
|
||||||
|
- Define `IGameRuntimeView`, typed command interfaces, ordered event/delta
|
||||||
|
records, instance clock, lifecycle states, generation tokens, and teardown
|
||||||
|
acknowledgements.
|
||||||
|
- Build App adapters over the current owners first. Adapters borrow; they do
|
||||||
|
not copy mutable collections or own lifetime.
|
||||||
|
- Add a normalized parity trace of accepted inbound events, runtime commands,
|
||||||
|
state revisions, and lifecycle edges.
|
||||||
|
- Prove press-time graphical input reaches the same current owners through the
|
||||||
|
command contract with no extra frame of latency.
|
||||||
|
|
||||||
|
Gate: adapter and direct-host traces are identical over deterministic session,
|
||||||
|
entity, inventory, chat, movement, and portal fixtures; no canonical owner has
|
||||||
|
moved yet.
|
||||||
|
|
||||||
|
### J2 — session lifetime and ordered transport group
|
||||||
|
|
||||||
|
- Remove App presentation dependencies from `LiveSessionController`,
|
||||||
|
`LiveSessionHost`, lifecycle host, event router, and command router.
|
||||||
|
- Move the same owner instances into Runtime with Core.Net transport and
|
||||||
|
instance-scoped clocks/queues.
|
||||||
|
- Preserve bind-before-connect, publish-after-EnterWorld, exact generation
|
||||||
|
rechecks, receive ordering, ack placement, graceful F653/disconnect, and
|
||||||
|
retryable replacement.
|
||||||
|
- Let App provide only endpoint credentials, presentation bindings, and
|
||||||
|
graphical lifecycle acknowledgements.
|
||||||
|
|
||||||
|
Gate: connected login/logout/reconnect and malformed/reentrant lifecycle suites
|
||||||
|
produce the same packet/order trace; no-window construction can connect and
|
||||||
|
disconnect without loading App or a backend.
|
||||||
|
|
||||||
|
### J3 — canonical identity, properties, and object-table group
|
||||||
|
|
||||||
|
- Strip renderer, particle, Wb, and streaming components from the canonical
|
||||||
|
record into an App projection store keyed only by local identity/incarnation.
|
||||||
|
- Move `LiveEntityRuntime`, accepted spawn/state/property timestamps,
|
||||||
|
`ClientObjectTable`, container/inventory indices, vitals, enchantments,
|
||||||
|
spell state, target/combat state, and their exact teardown into Runtime.
|
||||||
|
- Emit ordered create/update/rebucket/hidden/withdraw/delete deltas.
|
||||||
|
- Keep DAT-backed visual hydration, render registrations, effects, lights,
|
||||||
|
paperdoll, selection markers, and radar projections in App.
|
||||||
|
|
||||||
|
Gate: GUID reuse, equal-generation updates, parent/child identity,
|
||||||
|
inventory/container mutations, Hidden, delete/recreate, and portal teardown
|
||||||
|
match current traces; App has no server-GUID ownership.
|
||||||
|
|
||||||
|
### J4 — chat, inventory, magic, and gameplay-state services
|
||||||
|
|
||||||
|
- Move canonical chat history/channel state, inventory transactions, spell and
|
||||||
|
component state, enchantments, vitals, skills/attributes, and gameplay
|
||||||
|
cooldown state by coherent owner groups.
|
||||||
|
- Keep retained panel layout, text shaping, icons, paperdoll rendering, and
|
||||||
|
status-bar presentation in App.
|
||||||
|
- Route plugins and future bots through the same typed command/event surface
|
||||||
|
as graphical UI.
|
||||||
|
|
||||||
|
Gate: deterministic command/response traces and graphical ViewModel revisions
|
||||||
|
match; no Runtime type references UI abstractions.
|
||||||
|
|
||||||
|
### J5 — movement, physics, interaction, and combat group
|
||||||
|
|
||||||
|
- Move presentation-free movement interpretation, authoritative/predicted
|
||||||
|
physics state, selection identity, approach/use transactions, combat intent,
|
||||||
|
projectiles, and outbound movement cadence.
|
||||||
|
- Replace input and camera dependencies with typed commands and runtime views.
|
||||||
|
- Keep camera, mouse look, selection markers/highlight, combat/spell bars,
|
||||||
|
animated pose composition, sounds, and particles in App.
|
||||||
|
- Share immutable flat collision content while retaining per-session transition
|
||||||
|
scratch.
|
||||||
|
|
||||||
|
Gate: bit-identical collision/trajectory fixtures, target-facing and
|
||||||
|
auto-approach traces, outbound packet timing, combat/magic commands, and
|
||||||
|
graphical feel gates remain unchanged.
|
||||||
|
|
||||||
|
### J6 — world, portal, environment, and projection handshake
|
||||||
|
|
||||||
|
- Move authoritative teleport/session cell identity, world clock, weather
|
||||||
|
state, and reveal-generation state required by gameplay.
|
||||||
|
- Keep streaming publication, terrain/EnvCell rendering, portal tunnel,
|
||||||
|
materialization VFX, sky drawing, audio, and UI in App.
|
||||||
|
- Define the destination-ready/withdrawal acknowledgements between Runtime and
|
||||||
|
the graphical or headless host without making visual readiness gameplay
|
||||||
|
authority.
|
||||||
|
|
||||||
|
Gate: fresh login, repeated recall/portal, world edge, dungeon, reconnect, and
|
||||||
|
mid-portal disconnect traces match; the graphical screenshots remain accepted.
|
||||||
|
|
||||||
|
### J7 — one `GameRuntime` composition root
|
||||||
|
|
||||||
|
- Compose all moved lifetime groups into one instance-scoped `GameRuntime`.
|
||||||
|
- Make `GameWindow`/App own exactly one Runtime instance plus presentation
|
||||||
|
adapters.
|
||||||
|
- Delete temporary direct-owner adapters and every superseded App production
|
||||||
|
path.
|
||||||
|
- Register all Runtime and App projection owners with structural acquisition
|
||||||
|
leases and the exact teardown transaction.
|
||||||
|
|
||||||
|
Gate: graphical connected lifecycle/resource routes pass with no mirrored
|
||||||
|
state, no extra update latency, and no performance regression.
|
||||||
|
|
||||||
|
### J8 — no-window integration and Slice J closeout
|
||||||
|
|
||||||
|
- Build a transport/content test host that constructs Runtime directly.
|
||||||
|
- Connect, enter world, receive chat/inventory/world updates, move, use,
|
||||||
|
fight/cast through deterministic fixtures, portal, log out, reconnect, and
|
||||||
|
tear down without loading App/Silk/OpenAL/UI.
|
||||||
|
- Add cancellation, failure injection, same-process multiple-instance
|
||||||
|
isolation, credential-safe diagnostics, and zero-presentation-allocation
|
||||||
|
gates.
|
||||||
|
- Update architecture, milestone, roadmap, issue/divergence ledgers, and
|
||||||
|
durable memory.
|
||||||
|
|
||||||
|
Final gate: Release build/full tests, connected graphical route, no-window
|
||||||
|
integration, resource teardown, packet/order/timing parity, and the user visual
|
||||||
|
matrix all pass. Slice K may then build the Linux multi-session host without
|
||||||
|
another gameplay extraction.
|
||||||
|
|
||||||
|
## 5. Commit order
|
||||||
|
|
||||||
|
1. `arch(runtime): establish presentation-independent boundary`
|
||||||
|
2. `feat(runtime): define borrowed views commands and ordered events`
|
||||||
|
3. `refactor(runtime): move session lifetime and ordered transport`
|
||||||
|
4. `refactor(runtime): move canonical entity and property ownership`
|
||||||
|
5. `refactor(runtime): move gameplay state services`
|
||||||
|
6. `refactor(runtime): move movement physics interaction and combat`
|
||||||
|
7. `refactor(runtime): separate world authority from presentation`
|
||||||
|
8. `refactor(runtime): compose one graphical game runtime`
|
||||||
|
9. `test(runtime): close no-window parity and teardown`
|
||||||
|
|
||||||
|
Each commit is independently buildable and retains an exact rollback command
|
||||||
|
in this plan before its connected gate.
|
||||||
|
|
@ -39,6 +39,7 @@
|
||||||
<PackageReference Include="StbTrueTypeSharp" Version="1.26.12" />
|
<PackageReference Include="StbTrueTypeSharp" Version="1.26.12" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\AcDream.Runtime\AcDream.Runtime.csproj" />
|
||||||
<ProjectReference Include="..\AcDream.Core\AcDream.Core.csproj" />
|
<ProjectReference Include="..\AcDream.Core\AcDream.Core.csproj" />
|
||||||
<ProjectReference Include="..\AcDream.Core.Net\AcDream.Core.Net.csproj" />
|
<ProjectReference Include="..\AcDream.Core.Net\AcDream.Core.Net.csproj" />
|
||||||
<ProjectReference Include="..\AcDream.Content\AcDream.Content.csproj" />
|
<ProjectReference Include="..\AcDream.Content\AcDream.Content.csproj" />
|
||||||
|
|
|
||||||
19
src/AcDream.Runtime/AcDream.Runtime.csproj
Normal file
19
src/AcDream.Runtime/AcDream.Runtime.csproj
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
<RootNamespace>AcDream.Runtime</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<InternalsVisibleTo Include="AcDream.Runtime.Tests" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\AcDream.Core\AcDream.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\AcDream.Core.Net\AcDream.Core.Net.csproj" />
|
||||||
|
<ProjectReference Include="..\AcDream.Content\AcDream.Content.csproj" />
|
||||||
|
<ProjectReference Include="..\AcDream.Plugin.Abstractions\AcDream.Plugin.Abstractions.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
10
src/AcDream.Runtime/RuntimeAssemblyMarker.cs
Normal file
10
src/AcDream.Runtime/RuntimeAssemblyMarker.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
namespace AcDream.Runtime;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Identifies the presentation-independent runtime assembly.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Slice J0 establishes only the dependency boundary. Runtime owners move here
|
||||||
|
/// in coherent lifetime groups during the later Slice J steps.
|
||||||
|
/// </remarks>
|
||||||
|
internal static class RuntimeAssemblyMarker;
|
||||||
22
tests/AcDream.Runtime.Tests/AcDream.Runtime.Tests.csproj
Normal file
22
tests/AcDream.Runtime.Tests/AcDream.Runtime.Tests.csproj
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\AcDream.Runtime\AcDream.Runtime.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
127
tests/AcDream.Runtime.Tests/RuntimeDependencyBoundaryTests.cs
Normal file
127
tests/AcDream.Runtime.Tests/RuntimeDependencyBoundaryTests.cs
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
|
||||||
|
namespace AcDream.Runtime.Tests;
|
||||||
|
|
||||||
|
public sealed class RuntimeDependencyBoundaryTests
|
||||||
|
{
|
||||||
|
private static readonly string[] ForbiddenDependencyPrefixes =
|
||||||
|
[
|
||||||
|
"AcDream.App",
|
||||||
|
"AcDream.UI.",
|
||||||
|
"Silk.NET",
|
||||||
|
"OpenAL",
|
||||||
|
"Arch",
|
||||||
|
"ImGui",
|
||||||
|
];
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RuntimeAssemblyHasNoDirectPresentationOrBackendReferences()
|
||||||
|
{
|
||||||
|
var references = typeof(RuntimeAssemblyMarker).Assembly
|
||||||
|
.GetReferencedAssemblies()
|
||||||
|
.Select(static reference => reference.Name ?? string.Empty)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
Assert.DoesNotContain(references, IsForbidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RuntimeDependencyClosureHasNoPresentationOrBackendLibraries()
|
||||||
|
{
|
||||||
|
var depsPath = Path.ChangeExtension(
|
||||||
|
typeof(RuntimeDependencyBoundaryTests).Assembly.Location,
|
||||||
|
".deps.json");
|
||||||
|
|
||||||
|
using var document = JsonDocument.Parse(File.ReadAllText(depsPath));
|
||||||
|
var libraries = document.RootElement
|
||||||
|
.GetProperty("libraries")
|
||||||
|
.EnumerateObject()
|
||||||
|
.Select(static library => LibraryName(library.Name))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
Assert.DoesNotContain(libraries, IsForbidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RuntimeProjectDeclaresOnlyApprovedProjectDependencies()
|
||||||
|
{
|
||||||
|
var repositoryRoot = FindRepositoryRoot();
|
||||||
|
var projectPath = Path.Combine(
|
||||||
|
repositoryRoot,
|
||||||
|
"src",
|
||||||
|
"AcDream.Runtime",
|
||||||
|
"AcDream.Runtime.csproj");
|
||||||
|
var project = XDocument.Load(projectPath);
|
||||||
|
var projectDirectory = Path.GetDirectoryName(projectPath)!;
|
||||||
|
|
||||||
|
var actualReferences = project
|
||||||
|
.Descendants("ProjectReference")
|
||||||
|
.Select(reference => reference.Attribute("Include")?.Value)
|
||||||
|
.Where(static include => !string.IsNullOrWhiteSpace(include))
|
||||||
|
.Select(include => Path.GetFullPath(Path.Combine(projectDirectory, include!)))
|
||||||
|
.Order(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray();
|
||||||
|
var expectedReferences = new[]
|
||||||
|
{
|
||||||
|
"AcDream.Content",
|
||||||
|
"AcDream.Core",
|
||||||
|
"AcDream.Core.Net",
|
||||||
|
"AcDream.Plugin.Abstractions",
|
||||||
|
}
|
||||||
|
.Select(projectName => Path.Combine(
|
||||||
|
repositoryRoot,
|
||||||
|
"src",
|
||||||
|
projectName,
|
||||||
|
$"{projectName}.csproj"))
|
||||||
|
.Order(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
Assert.Equal(expectedReferences, actualReferences);
|
||||||
|
Assert.Empty(project.Descendants("PackageReference"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LoadingRuntimeMarkerDoesNotLoadPresentationOrBackendAssemblies()
|
||||||
|
{
|
||||||
|
_ = typeof(RuntimeAssemblyMarker).Assembly;
|
||||||
|
|
||||||
|
var loadedAssemblies = AppDomain.CurrentDomain
|
||||||
|
.GetAssemblies()
|
||||||
|
.Select(static assembly => assembly.GetName().Name ?? string.Empty)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
Assert.DoesNotContain(loadedAssemblies, IsForbidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FindRepositoryRoot()
|
||||||
|
{
|
||||||
|
var directory = new DirectoryInfo(AppContext.BaseDirectory);
|
||||||
|
while (directory is not null)
|
||||||
|
{
|
||||||
|
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
|
||||||
|
{
|
||||||
|
return directory.FullName;
|
||||||
|
}
|
||||||
|
|
||||||
|
directory = directory.Parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new DirectoryNotFoundException(
|
||||||
|
$"Could not find AcDream.slnx above {AppContext.BaseDirectory}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string LibraryName(string libraryIdentity)
|
||||||
|
{
|
||||||
|
var separatorIndex = libraryIdentity.IndexOf('/');
|
||||||
|
return separatorIndex < 0
|
||||||
|
? libraryIdentity
|
||||||
|
: libraryIdentity[..separatorIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsForbidden(string assemblyName)
|
||||||
|
{
|
||||||
|
return ForbiddenDependencyPrefixes.Any(prefix =>
|
||||||
|
assemblyName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue