Build the first Godot CultMesh runtime slice

This commit is contained in:
Meta Vens 2026-08-26 21:58:20 +02:00
commit fd9f46a1f4
19 changed files with 696 additions and 0 deletions

8
.gitignore vendored
View file

@ -10,3 +10,11 @@ Thumbs.db
.obsidian/workspace.json
.obsidian/workspace-mobile.json
.trash/
# C# and Godot runtime state
/artifacts/
**/bin/
**/obj/
client/.godot/
*.user
*.suo

9
Delvehold.slnx Normal file
View file

@ -0,0 +1,9 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/Delvehold.Protocol/Delvehold.Protocol.csproj" />
<Project Path="src/Delvehold.WorldHost/Delvehold.WorldHost.csproj" />
</Folder>
<Folder Name="/client/">
<Project Path="client/Delvehold.Client.csproj" />
</Folder>
</Solution>

View file

@ -0,0 +1,36 @@
---
title: Implementation Map
description: Current runtime body, authority boundaries, and focused verification targets.
aliases:
- /documentation/implementation-map
---
# Implementation Map
This page describes the body that exists in source. The [[Game Design/index|Game Design Document]] describes the intended game and remains the authority for design.
## Runtime body
- `Delvehold.WorldHost` is the sole writer of canonical local world state. It owns the persistent CultCache, the explicit first-world seed, and admission of typed commands.
- `Delvehold.Client` is one Godot 4.7.2 C# application. DELVE and HOLD are views inside that application, not separate clients or authorities.
- `Delvehold.Protocol` contains the typed CultMesh operation payloads shared by the client and host. It owns no state and performs no simulation.
- CultLib revision `334e60f1928b4212a29dd8b0d19b2c099fe6365e` supplies CultCache, CultNet, and CultMesh. Builds reject another revision or a dirty CultLib worktree. The host targets the installed .NET 10 runtime; Godot's C# client targets its supported .NET 8 surface.
## First live path
The client discovers the host through its CultMesh rendezvous endpoint and sends `delvehold.world/enter`. The host returns and persists an idempotent typed world-entry command receipt. Entering the world does not advance simulation or grant the client write authority.
The seed state names one workshop, one commons, one dungeon core, one contract, and one material. These identifiers make the connection observable; they are not a claim that the first playable slice is complete.
## Forbidden writers
- The Godot scene tree, UI controls, and client session cannot write canonical world state.
- The protocol assembly cannot persist, simulate, or invent defaults on behalf of the host.
- Reconnect and replay cannot manufacture a second receipt for the same idempotency key.
- JSON is not a load-bearing state format. Canonical state and receipts are typed CultCache documents.
## Verification layer
`scripts/verify-runtime.ps1` builds the three projects, starts the host, connects the headless Godot client, rejects an idempotency collision and an invalid projection, restarts the host against the same CultCache, and replays the original enter intent. The smoke compares the complete accepted receipt summary, including its original effective time, across both accepted calls, requires the CultCache state file to remain present, and rejects known canonical-write APIs in client source.
Dungeon generation, CultMath randomness, CultGeometry isosurface extraction, gameplay mutation, and the Ghostlight boundary remain subsequent cuts.

View file

@ -6,3 +6,4 @@ description: Technical contracts, authority boundaries, and implementation maps.
The machinery beneath DELVE/HOLD: typed world state, ownership boundaries, client projections, and the contracts joining them.
- [[Authority Map|Authority map]]
- [[Implementation Map|Implementation map]]

13
Directory.Build.props Normal file
View file

@ -0,0 +1,13 @@
<Project>
<PropertyGroup>
<TargetFramework Condition="'$(TargetFramework)' == ''">net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
<CultLibRoot Condition="'$(CultLibRoot)' == '' and '$(CULTLIB_ROOT)' != ''">$(CULTLIB_ROOT)</CultLibRoot>
<CultLibRoot Condition="'$(CultLibRoot)' == ''">$(MSBuildThisFileDirectory)..\CultLib</CultLibRoot>
<CultLibRevision>334e60f1928b4212a29dd8b0d19b2c099fe6365e</CultLibRevision>
<GitExecutable Condition="'$(GitExecutable)' == '' and Exists('C:\Program Files\Git\cmd\git.exe')">C:\Program Files\Git\cmd\git.exe</GitExecutable>
<GitExecutable Condition="'$(GitExecutable)' == ''">git</GitExecutable>
</PropertyGroup>
</Project>

