diff --git a/Aquarium.Engine.slnx b/Aquarium.Engine.slnx
index 2eb83ff..5335fd0 100644
--- a/Aquarium.Engine.slnx
+++ b/Aquarium.Engine.slnx
@@ -1,5 +1,7 @@
+
+
diff --git a/README.md b/README.md
index 651fc91..e8606e4 100644
--- a/README.md
+++ b/README.md
@@ -78,6 +78,17 @@ compiles changed shaders in the running process, then swaps the D3D shader
objects only after compilation succeeds. A bad shader edit leaves the previous
working shaders bound and writes the compiler failure to stderr.
+Live gameplay/runtime code is split into `Aquarium.Engine.Live` behind
+`Aquarium.Engine.Contracts`. The host loads that live DLL through a collectible
+assembly load context, while `dev-watch.ps1` builds live-only changes into
+`artifacts\dev-reload\live-slots` and updates `live-current.txt`. The running
+host sees the pointer change, loads and starts the new runtime first, then
+disposes the old one only after the replacement is valid. A bad live DLL leaves
+the previous runtime alive and reports the reload failure to stderr. Successful
+reloads rehydrate through CultCache without restarting the window or D3D device.
+Host, renderer, contract, project, and script changes still rebuild and restart
+the apphost.
+
Runtime live state is a typed CultCache document, not a loose sidecar file. The
first document is `epiphany.aquarium.live_state`, currently banking camera
target, yaw, pitch, distance, time, and save generation every few frames so a
diff --git a/scripts/dev-reload.ps1 b/scripts/dev-reload.ps1
index 8f13ed4..b5c6f38 100644
--- a/scripts/dev-reload.ps1
+++ b/scripts/dev-reload.ps1
@@ -11,10 +11,12 @@ Set-StrictMode -Version Latest
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
$projectPath = Join-Path $repoRoot "src\Aquarium.Engine\Aquarium.Engine.csproj"
+$liveProjectPath = Join-Path $repoRoot "src\Aquarium.Engine.Live\Aquarium.Engine.Live.csproj"
$devRoot = Join-Path $repoRoot "artifacts\dev-reload"
$slotRoot = Join-Path $devRoot "slots"
$statePath = Join-Path $devRoot "state.clixml"
$buildStatePath = Join-Path $devRoot "last-build.clixml"
+$liveReloadPointerPath = Join-Path $devRoot "live-current.txt"
$cultCachePath = Join-Path $devRoot "cultcache\aquarium-client.msgpack"
$shaderSourcePath = Join-Path $repoRoot "src\Aquarium.Engine\Render\Shaders\Aquarium.hlsl"
$stdoutLogPath = Join-Path $devRoot "latest.out.log"
@@ -138,6 +140,13 @@ if ($LASTEXITCODE -ne 0) {
throw "dotnet build failed with exit code $LASTEXITCODE."
}
+dotnet build $liveProjectPath `
+ -c Debug `
+ -o $slotPath
+if ($LASTEXITCODE -ne 0) {
+ throw "live runtime build failed with exit code $LASTEXITCODE."
+}
+
if ($BuildOnly) {
@{
slot = $slotPath
@@ -153,11 +162,23 @@ if ($BuildOnly) {
Stop-PreviousOwnedProcess
$exePath = Join-Path $slotPath "Aquarium.Engine.exe"
+$liveAssemblyPath = Join-Path $slotPath "Aquarium.Engine.Live.dll"
if (-not (Test-Path $exePath)) {
throw "Expected apphost was not produced: $exePath"
}
-$arguments = @("--cache", $cultCachePath, "--shader-source", $shaderSourcePath)
+if (-not (Test-Path $liveAssemblyPath)) {
+ throw "Expected live runtime assembly was not produced: $liveAssemblyPath"
+}
+
+Set-Content -Path $liveReloadPointerPath -Value $liveAssemblyPath -Encoding UTF8
+
+$arguments = @(
+ "--cache", $cultCachePath,
+ "--shader-source", $shaderSourcePath,
+ "--live-assembly", $liveAssemblyPath,
+ "--live-reload-pointer", $liveReloadPointerPath
+)
if ($Headless) {
$arguments += "--headless"
}
diff --git a/scripts/dev-watch.ps1 b/scripts/dev-watch.ps1
index 6f53e31..ddbfcb6 100644
--- a/scripts/dev-watch.ps1
+++ b/scripts/dev-watch.ps1
@@ -12,9 +12,13 @@ Set-StrictMode -Version Latest
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
$reloadScript = Join-Path $PSScriptRoot "dev-reload.ps1"
+$liveProjectPath = Join-Path $repoRoot "src\Aquarium.Engine.Live\Aquarium.Engine.Live.csproj"
$watchLogPath = Join-Path $repoRoot "artifacts\dev-reload\watch.log"
+$liveSlotRoot = Join-Path $repoRoot "artifacts\dev-reload\live-slots"
+$liveReloadPointerPath = Join-Path $repoRoot "artifacts\dev-reload\live-current.txt"
New-Item -ItemType Directory -Force -Path (Split-Path $watchLogPath) | Out-Null
+New-Item -ItemType Directory -Force -Path $liveSlotRoot | Out-Null
function Write-WatchLog([string]$message) {
$line = "$(Get-Date -Format o) $message"
@@ -22,7 +26,7 @@ function Write-WatchLog([string]$message) {
Add-Content -Path $watchLogPath -Value $line
}
-function Get-WatchedFiles {
+function Get-SourceFiles {
$roots = @(
(Join-Path $repoRoot "src"),
(Join-Path $repoRoot "Aquarium.Engine.slnx"),
@@ -49,8 +53,20 @@ function Get-WatchedFiles {
}
}
-function Get-SourceFingerprint {
- $fingerprintInput = Get-WatchedFiles |
+function Get-LiveFiles {
+ Get-SourceFiles | Where-Object {
+ $_.FullName -like (Join-Path $repoRoot "src\Aquarium.Engine.Live\*")
+ }
+}
+
+function Get-RestartFiles {
+ Get-SourceFiles | Where-Object {
+ $_.FullName -notlike (Join-Path $repoRoot "src\Aquarium.Engine.Live\*")
+ }
+}
+
+function Get-Fingerprint($files) {
+ $fingerprintInput = $files |
Sort-Object FullName |
ForEach-Object {
"$($_.FullName)|$($_.Length)|$($_.LastWriteTimeUtc.Ticks)"
@@ -68,15 +84,39 @@ function Get-SourceFingerprint {
}
}
-function Wait-StableFingerprint {
- $first = Get-SourceFingerprint
- Start-Sleep -Milliseconds $DebounceMilliseconds
- $second = Get-SourceFingerprint
+function Get-SourceFingerprint {
+ Get-Fingerprint (Get-SourceFiles)
+}
- while ($first -ne $second) {
+function Get-LiveFingerprint {
+ Get-Fingerprint (Get-LiveFiles)
+}
+
+function Get-RestartFingerprint {
+ Get-Fingerprint (Get-RestartFiles)
+}
+
+function Wait-StableFingerprints {
+ $first = @{
+ all = Get-SourceFingerprint
+ live = Get-LiveFingerprint
+ restart = Get-RestartFingerprint
+ }
+ Start-Sleep -Milliseconds $DebounceMilliseconds
+ $second = @{
+ all = Get-SourceFingerprint
+ live = Get-LiveFingerprint
+ restart = Get-RestartFingerprint
+ }
+
+ while ($first.all -ne $second.all) {
$first = $second
Start-Sleep -Milliseconds $DebounceMilliseconds
- $second = Get-SourceFingerprint
+ $second = @{
+ all = Get-SourceFingerprint
+ live = Get-LiveFingerprint
+ restart = Get-RestartFingerprint
+ }
}
return $second
@@ -111,17 +151,39 @@ function Invoke-Reload {
}
}
-$lastGoodFingerprint = Wait-StableFingerprint
+function Invoke-LiveReload {
+ param([string]$reason)
+
+ $timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
+ $liveSlot = Join-Path $liveSlotRoot "$timestamp-$([guid]::NewGuid().ToString("N").Substring(0, 8))"
+ New-Item -ItemType Directory -Force -Path $liveSlot | Out-Null
+
+ Write-WatchLog "Live reload requested: $reason"
+ dotnet build $liveProjectPath -c Debug -o $liveSlot
+ if ($LASTEXITCODE -ne 0) {
+ throw "live runtime build failed with exit code $LASTEXITCODE."
+ }
+
+ $liveAssemblyPath = Join-Path $liveSlot "Aquarium.Engine.Live.dll"
+ if (-not (Test-Path $liveAssemblyPath)) {
+ throw "Expected live runtime assembly was not produced: $liveAssemblyPath"
+ }
+
+ Set-Content -Path $liveReloadPointerPath -Value $liveAssemblyPath -Encoding UTF8
+ Write-WatchLog "Live reload pointer updated: $liveAssemblyPath"
+}
+
+$lastGoodFingerprint = Wait-StableFingerprints
$lastFailedFingerprint = $null
if (-not $NoInitialLaunch) {
try {
Invoke-Reload "initial launch"
- $lastGoodFingerprint = Wait-StableFingerprint
+ $lastGoodFingerprint = Wait-StableFingerprints
$lastFailedFingerprint = $null
}
catch {
- $lastFailedFingerprint = $lastGoodFingerprint
+ $lastFailedFingerprint = $lastGoodFingerprint.all
Write-WatchLog "Initial launch failed; keeping any previous good process alive. $($_.Exception.Message)"
}
}
@@ -130,9 +192,9 @@ Write-WatchLog "Watching Aquarium sources. Press Ctrl+C to stop."
do {
Start-Sleep -Milliseconds $IntervalMilliseconds
- $currentFingerprint = Wait-StableFingerprint
+ $currentFingerprint = Wait-StableFingerprints
- if ($currentFingerprint -eq $lastGoodFingerprint -or $currentFingerprint -eq $lastFailedFingerprint) {
+ if ($currentFingerprint.all -eq $lastGoodFingerprint.all -or $currentFingerprint.all -eq $lastFailedFingerprint) {
if ($Once) {
break
}
@@ -141,12 +203,18 @@ do {
}
try {
- Invoke-Reload "source change"
+ if ($currentFingerprint.restart -eq $lastGoodFingerprint.restart -and $currentFingerprint.live -ne $lastGoodFingerprint.live) {
+ Invoke-LiveReload "live source change"
+ }
+ else {
+ Invoke-Reload "host source change"
+ }
+
$lastGoodFingerprint = $currentFingerprint
$lastFailedFingerprint = $null
}
catch {
- $lastFailedFingerprint = $currentFingerprint
+ $lastFailedFingerprint = $currentFingerprint.all
Write-WatchLog "Reload failed; previous good process stays alive. $($_.Exception.Message)"
}
diff --git a/src/Aquarium.Engine.Contracts/Aquarium.Engine.Contracts.csproj b/src/Aquarium.Engine.Contracts/Aquarium.Engine.Contracts.csproj
new file mode 100644
index 0000000..9ed914b
--- /dev/null
+++ b/src/Aquarium.Engine.Contracts/Aquarium.Engine.Contracts.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
diff --git a/src/Aquarium.Engine.Contracts/AquariumFrame.cs b/src/Aquarium.Engine.Contracts/AquariumFrame.cs
new file mode 100644
index 0000000..159cbff
--- /dev/null
+++ b/src/Aquarium.Engine.Contracts/AquariumFrame.cs
@@ -0,0 +1,6 @@
+using System.Numerics;
+
+namespace Aquarium.Engine;
+
+public readonly record struct AquariumFrame(GridFrame Grid, Vector3 CameraPosition, float TimeSeconds);
+
diff --git a/src/Aquarium.Engine.Contracts/AquariumRuntimeContracts.cs b/src/Aquarium.Engine.Contracts/AquariumRuntimeContracts.cs
new file mode 100644
index 0000000..80fe2b0
--- /dev/null
+++ b/src/Aquarium.Engine.Contracts/AquariumRuntimeContracts.cs
@@ -0,0 +1,22 @@
+using Aquarium.Engine.Input;
+
+namespace Aquarium.Engine;
+
+public interface IAquariumRuntime : IDisposable
+{
+ AquariumRuntimeOptions Options { get; }
+
+ AquariumFrame Frame { get; }
+
+ void Start();
+
+ void Update(float deltaSeconds, InputState input);
+}
+
+public interface IAquariumRuntimeFactory
+{
+ IAquariumRuntime Create(AquariumRuntimeOptions options);
+}
+
+public readonly record struct AquariumRuntimeOptions(bool Headless, string? CultCachePath);
+
diff --git a/src/Aquarium.Engine/Input/InputState.cs b/src/Aquarium.Engine.Contracts/Input/InputState.cs
similarity index 100%
rename from src/Aquarium.Engine/Input/InputState.cs
rename to src/Aquarium.Engine.Contracts/Input/InputState.cs
diff --git a/src/Aquarium.Engine/World/GridFrame.cs b/src/Aquarium.Engine.Contracts/World/GridFrame.cs
similarity index 52%
rename from src/Aquarium.Engine/World/GridFrame.cs
rename to src/Aquarium.Engine.Contracts/World/GridFrame.cs
index c2d6bcf..87da8f9 100644
--- a/src/Aquarium.Engine/World/GridFrame.cs
+++ b/src/Aquarium.Engine.Contracts/World/GridFrame.cs
@@ -4,8 +4,8 @@ namespace Aquarium.Engine;
public readonly record struct GridFrame(Vector2 Center, float Radius)
{
- public static GridFrame FromCamera(OrbitCameraRig camera)
+ public static GridFrame FromCamera(Vector2 target, float distance)
{
- return new GridFrame(camera.Target, camera.Distance);
+ return new GridFrame(target, distance);
}
}
diff --git a/src/Aquarium.Engine.Live/Aquarium.Engine.Live.csproj b/src/Aquarium.Engine.Live/Aquarium.Engine.Live.csproj
new file mode 100644
index 0000000..068b095
--- /dev/null
+++ b/src/Aquarium.Engine.Live/Aquarium.Engine.Live.csproj
@@ -0,0 +1,16 @@
+
+
+
+ net10.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
diff --git a/src/Aquarium.Engine/AquariumRuntime.cs b/src/Aquarium.Engine.Live/AquariumRuntime.cs
similarity index 85%
rename from src/Aquarium.Engine/AquariumRuntime.cs
rename to src/Aquarium.Engine.Live/AquariumRuntime.cs
index 1ba55ee..046553d 100644
--- a/src/Aquarium.Engine/AquariumRuntime.cs
+++ b/src/Aquarium.Engine.Live/AquariumRuntime.cs
@@ -4,7 +4,7 @@ using Aquarium.Engine.State;
namespace Aquarium.Engine;
-public sealed class AquariumRuntime : IDisposable
+public sealed class AquariumRuntime : IAquariumRuntime
{
private const float StateSaveIntervalSeconds = 0.20f;
@@ -26,7 +26,7 @@ public sealed class AquariumRuntime : IDisposable
pitchRadians: liveState.CameraPitchRadians,
distance: liveState.CameraDistance);
timeSeconds = liveState.TimeSeconds;
- gridFrame = GridFrame.FromCamera(cameraRig);
+ gridFrame = GridFrame.FromCamera(cameraRig.Target, cameraRig.Distance);
}
public AquariumRuntimeOptions Options { get; }
@@ -46,7 +46,7 @@ public sealed class AquariumRuntime : IDisposable
{
timeSeconds += Math.Max(deltaSeconds, 0.0f);
cameraRig.ApplyInput(input, deltaSeconds);
- gridFrame = GridFrame.FromCamera(cameraRig);
+ gridFrame = GridFrame.FromCamera(cameraRig.Target, cameraRig.Distance);
secondsSinceStateSave += Math.Max(deltaSeconds, 0.0f);
if (secondsSinceStateSave >= StateSaveIntervalSeconds)
{
@@ -75,6 +75,10 @@ public sealed class AquariumRuntime : IDisposable
}
}
-public readonly record struct AquariumRuntimeOptions(bool Headless, string? CultCachePath);
-
-public readonly record struct AquariumFrame(GridFrame Grid, System.Numerics.Vector3 CameraPosition, float TimeSeconds);
+public sealed class AquariumRuntimeFactory : IAquariumRuntimeFactory
+{
+ public IAquariumRuntime Create(AquariumRuntimeOptions options)
+ {
+ return new AquariumRuntime(options);
+ }
+}
diff --git a/src/Aquarium.Engine/State/AquariumCultStateStore.cs b/src/Aquarium.Engine.Live/State/AquariumCultStateStore.cs
similarity index 100%
rename from src/Aquarium.Engine/State/AquariumCultStateStore.cs
rename to src/Aquarium.Engine.Live/State/AquariumCultStateStore.cs
diff --git a/src/Aquarium.Engine/State/AquariumLiveState.cs b/src/Aquarium.Engine.Live/State/AquariumLiveState.cs
similarity index 100%
rename from src/Aquarium.Engine/State/AquariumLiveState.cs
rename to src/Aquarium.Engine.Live/State/AquariumLiveState.cs
diff --git a/src/Aquarium.Engine/World/Angle.cs b/src/Aquarium.Engine.Live/World/Angle.cs
similarity index 100%
rename from src/Aquarium.Engine/World/Angle.cs
rename to src/Aquarium.Engine.Live/World/Angle.cs
diff --git a/src/Aquarium.Engine/World/OrbitCameraRig.cs b/src/Aquarium.Engine.Live/World/OrbitCameraRig.cs
similarity index 100%
rename from src/Aquarium.Engine/World/OrbitCameraRig.cs
rename to src/Aquarium.Engine.Live/World/OrbitCameraRig.cs
diff --git a/src/Aquarium.Engine/Aquarium.Engine.csproj b/src/Aquarium.Engine/Aquarium.Engine.csproj
index b91dfac..e9b5dda 100644
--- a/src/Aquarium.Engine/Aquarium.Engine.csproj
+++ b/src/Aquarium.Engine/Aquarium.Engine.csproj
@@ -9,6 +9,7 @@
+
@@ -17,8 +18,6 @@
-
-
diff --git a/src/Aquarium.Engine/AquariumHost.cs b/src/Aquarium.Engine/AquariumHost.cs
index 9045125..8eaa99d 100644
--- a/src/Aquarium.Engine/AquariumHost.cs
+++ b/src/Aquarium.Engine/AquariumHost.cs
@@ -1,5 +1,6 @@
using System.Diagnostics;
using Aquarium.Engine.Input;
+using Aquarium.Engine.Live;
using Aquarium.Engine.Platform;
using Aquarium.Engine.Render;
@@ -9,7 +10,9 @@ public static class AquariumHost
{
public static int Run(string[] args)
{
- using var runtime = new AquariumRuntime(new AquariumRuntimeOptions(ParseHeadless(args), ParseCachePath(args)));
+ var runtimeOptions = new AquariumRuntimeOptions(ParseHeadless(args), ParseCachePath(args));
+ using var runtimeLoader = new LiveRuntimeLoader(runtimeOptions, ParseLiveAssemblyPath(args), ParseLiveReloadPointerPath(args));
+ var runtime = runtimeLoader.Load();
var input = new InputState();
var width = runtime.Options.Headless ? 640 : 1280;
var height = runtime.Options.Headless ? 360 : 720;
@@ -18,8 +21,6 @@ public static class AquariumHost
window.PaintSplash();
using var renderer = new D3D11Renderer(window.Handle, window.ClientWidth, window.ClientHeight, ParseShaderPath(args));
- runtime.Start();
-
var frameClock = Stopwatch.StartNew();
var lastFrame = frameClock.Elapsed;
var frames = 0;
@@ -36,8 +37,8 @@ public static class AquariumHost
var deltaSeconds = (float)(now - lastFrame).TotalSeconds;
lastFrame = now;
- runtime.Update(deltaSeconds, input);
- renderer.Render(runtime.Frame);
+ runtimeLoader.Update(deltaSeconds, input);
+ renderer.Render(runtimeLoader.Runtime.Frame);
if (runtime.Options.Headless && ++frames >= 2)
{
@@ -80,4 +81,32 @@ public static class AquariumHost
return Environment.GetEnvironmentVariable("AQUARIUM_SHADER_SOURCE");
}
+
+ private static string? ParseLiveAssemblyPath(IReadOnlyCollection args)
+ {
+ var values = args.ToArray();
+ for (var index = 0; index < values.Length - 1; index++)
+ {
+ if (string.Equals(values[index], "--live-assembly", StringComparison.OrdinalIgnoreCase))
+ {
+ return values[index + 1];
+ }
+ }
+
+ return Environment.GetEnvironmentVariable("AQUARIUM_LIVE_ASSEMBLY");
+ }
+
+ private static string? ParseLiveReloadPointerPath(IReadOnlyCollection args)
+ {
+ var values = args.ToArray();
+ for (var index = 0; index < values.Length - 1; index++)
+ {
+ if (string.Equals(values[index], "--live-reload-pointer", StringComparison.OrdinalIgnoreCase))
+ {
+ return values[index + 1];
+ }
+ }
+
+ return Environment.GetEnvironmentVariable("AQUARIUM_LIVE_RELOAD_POINTER");
+ }
}
diff --git a/src/Aquarium.Engine/Live/LiveRuntimeLoader.cs b/src/Aquarium.Engine/Live/LiveRuntimeLoader.cs
new file mode 100644
index 0000000..88121b1
--- /dev/null
+++ b/src/Aquarium.Engine/Live/LiveRuntimeLoader.cs
@@ -0,0 +1,157 @@
+using System.Reflection;
+using System.Runtime.Loader;
+using System.Diagnostics;
+
+namespace Aquarium.Engine.Live;
+
+public sealed class LiveRuntimeLoader : IDisposable
+{
+ private const string LiveAssemblyName = "Aquarium.Engine.Live.dll";
+ private const string FactoryTypeName = "Aquarium.Engine.AquariumRuntimeFactory";
+
+ private readonly string liveAssemblyPath;
+ private readonly string? liveReloadPointerPath;
+ private readonly Stopwatch reloadClock = Stopwatch.StartNew();
+ private readonly AquariumRuntimeOptions options;
+ private ReloadedRuntime? current;
+ private TimeSpan lastReloadCheck;
+ private string? currentAssemblyPath;
+
+ public LiveRuntimeLoader(AquariumRuntimeOptions options, string? liveAssemblyPath = null, string? liveReloadPointerPath = null)
+ {
+ this.liveAssemblyPath = liveAssemblyPath ?? Path.Combine(AppContext.BaseDirectory, LiveAssemblyName);
+ this.liveReloadPointerPath = liveReloadPointerPath;
+ this.options = options;
+ }
+
+ public IAquariumRuntime Runtime
+ {
+ get
+ {
+ if (current == null)
+ {
+ Reload(liveAssemblyPath, options);
+ }
+
+ return current!.Runtime;
+ }
+ }
+
+ public IAquariumRuntime Load()
+ {
+ Reload(liveAssemblyPath, options);
+ return Runtime;
+ }
+
+ public void Reload(string assemblyPath, AquariumRuntimeOptions options)
+ {
+ var previous = current;
+ var loadContext = new LiveAssemblyLoadContext(assemblyPath);
+ IAquariumRuntime? nextRuntime = null;
+
+ try
+ {
+ var assembly = loadContext.LoadFromAssemblyPath(assemblyPath);
+ var factoryType = assembly.GetType(FactoryTypeName, throwOnError: true)
+ ?? throw new InvalidOperationException($"Live runtime factory was not found: {FactoryTypeName}");
+ var factory = (IAquariumRuntimeFactory?)Activator.CreateInstance(factoryType)
+ ?? throw new InvalidOperationException($"Live runtime factory could not be created: {FactoryTypeName}");
+
+ nextRuntime = factory.Create(options);
+ nextRuntime.Start();
+ }
+ catch
+ {
+ nextRuntime?.Dispose();
+ loadContext.Unload();
+ throw;
+ }
+
+ current = new ReloadedRuntime(loadContext, nextRuntime);
+ currentAssemblyPath = assemblyPath;
+ previous?.Runtime.Dispose();
+ previous?.Unload();
+ }
+
+ public void Update(float deltaSeconds, Input.InputState input)
+ {
+ TryReloadFromPointer();
+ Runtime.Update(deltaSeconds, input);
+ }
+
+ private void TryReloadFromPointer()
+ {
+ if (string.IsNullOrWhiteSpace(liveReloadPointerPath) || reloadClock.Elapsed - lastReloadCheck < TimeSpan.FromMilliseconds(500))
+ {
+ return;
+ }
+
+ lastReloadCheck = reloadClock.Elapsed;
+ string candidatePath;
+ try
+ {
+ candidatePath = File.ReadAllText(liveReloadPointerPath).Trim();
+ }
+ catch
+ {
+ return;
+ }
+
+ if (string.IsNullOrWhiteSpace(candidatePath)
+ || string.Equals(candidatePath, currentAssemblyPath, StringComparison.OrdinalIgnoreCase)
+ || !File.Exists(candidatePath))
+ {
+ return;
+ }
+
+ try
+ {
+ Reload(candidatePath, options);
+ Console.WriteLine($"Live runtime reload applied: {candidatePath}");
+ }
+ catch (Exception error)
+ {
+ Console.Error.WriteLine($"Live runtime reload failed; keeping previous runtime. {error.Message}");
+ }
+ }
+
+ public void Dispose()
+ {
+ var runtime = current;
+ current = null;
+ runtime?.Runtime.Dispose();
+ runtime?.Unload();
+ }
+
+ private sealed class ReloadedRuntime(LiveAssemblyLoadContext loadContext, IAquariumRuntime runtime)
+ {
+ public IAquariumRuntime Runtime { get; } = runtime;
+
+ public void Unload()
+ {
+ loadContext.Unload();
+ }
+ }
+
+ private sealed class LiveAssemblyLoadContext(string assemblyPath) : AssemblyLoadContext(isCollectible: true)
+ {
+ private readonly AssemblyDependencyResolver resolver = new(assemblyPath);
+
+ protected override Assembly? Load(AssemblyName assemblyName)
+ {
+ if (assemblyName.Name == typeof(IAquariumRuntime).Assembly.GetName().Name)
+ {
+ return null;
+ }
+
+ var path = resolver.ResolveAssemblyToPath(assemblyName);
+ return path == null ? null : LoadFromAssemblyPath(path);
+ }
+
+ protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
+ {
+ var path = resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
+ return path == null ? IntPtr.Zero : LoadUnmanagedDllFromPath(path);
+ }
+ }
+}