22
Directory.Build.targets Normal file
View file

@ -0,0 +1,22 @@
<Project>
<Target Name="VerifyCultLibRevision"
BeforeTargets="ResolveProjectReferences"
Condition="'$(SkipCultLibRevisionCheck)' != 'true'">
<Error Condition="!Exists('$(CultLibRoot)\src\GameCult.Mesh\GameCult.Mesh.csproj')"
Text="CultLibRoot '$(CultLibRoot)' does not contain the required GameCult.Mesh project." />
<Exec Command="&quot;$(GitExecutable)&quot; -C &quot;$(CultLibRoot)&quot; rev-parse HEAD"
ConsoleToMSBuild="true"
StandardOutputImportance="Low">
<Output TaskParameter="ConsoleOutput" PropertyName="ActualCultLibRevision" />
</Exec>
<Error Condition="'$([System.String]::Copy($(ActualCultLibRevision)).Trim())' != '$(CultLibRevision)'"
Text="CultLib must be revision $(CultLibRevision); found $([System.String]::Copy($(ActualCultLibRevision)).Trim()). Set CultLibRoot to the admitted clean worktree." />
<Exec Command="&quot;$(GitExecutable)&quot; -C &quot;$(CultLibRoot)&quot; status --porcelain --untracked-files=normal"
ConsoleToMSBuild="true"
StandardOutputImportance="Low">
<Output TaskParameter="ConsoleOutput" PropertyName="CultLibStatus" />
</Exec>
<Error Condition="'$([System.String]::Copy($(CultLibStatus)).Trim())' != ''"
Text="CultLibRoot must be clean before Delvehold consumes it." />
</Target>
</Project>

View file

@ -21,3 +21,20 @@ The launcher expects `GameCult-Quartz` beside this repository or at `GAMECULT_QU
```
Generated output is written to `quartz-site/public/`.
## Local runtime
The first runtime foundation expects the admitted CultLib worktree at
`F:\Projects\CultLib-aetheria-authority` and Godot 4.7.2 Mono at
`F:\Applications\Godot_v4.7.2-stable_mono_win64`. Build and exercise the
Godot-to-host CultMesh path, including a host restart, with:
```powershell
.\scripts\verify-runtime.ps1
```
Open the project in the Godot editor with:
```powershell
.\scripts\open-godot.ps1
```

View file

@ -0,0 +1,12 @@
<Project Sdk="Godot.NET.Sdk/4.7.2">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading>
<AssemblyName>Delvehold.Client</AssemblyName>
<RootNamespace>Delvehold.Client</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\src\Delvehold.Protocol\Delvehold.Protocol.csproj" />
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Mesh\GameCult.Mesh.csproj" />
</ItemGroup>
</Project>

160
client/Main.cs Normal file
View file

@ -0,0 +1,160 @@
using Delvehold.Protocol;
using GameCult.Mesh;
using Godot;
namespace Delvehold.Client;
public partial class Main : Control
{
private Label _mode = null!;
private Label _status = null!;
private Button _delve = null!;
private Button _hold = null!;
private CultMeshClient? _mesh;
private string _projection = "hold";
public override void _Ready()
{
BuildInterface();
_ = EnterWorldAsync();
}
public override void _ExitTree()
{
_mesh?.Dispose();
}
private void BuildInterface()
{
var margin = new MarginContainer();
margin.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
margin.AddThemeConstantOverride("margin_left", 48);
margin.AddThemeConstantOverride("margin_top", 42);
margin.AddThemeConstantOverride("margin_right", 48);
margin.AddThemeConstantOverride("margin_bottom", 42);
AddChild(margin);
var column = new VBoxContainer();
column.AddThemeConstantOverride("separation", 18);
margin.AddChild(column);
var title = new Label { Text = "DELVE / HOLD" };
title.AddThemeFontSizeOverride("font_size", 42);
column.AddChild(title);
_mode = new Label { Text = "HOLD projection" };
_mode.AddThemeFontSizeOverride("font_size", 24);
column.AddChild(_mode);
var modes = new HBoxContainer();
modes.AddThemeConstantOverride("separation", 12);
column.AddChild(modes);
_delve = new Button { Text = "DELVE", CustomMinimumSize = new Vector2(160, 48) };
_hold = new Button { Text = "HOLD", CustomMinimumSize = new Vector2(160, 48), Disabled = true };
_delve.Pressed += () => SelectProjection("delve");
_hold.Pressed += () => SelectProjection("hold");
modes.AddChild(_delve);
modes.AddChild(_hold);
_status = new Label { Text = "Finding the Greathold world host…", AutowrapMode = TextServer.AutowrapMode.WordSmart };
column.AddChild(_status);
}
private void SelectProjection(string projection)
{
_projection = projection;
_mode.Text = projection.ToUpperInvariant() + " projection";
_delve.Disabled = projection == "delve";
_hold.Disabled = projection == "hold";
}
private async Task EnterWorldAsync()
{
var options = ClientArguments.Parse(OS.GetCmdlineUserArgs());
SelectProjection(options.Projection);
try
{
_mesh = new CultMeshClient(new CultMeshClientOptions
{
RendezvousEndpoints = [options.Endpoint],
Discovery = new CultMeshVerseDiscoveryClientOptions { TransportVersion = "cultmesh.v0" },
Sessions = new CultMeshSessionManagerOptions
{
Trust = new CultMeshAuthorityTrustPolicy(CultMeshAuthorityTrustMode.LocalDevelopment)
},
OperationResponseTimeout = TimeSpan.FromSeconds(8)
});
var receipt = await _mesh.InvokeAsync<WorldEnterIntent, WorldEnterReceipt>(
new CultMeshSessionTarget(
DelveholdWorldContract.VerseId,
DelveholdWorldContract.AuthorityRuntimeId),
DelveholdWorldContract.ServiceId,
DelveholdWorldContract.EnterOperation,
DelveholdWorldContract.EnterIntentSchema,
DelveholdWorldContract.EnterReceiptSchema,
new WorldEnterIntent
{
Projection = _projection
},
options.ClientRuntimeId,
options.SmokeId);
var line = $"{receipt.Status} {receipt.Value.IdempotencyKey} world={receipt.Value.WorldId} revision={receipt.Value.WorldRevision} effective={receipt.Value.EffectiveAtUtc:O}";
var admittedAsExpected = receipt.Status == options.ExpectedStatus;
CallDeferred(MethodName.ShowResult, line, admittedAsExpected, options.HeadlessSmoke);
}
catch (Exception error)
{
CallDeferred(MethodName.ShowResult, error.Message, false, options.HeadlessSmoke);
}
}
private void ShowResult(string message, bool succeeded, bool quit)
{
_status.Text = succeeded ? message : "Connection failed: " + message;
GD.Print($"DELVEHOLD_SMOKE {(succeeded ? "PASS" : "FAIL")} {message}");
if (quit) GetTree().Quit(succeeded ? 0 : 1);
}
private sealed record ClientArguments(
string Endpoint,
string ClientRuntimeId,
string SmokeId,
string Projection,
string ExpectedStatus,
bool HeadlessSmoke)
{
public static ClientArguments Parse(string[] args)
{
var endpoint = "cultnet+tcp://127.0.0.1:4075";
var clientId = "delvehold.godot.local";
var smokeId = Guid.NewGuid().ToString("N");
var projection = "hold";
var expectedStatus = "accepted";
var headless = false;
for (var index = 0; index < args.Length; index++)
{
switch (args[index])
{
case "--endpoint" when index + 1 < args.Length:
endpoint = args[++index];
break;
case "--client-id" when index + 1 < args.Length:
clientId = args[++index];
break;
case "--smoke-id" when index + 1 < args.Length:
smokeId = args[++index];
headless = true;
break;
case "--projection" when index + 1 < args.Length:
projection = args[++index].ToLowerInvariant();
break;
case "--expect-status" when index + 1 < args.Length:
expectedStatus = args[++index].ToLowerInvariant();
break;
}
}
return new ClientArguments(endpoint, clientId, smokeId, projection, expectedStatus, headless);
}
}
}

1
client/Main.cs.uid Normal file
View file

@ -0,0 +1 @@
uid://dgm3vnnuux4iy

12
client/Main.tscn Normal file
View file

@ -0,0 +1,12 @@
[gd_scene load_steps=2 format=3]
[ext_resource path="res://Main.cs" type="Script" id="1"]
[node name="Delvehold" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1")

33
client/project.godot Normal file
View file

@ -0,0 +1,33 @@
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters
config_version=5
[application]
config/name="DELVE/HOLD"
run/main_scene="res://Main.tscn"
config/features=PackedStringArray("4.7", "C#")
[display]
window/size/viewport_width=1280
window/size/viewport_height=720
window/size/window_width_override=1280
window/size/window_height_override=720
window/stretch/mode="canvas_items"
[dotnet]
project/assembly_name="Delvehold.Client"
[rendering]
renderer/rendering_method="gl_compatibility"
renderer/rendering_method.mobile="gl_compatibility"
environment/defaults/default_clear_color=Color(0.035, 0.027, 0.024, 1)

13
scripts/open-godot.ps1 Normal file
View file

@ -0,0 +1,13 @@
[CmdletBinding()]
param(
[string]$GodotRoot = 'F:\Applications\Godot_v4.7.2-stable_mono_win64',
[string]$CultLibRoot = 'F:\Projects\CultLib-aetheria-authority'
)
$godot = Join-Path $GodotRoot 'Godot_v4.7.2-stable_mono_win64.exe'
if (-not (Test-Path -LiteralPath $godot)) {
throw "Godot 4.7.2 Mono was not found at '$godot'."
}
$env:CULTLIB_ROOT = $CultLibRoot
Start-Process -FilePath $godot -ArgumentList @('--editor', '--path', (Join-Path $PSScriptRoot '..\client'))

View file

@ -0,0 +1,90 @@
[CmdletBinding()]
param(
[string]$CultLibRoot = 'F:\Projects\CultLib-aetheria-authority',
[string]$GodotRoot = 'F:\Applications\Godot_v4.7.2-stable_mono_win64'
)
$ErrorActionPreference = 'Stop'
$repoRoot = Split-Path $PSScriptRoot -Parent
$godot = Join-Path $GodotRoot 'Godot_v4.7.2-stable_mono_win64_console.exe'
if (-not (Test-Path -LiteralPath $godot)) {
throw "Godot 4.7.2 Mono console was not found at '$godot'."
}
dotnet build (Join-Path $repoRoot 'Delvehold.slnx') -p:CultLibRoot=$CultLibRoot
if ($LASTEXITCODE -ne 0) { throw 'The focused .NET/Godot build failed.' }
$smokeRoot = Join-Path $repoRoot ('artifacts\runtime-smoke\' + [guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $smokeRoot -Force | Out-Null
$statePath = Join-Path $smokeRoot 'world.cc'
$port = Get-Random -Minimum 41000 -Maximum 49000
$endpoint = "cultnet+tcp://127.0.0.1:$port"
$smokeId = 'runtime-smoke-enter-v0'
$clientId = 'delvehold.godot.smoke'
$hostProject = Join-Path $repoRoot 'src\Delvehold.WorldHost\Delvehold.WorldHost.csproj'
$clientProject = Join-Path $repoRoot 'client'
function Start-WorldHost([string]$suffix) {
$stdout = Join-Path $smokeRoot "host-$suffix.out.log"
$stderr = Join-Path $smokeRoot "host-$suffix.err.log"
$process = Start-Process -FilePath 'dotnet' -PassThru -WindowStyle Hidden `
-RedirectStandardOutput $stdout -RedirectStandardError $stderr `
-ArgumentList @('run', '--no-build', '--project', $hostProject, '-p:CultLibRoot=' + $CultLibRoot, '--', '--state', $statePath, '--port', $port)
$deadline = [DateTime]::UtcNow.AddSeconds(20)
while ([DateTime]::UtcNow -lt $deadline) {
if ($process.HasExited) {
throw "World host exited before readiness. See '$stderr'."
}
if ((Test-Path -LiteralPath $stdout) -and (Select-String -Path $stdout -Pattern 'DELVEHOLD_HOST_READY' -Quiet)) {
return $process
}
Start-Sleep -Milliseconds 100
}
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
throw "World host did not become ready. See '$stdout' and '$stderr'."
}
function Invoke-GodotSmoke([string]$suffix, [string]$projection, [string]$expectedStatus, [string]$intentId = $smokeId) {
$log = Join-Path $smokeRoot "godot-$suffix.log"
& $godot --headless --path $clientProject --editor --quit-after 1 2>&1 | Set-Content -Path $log
if ($LASTEXITCODE -ne 0) { throw "Godot import/build failed. See '$log'." }
$runtimeOutput = & $godot --headless --path $clientProject -- --endpoint $endpoint --client-id $clientId --smoke-id $intentId --projection $projection --expect-status $expectedStatus 2>&1
$runtimeOutput | Tee-Object -FilePath $log -Append | Write-Host
if ($LASTEXITCODE -ne 0) { throw "Godot runtime smoke failed. See '$log'." }
if (-not (Select-String -Path $log -Pattern "DELVEHOLD_SMOKE PASS $expectedStatus $intentId" -Quiet)) {
throw "Godot did not report the expected '$expectedStatus' typed receipt. See '$log'."
}
return (Select-String -Path $log -Pattern "DELVEHOLD_SMOKE PASS $expectedStatus $intentId" | Select-Object -Last 1).Line
}
$forbiddenClientWriters = Select-String -Path (Join-Path $repoRoot 'client\*.cs') `
-Pattern 'CultMesh\.CreateNodeAsync|\.Database\.PutAsync|\.Cache\.UpsertAsync'
if ($forbiddenClientWriters) {
throw 'The Godot client contains a canonical CultCache/CultMesh write path.'
}
$hostProcess = $null
try {
$hostProcess = Start-WorldHost 'first'
$firstReceipt = Invoke-GodotSmoke 'first' 'hold' 'accepted'
$null = Invoke-GodotSmoke 'collision' 'delve' 'rejected'
$null = Invoke-GodotSmoke 'invalid-projection' 'banana' 'rejected' 'runtime-smoke-invalid-v0'
Stop-Process -Id $hostProcess.Id -Force
$hostProcess.WaitForExit()
$hostProcess = Start-WorldHost 'restart'
$restartReceipt = Invoke-GodotSmoke 'restart' 'hold' 'accepted'
}
finally {
if ($null -ne $hostProcess -and -not $hostProcess.HasExited) {
Stop-Process -Id $hostProcess.Id -Force -ErrorAction SilentlyContinue
}
}
if (-not (Test-Path -LiteralPath $statePath)) {
throw 'The host did not persist its CultCache state.'
}
if ($firstReceipt -ne $restartReceipt) {
throw "The restarted host did not return the original persisted receipt.`nFirst: $firstReceipt`nRestart: $restartReceipt"
}
Write-Host "DELVEHOLD_RUNTIME_VERIFIED state=$statePath receipt=$smokeId"

View file

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>Delvehold.Protocol</AssemblyName>
<RootNamespace>Delvehold.Protocol</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Caching\GameCult.Caching.csproj" />
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Caching.MessagePack\GameCult.Caching.MessagePack.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Caching.MessagePack.Generator\GameCult.Caching.MessagePack.Generator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,33 @@
using GameCult.Caching;
using MessagePack;
namespace Delvehold.Protocol;
public static class DelveholdWorldContract
{
public const string VerseId = "delvehold.shared-world";
public const string AuthorityRuntimeId = "delvehold.world-host";
public const string ServiceId = "delvehold.world";
public const string EnterOperation = "enter";
public const string EnterIntentSchema = "delvehold.world_enter_intent.v0";
public const string EnterReceiptSchema = "delvehold.world_enter_receipt.v0";
}
[MessagePackObject]
public sealed class WorldEnterIntent
{
[Key(0)] public string Projection { get; set; } = "hold";
}
[MessagePackObject]
[CultDocument("delvehold.world_enter_receipt", DelveholdWorldContract.EnterReceiptSchema)]
public sealed class WorldEnterReceipt
{
[Key(0), CultName] public string IdempotencyKey { get; set; } = "";
[Key(1)] public string ClientRuntimeId { get; set; } = "";
[Key(2)] public string Projection { get; set; } = "hold";
[Key(3)] public long WorldRevision { get; set; }
[Key(4)] public string WorldId { get; set; } = "";
[Key(5)] public string Message { get; set; } = "";
[Key(6)] public DateTime EffectiveAtUtc { get; set; }
}

View file

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<AssemblyName>Delvehold.WorldHost</AssemblyName>
<RootNamespace>Delvehold.WorldHost</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Delvehold.Protocol\Delvehold.Protocol.csproj" />
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Mesh\GameCult.Mesh.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Caching.MessagePack.Generator\GameCult.Caching.MessagePack.Generator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,170 @@
using System.Net;
using System.Net.Sockets;
using Delvehold.Protocol;
using Delvehold.WorldHost;
using GameCult.Caching;
using GameCult.Caching.MessagePack;
using GameCult.Mesh;
using GameCult.Networking;
const string routeGeneration = "delvehold-local-v1";
var settings = HostSettings.Parse(args);
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(settings.StatePath))!);
var documentTypes = new[] { typeof(DelveholdWorldState), typeof(WorldEnterReceipt) };
var cacheRegistry = CultMesh.CreateCultCacheDocumentRegistry(documentTypes);
var networkRegistry = CultMesh.CreateCultNetDocumentRegistry(documentTypes, cacheRegistry);
using var node = await CultMesh.CreateNodeAsync(settings.StatePath, new CultMeshNodeOptions
{
StartServer = false,
CacheOptions = new CultCacheOpenOptions
{
Registry = cacheRegistry,
FlushOnDispose = true,
StoreFlushOnDispose = true
},
DatabaseOptions = new CultNetDatabaseOptions
{
RuntimeId = DelveholdWorldContract.AuthorityRuntimeId,
DocumentRegistry = networkRegistry
}
});
var worldKey = new CultRecordKey(WorldStateContract.RecordKey);
if (node.Cache.Get<DelveholdWorldState>(worldKey) is null)
await node.Database.PutAsync(worldKey, DelveholdWorldState.FirstWorld());
await node.FlushAsync();
using var server = new TcpFramedCultNetSchemaServer(
new TcpListener(IPAddress.Loopback, settings.Port));
var endpoint = $"cultnet+tcp://127.0.0.1:{server.LocalEndPoint.Port}";
using var identity = new CultMeshSessionIdentityServer(
server,
DelveholdWorldContract.AuthorityRuntimeId,
[DelveholdWorldContract.VerseId],
[CultMeshProtocols.Documents.Value],
[routeGeneration]);
using var catalog = new CultMeshVerseCatalog();
catalog.Upsert(new CultMeshVerseDescriptor(
DelveholdWorldContract.VerseId,
"DELVE/HOLD shared world",
CultMeshVerseAuthorityModel.OperatorCluster,
new CultMeshVerseCompatibility("cultmesh.v0", "delvehold-local-rules-v0"),
authorityRoutes:
[
new CultMeshAuthorityRoute(
DelveholdWorldContract.AuthorityRuntimeId,
endpoint,
[CultMeshProtocols.Documents.Value],
priority: 10,
generation: routeGeneration)
],
description: "The authoritative DELVE/HOLD world host."));
server.OnCultNet<CultMeshVerseCatalogRequestMessage>((request, peer) =>
{
peer.SendCultNet(CultMeshVerseMessages.CreateCatalogResponse(catalog, request));
return Task.CompletedTask;
});
using var operationGate = new SemaphoreSlim(1, 1);
using var operations = new CultNetOperationServer(server, DelveholdWorldContract.AuthorityRuntimeId)
.Register<WorldEnterIntent, WorldEnterReceipt>(
DelveholdWorldContract.ServiceId,
DelveholdWorldContract.EnterOperation,
DelveholdWorldContract.EnterIntentSchema,
DelveholdWorldContract.EnterReceiptSchema,
async context =>
{
await operationGate.WaitAsync();
try
{
var sourceRuntimeId = context.SourceRuntimeId?.Trim() ?? "";
var projection = context.Value.Projection?.Trim().ToLowerInvariant() ?? "";
if (sourceRuntimeId.Length == 0 || (projection != "delve" && projection != "hold"))
{
var invalidWorld = node.Cache.Get<DelveholdWorldState>(worldKey)
?? throw new InvalidOperationException("Canonical world state is unavailable.");
return CultNetOperationReply<WorldEnterReceipt>.Rejected(
new WorldEnterReceipt
{
IdempotencyKey = context.IdempotencyKey,
ClientRuntimeId = sourceRuntimeId,
Projection = projection,
WorldRevision = invalidWorld.Revision,
WorldId = invalidWorld.WorldId,
Message = "World entry requires a caller identity and the DELVE or HOLD projection.",
EffectiveAtUtc = DateTime.UtcNow
},
"invalid-world-entry");
}
var receiptKey = new CultRecordKey("delvehold:receipt:" + context.IdempotencyKey);
var existing = node.Cache.Get<WorldEnterReceipt>(receiptKey);
if (existing is not null)
{
var matches = existing.ClientRuntimeId == sourceRuntimeId &&
existing.Projection == projection;
return matches
? CultNetOperationReply<WorldEnterReceipt>.Accepted(existing)
: CultNetOperationReply<WorldEnterReceipt>.Rejected(
existing,
"idempotency-key-collision");
}
var world = node.Cache.Get<DelveholdWorldState>(worldKey)
?? throw new InvalidOperationException("Canonical world state is unavailable.");
var receipt = new WorldEnterReceipt
{
IdempotencyKey = context.IdempotencyKey,
ClientRuntimeId = sourceRuntimeId,
Projection = projection,
WorldRevision = world.Revision,
WorldId = world.WorldId,
Message = $"Entered {world.WorldId} through the {projection.ToUpperInvariant()} projection.",
EffectiveAtUtc = DateTime.UtcNow
};
await node.Database.PutAsync(receiptKey, receipt);
await node.FlushAsync();
return CultNetOperationReply<WorldEnterReceipt>.Accepted(receipt);
}
finally
{
operationGate.Release();
}
});
server.PeerFailed += (peer, error) => Console.Error.WriteLine($"DELVEHOLD_PEER_FAILED {peer} {error.Message}");
Console.WriteLine($"DELVEHOLD_HOST_READY {endpoint}");
var stopped = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
Console.CancelKeyPress += (_, eventArgs) =>
{
eventArgs.Cancel = true;
stopped.TrySetResult();
};
await Task.WhenAny(stopped.Task, server.BackgroundFailure);
if (server.BackgroundFailure.IsCompleted)
throw await server.BackgroundFailure;
internal sealed record HostSettings(string StatePath, int Port)
{
public static HostSettings Parse(string[] args)
{
var state = Path.Combine("artifacts", "host", "delvehold.cc");
var port = 4075;
for (var index = 0; index < args.Length; index++)
{
switch (args[index])
{
case "--state" when index + 1 < args.Length:
state = args[++index];
break;
case "--port" when index + 1 < args.Length:
port = int.Parse(args[++index]);
break;
}
}
return new HostSettings(state, port);
}
}

View file

@ -0,0 +1,34 @@
using GameCult.Caching;
using MessagePack;
namespace Delvehold.WorldHost;
internal static class WorldStateContract
{
public const string Schema = "delvehold.world_state.v0";
public const string RecordKey = "delvehold:world:canonical";
}
[MessagePackObject]
[CultDocument("delvehold.world_state", WorldStateContract.Schema)]
public sealed class DelveholdWorldState
{
[Key(0), CultName] public string WorldId { get; set; } = "";
[Key(1)] public long Revision { get; set; }
[Key(2)] public string WorkshopId { get; set; } = "";
[Key(3)] public string CommonsId { get; set; } = "";
[Key(4)] public string DungeonCoreId { get; set; } = "";
[Key(5)] public string ContractId { get; set; } = "";
[Key(6)] public string MaterialId { get; set; } = "";
public static DelveholdWorldState FirstWorld() => new()
{
WorldId = "greathold-first-world",
Revision = 1,
WorkshopId = "workshop-emberbench",
CommonsId = "commons-deepmarket",
DungeonCoreId = "core-cinderwake",
ContractId = "contract-first-light",
MaterialId = "material-wardstone"
};
}