Discover Muninn Move evidence through Odin
This commit is contained in:
parent
de22059725
commit
538b95e2c3
5 changed files with 508 additions and 9 deletions
|
|
@ -134,6 +134,12 @@ if (args.Any(arg => string.Equals(arg, "--muninn-move-cultmesh-stream-smoke", St
|
|||
ParseStringOption(args, "--native-reservoir", DefaultNativeReservoirPath()));
|
||||
}
|
||||
|
||||
if (args.Any(arg => string.Equals(arg, "--muninn-move-odin-live-smoke", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return RunMuninnMoveOdinLiveSmoke(
|
||||
ParseStringOption(args, "--odin-cultmesh-uri", "cultmesh://127.0.0.1:17871/rendezvous/provider-catalog"));
|
||||
}
|
||||
|
||||
if (args.Any(arg => string.Equals(arg, "--move-fusion-smoke", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return RunMoveFusionSmoke();
|
||||
|
|
@ -192,7 +198,16 @@ if (args.Any(arg => string.Equals(arg, "--move-proof-runtime-snapshot-smoke", St
|
|||
ParseStringOption(args, "--native-reservoir", DefaultNativeReservoirPath()),
|
||||
activateViaConfiguredSource: true,
|
||||
requireActivationSurface: true,
|
||||
activateViaSnapshotProvider: true);
|
||||
activateViaSnapshotProvider: true,
|
||||
snapshotOutputPath: ParseStringOption(args, "--snapshot-output", ""));
|
||||
}
|
||||
|
||||
if (args.Any(arg => string.Equals(arg, "--move-proof-runtime-snapshot-file-smoke", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return RunMoveProofRuntimeSnapshotFileSmoke(
|
||||
ParseStringOption(args, "--native-reservoir", DefaultNativeReservoirPath()),
|
||||
ParseStringOption(args, "--snapshot", ""),
|
||||
args.Any(arg => string.Equals(arg, "--require-full-pose", StringComparison.OrdinalIgnoreCase)));
|
||||
}
|
||||
|
||||
if (args.Any(arg => string.Equals(arg, "--move-proof-presented-frame-smoke", StringComparison.OrdinalIgnoreCase)))
|
||||
|
|
@ -1909,6 +1924,58 @@ static async Task<int> RunMuninnMoveLiveRosterSmokeAsync(IReadOnlyList<string> i
|
|||
: 1;
|
||||
}
|
||||
|
||||
static int RunMuninnMoveOdinLiveSmoke(string odinCultMeshUri)
|
||||
{
|
||||
const string providerId = "muninn.telemetry.nightwing";
|
||||
const string streamId = "muninn:nightwing:move-evidence";
|
||||
var discovered = MimirOdinMoveProofEvidenceRingProvider.Discover(odinCultMeshUri, providerId, streamId);
|
||||
var configuration = new MimirMoveProofRuntimeConfiguration
|
||||
{
|
||||
OdinCultMeshUri = odinCultMeshUri,
|
||||
MuninnProviderId = providerId,
|
||||
EvidenceStreamId = streamId
|
||||
};
|
||||
if (!MimirOdinMoveProofEvidenceRingProvider.Instance.TryOpenEvidenceRing(
|
||||
configuration,
|
||||
out var lease,
|
||||
out var diagnostic) || lease is null)
|
||||
{
|
||||
throw new InvalidOperationException(diagnostic);
|
||||
}
|
||||
|
||||
using (lease)
|
||||
{
|
||||
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(8);
|
||||
while (DateTime.UtcNow < deadline)
|
||||
{
|
||||
if (lease.Ring.TryAcquireLatestRead(out var frameLease))
|
||||
{
|
||||
using (frameLease)
|
||||
{
|
||||
var frame = MimirMuninnMoveEvidenceAdapter.DeserializeStreamFrame(
|
||||
frameLease.Memory[..frameLease.Handle.ByteLength]);
|
||||
var ok = frame.FrameId.StartsWith($"{streamId}:", StringComparison.Ordinal) &&
|
||||
frame.ControllerStates.Length >= 1 &&
|
||||
discovered.Channel == "move-evidence";
|
||||
Console.WriteLine(JsonSerializer.Serialize(new
|
||||
{
|
||||
ok,
|
||||
diagnostic,
|
||||
discovered,
|
||||
frame.FrameId,
|
||||
frame.ProducerPeerId,
|
||||
controllers = frame.ControllerStates.Select(state => state.MoveId).Distinct().Order().ToArray(),
|
||||
markers = frame.MarkerCandidates.Length
|
||||
}));
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
}
|
||||
Thread.Sleep(20);
|
||||
}
|
||||
}
|
||||
throw new TimeoutException($"No live frame arrived from Odin-discovered stream {streamId}.");
|
||||
}
|
||||
|
||||
static int RunMuninnMoveCultMeshStreamSmoke(string nativeReservoirPath)
|
||||
{
|
||||
if (!File.Exists(nativeReservoirPath))
|
||||
|
|
@ -2308,7 +2375,8 @@ static int RunMoveProofRuntimeDriverSmoke(
|
|||
string nativeReservoirPath,
|
||||
bool activateViaConfiguredSource,
|
||||
bool requireActivationSurface = false,
|
||||
bool activateViaSnapshotProvider = false)
|
||||
bool activateViaSnapshotProvider = false,
|
||||
string snapshotOutputPath = "")
|
||||
{
|
||||
if (!File.Exists(nativeReservoirPath))
|
||||
{
|
||||
|
|
@ -2320,8 +2388,11 @@ static int RunMoveProofRuntimeDriverSmoke(
|
|||
const ulong rightWitnessHash = 0x0000000000000B0B;
|
||||
const string observedAt = "2026-06-11T18:30:00.0040000Z";
|
||||
var streamId = "muninn:nightwing:move-evidence";
|
||||
var deleteSnapshotOnExit = activateViaSnapshotProvider && string.IsNullOrWhiteSpace(snapshotOutputPath);
|
||||
var snapshotPath = activateViaSnapshotProvider
|
||||
? Path.Combine(Path.GetTempPath(), $"mimir-move-proof-evidence-{Guid.NewGuid():N}.mpack")
|
||||
? deleteSnapshotOnExit
|
||||
? Path.Combine(Path.GetTempPath(), $"mimir-move-proof-evidence-{Guid.NewGuid():N}.mpack")
|
||||
: Path.GetFullPath(snapshotOutputPath)
|
||||
: "";
|
||||
var frame = new MimirMuninnMoveEvidenceStreamFrame(
|
||||
FrameId: "muninn:nightwing:move-evidence:79",
|
||||
|
|
@ -2555,7 +2626,7 @@ static int RunMoveProofRuntimeDriverSmoke(
|
|||
duplicateSuppressed
|
||||
? 0
|
||||
: 1;
|
||||
if (!string.IsNullOrWhiteSpace(snapshotPath) && File.Exists(snapshotPath))
|
||||
if (deleteSnapshotOnExit && File.Exists(snapshotPath))
|
||||
{
|
||||
File.Delete(snapshotPath);
|
||||
}
|
||||
|
|
@ -2563,6 +2634,195 @@ static int RunMoveProofRuntimeDriverSmoke(
|
|||
return result;
|
||||
}
|
||||
|
||||
static int RunMoveProofRuntimeSnapshotFileSmoke(
|
||||
string nativeReservoirPath,
|
||||
string snapshotPath,
|
||||
bool requireFullPose)
|
||||
{
|
||||
if (!File.Exists(nativeReservoirPath))
|
||||
{
|
||||
Console.Error.WriteLine($"move-proof-runtime-snapshot-file-smoke missing-native path={nativeReservoirPath}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(snapshotPath) || !File.Exists(snapshotPath))
|
||||
{
|
||||
Console.Error.WriteLine($"move-proof-runtime-snapshot-file-smoke missing-snapshot path={snapshotPath}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var absoluteSnapshotPath = Path.GetFullPath(snapshotPath);
|
||||
MimirMoveProofEvidenceFrameSnapshotDocument snapshot;
|
||||
MimirMuninnMoveEvidenceStreamFrame decoded;
|
||||
try
|
||||
{
|
||||
snapshot = MimirMoveProofEvidenceFrameSnapshot.Load(absoluteSnapshotPath);
|
||||
decoded = MimirMuninnMoveEvidenceAdapter.DeserializeStreamFrame(snapshot.Payload);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or MessagePackSerializationException)
|
||||
{
|
||||
Console.Error.WriteLine($"move-proof-runtime-snapshot-file-smoke snapshot-decode-failed path={absoluteSnapshotPath} error={ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var suffix = SequenceSuffix(decoded.FrameId);
|
||||
var decodedSamples = MimirMuninnMoveEvidenceAdapter.BuildNativeSamples(
|
||||
decoded.MarkerCandidates,
|
||||
decoded.ControllerStates);
|
||||
var markerSamples = decodedSamples
|
||||
.Where(sample => sample.EvidenceKind == (uint)MimirNativeMoveEvidenceKind.OpticalMarker)
|
||||
.ToArray();
|
||||
var controllerSamples = decodedSamples
|
||||
.Where(sample => sample.EvidenceKind == (uint)MimirNativeMoveEvidenceKind.ControllerState)
|
||||
.ToArray();
|
||||
var distinctMarkerWitnesses = markerSamples
|
||||
.Select(sample => sample.WitnessIdHash)
|
||||
.Where(hash => hash != 0UL)
|
||||
.Distinct()
|
||||
.Count();
|
||||
var markerWitnessSummary = string.Join(
|
||||
",",
|
||||
decoded.MarkerCandidates
|
||||
.GroupBy(marker => marker.SourceIdHash)
|
||||
.OrderBy(group => group.Key)
|
||||
.Select(group =>
|
||||
$"0x{group.Key:X16}:{group.Count()}@peak{group.Max(marker => marker.PeakLuma)}:area{group.Max(marker => marker.AreaPx)}:radius{group.Max(marker => marker.RadiusPx):0.0}:score{group.Max(marker => marker.Score):0.000}"));
|
||||
var nearestControllerSkewMs = markerSamples.Length == 0 || controllerSamples.Length == 0
|
||||
? double.NaN
|
||||
: markerSamples
|
||||
.Select(marker => controllerSamples
|
||||
.Min(controller => TimestampDelta(marker.SourceTimestampNs, controller.SourceTimestampNs)))
|
||||
.Min() / 1_000_000.0;
|
||||
var configuration = CreateMoveProofConfigurationForSnapshot(
|
||||
nativeReservoirPath,
|
||||
absoluteSnapshotPath,
|
||||
snapshot,
|
||||
decoded);
|
||||
var configErrors = configuration.Validate();
|
||||
if (configErrors.Length > 0)
|
||||
{
|
||||
Console.Error.WriteLine($"move-proof-runtime-snapshot-file-smoke config-invalid errors={string.Join("|", configErrors)}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
using var runtime = new MimirRuntime(
|
||||
new AquariumRuntimeOptions(Headless: true, CultCachePath: ""),
|
||||
new MimirRuntimeConfiguration
|
||||
{
|
||||
Settings = new MimirSynchronizationSettings(),
|
||||
MoveProofSources = [configuration],
|
||||
});
|
||||
runtime.OnSceneReady();
|
||||
var input = new InputState();
|
||||
runtime.Update(0.016f, input);
|
||||
var proofFrame = runtime.Frame;
|
||||
var proofSurface = runtime.LatestMoveProofSurface;
|
||||
var proofSplineCount = proofFrame.Scene.SplineFrame.Splines.Count(spline =>
|
||||
spline.Id.StartsWith("move-proof-", StringComparison.Ordinal));
|
||||
var activationStatus = runtime.MoveProofActivationStatuses.LastOrDefault(status =>
|
||||
string.Equals(status.EvidenceStreamId, configuration.EvidenceStreamId, StringComparison.Ordinal));
|
||||
var activationDocument = runtime
|
||||
.CreateMoveProofRuntimeActivationDocuments(snapshot.CapturedAtNs == 0 ? (ulong)decoded.PublishedAtNs : snapshot.CapturedAtNs)
|
||||
.SingleOrDefault(document => string.Equals(document.EvidenceStreamId, configuration.EvidenceStreamId, StringComparison.Ordinal));
|
||||
var expectedMimirEvidenceFrameId = $"{configuration.MimirEvidenceFramePrefix}:{suffix}";
|
||||
var expectedMimirPoseFrameId = $"{configuration.MimirPoseFramePrefix}:{suffix}";
|
||||
var expectedFensalirFrameId = $"{configuration.FensalirFramePrefix}:{suffix}";
|
||||
|
||||
Console.WriteLine(
|
||||
$"move-proof-runtime-snapshot-file-smoke snapshot=\"{absoluteSnapshotPath}\" proof={proofSurface?.ProofId ?? "none"} chain={proofSurface?.MuninnEvidenceFrameId ?? "none"}->{proofSurface?.MimirEvidenceFrameId ?? "none"}->{proofSurface?.MimirPoseFrameId ?? "none"}->{proofSurface?.FensalirFrameId ?? "none"} verdict={proofSurface?.Verdict.ToString() ?? "none"} fullPose={proofSurface?.IsFullPose.ToString() ?? "False"} markers={proofSurface?.OpticalMarkerCount.ToString() ?? "0"} controllers={proofSurface?.ControllerStateCount.ToString() ?? "0"} poses={proofSurface?.PoseCount.ToString() ?? "0"} markerWitnesses={distinctMarkerWitnesses} markerWitnessSummary=\"{markerWitnessSummary}\" nearestControllerSkewMs={nearestControllerSkewMs:0.000} failure=\"{proofSurface?.FailureReason ?? ""}\" splines={proofSplineCount} activationActive={activationStatus?.Active.ToString() ?? "False"} activationDiagnostic=\"{activationStatus?.Diagnostic ?? ""}\" activationDocumentActive={activationDocument?.Active.ToString() ?? "False"} requireFullPose={requireFullPose}");
|
||||
|
||||
return proofSurface is not null &&
|
||||
activationStatus?.Active == true &&
|
||||
activationDocument is { Active: true, DriverRegistered: true } &&
|
||||
activationDocument.Diagnostic.Contains("one-copy evidence snapshot supplied", StringComparison.Ordinal) &&
|
||||
proofSurface.MuninnEvidenceFrameId == decoded.FrameId &&
|
||||
proofSurface.MimirEvidenceFrameId == expectedMimirEvidenceFrameId &&
|
||||
proofSurface.MimirPoseFrameId == expectedMimirPoseFrameId &&
|
||||
proofSurface.FensalirFrameId == expectedFensalirFrameId &&
|
||||
proofFrame.Scene.SplineFrame.HasInput &&
|
||||
proofSplineCount >= 1 &&
|
||||
(!requireFullPose || proofSurface.IsFullPose)
|
||||
? 0
|
||||
: 1;
|
||||
}
|
||||
|
||||
static MimirMoveProofRuntimeConfiguration CreateMoveProofConfigurationForSnapshot(
|
||||
string nativeReservoirPath,
|
||||
string snapshotPath,
|
||||
MimirMoveProofEvidenceFrameSnapshotDocument snapshot,
|
||||
MimirMuninnMoveEvidenceStreamFrame frame)
|
||||
{
|
||||
var cameras = frame.MarkerCandidates
|
||||
.Where(marker => marker.SourceIdHash != 0UL)
|
||||
.GroupBy(marker => marker.SourceIdHash)
|
||||
.Select((group, index) =>
|
||||
{
|
||||
var marker = group.First();
|
||||
var side = index == 0 ? -0.1 : 0.1;
|
||||
return new MimirMoveProofCameraCalibrationConfiguration
|
||||
{
|
||||
CameraId = string.IsNullOrWhiteSpace(marker.CameraId)
|
||||
? $"snapshot-camera-{index}"
|
||||
: $"{marker.HostId}:{marker.CameraId}",
|
||||
WitnessIdHash = group.Key,
|
||||
PositionMeters = new MimirVector3Snapshot(side, 0.0, 0.0),
|
||||
Orientation = new MimirQuaternionSnapshot(0.0, 0.0, 0.0, 1.0),
|
||||
FocalLengthXPx = 100.0,
|
||||
FocalLengthYPx = 100.0,
|
||||
PrincipalPointXPx = 160.0,
|
||||
PrincipalPointYPx = 120.0,
|
||||
};
|
||||
})
|
||||
.Take(2)
|
||||
.ToList();
|
||||
while (cameras.Count < 2)
|
||||
{
|
||||
var index = cameras.Count;
|
||||
cameras.Add(new MimirMoveProofCameraCalibrationConfiguration
|
||||
{
|
||||
CameraId = $"snapshot-calibration-placeholder-{index}",
|
||||
WitnessIdHash = 0xF00D_0000_0000_0001UL + (ulong)index,
|
||||
PositionMeters = new MimirVector3Snapshot(index == 0 ? -0.1 : 0.1, 0.0, 0.0),
|
||||
Orientation = new MimirQuaternionSnapshot(0.0, 0.0, 0.0, 1.0),
|
||||
FocalLengthXPx = 100.0,
|
||||
FocalLengthYPx = 100.0,
|
||||
PrincipalPointXPx = 160.0,
|
||||
PrincipalPointYPx = 120.0,
|
||||
});
|
||||
}
|
||||
|
||||
return new MimirMoveProofRuntimeConfiguration
|
||||
{
|
||||
EvidenceStreamId = snapshot.EvidenceStreamId,
|
||||
EvidenceSnapshotPath = snapshotPath,
|
||||
NativeReservoirPath = nativeReservoirPath,
|
||||
MimirEvidenceSourceId = "mimir:starfire:move-evidence",
|
||||
MimirEvidenceFramePrefix = "mimir:starfire:move-evidence",
|
||||
MimirPoseFramePrefix = "mimir:starfire:move-pose",
|
||||
MimirPoseProducerPeerId = "mimir:starfire",
|
||||
FensalirFramePrefix = "fensalir:starfire:presented-frame",
|
||||
FusionAuthorityId = "mimir.runtime.move-fusion",
|
||||
ConsumerContract = "fensalir.move-controller-input",
|
||||
Calibration = new MimirMoveProofCalibrationConfiguration
|
||||
{
|
||||
CalibrationId = "mimir-move-snapshot-replay-calibration-v1",
|
||||
TrackingSpaceId = "mimir-stage-space",
|
||||
Cameras = cameras,
|
||||
MaximumAssociationSkewMilliseconds = 1000.0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
static string SequenceSuffix(string frameId)
|
||||
{
|
||||
var separator = frameId.LastIndexOf(':');
|
||||
return separator >= 0 && separator < frameId.Length - 1
|
||||
? frameId[(separator + 1)..]
|
||||
: frameId;
|
||||
}
|
||||
|
||||
static ulong TimestampDelta(ulong left, ulong right) => left >= right ? left - right : right - left;
|
||||
|
||||
static int RunMoveProofPresentedFrameSmoke(string outputPath)
|
||||
{
|
||||
var absoluteOutputPath = Path.GetFullPath(outputPath);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
<ProjectReference Include="..\..\..\Fensalir\src\Aquarium.Engine.Contracts\Aquarium.Engine.Contracts.csproj" />
|
||||
<ProjectReference Include="..\..\..\CultLib\src\GameCult.Caching.MessagePack\GameCult.Caching.MessagePack.csproj" />
|
||||
<ProjectReference Include="..\..\..\CultLib\src\GameCult.Mesh\GameCult.Mesh.csproj" />
|
||||
<ProjectReference Include="..\..\..\CultLib\src\GameCult.Networking\GameCult.Networking.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -15,17 +15,23 @@ public interface IMimirMoveProofEvidenceRingProvider
|
|||
public sealed class MimirMoveProofEvidenceRingLease : IDisposable
|
||||
{
|
||||
private readonly bool ownsRing;
|
||||
private readonly IDisposable? transportOwner;
|
||||
|
||||
public MimirMoveProofEvidenceRingLease(CultMeshSharedMemoryFrameRing ring, bool ownsRing = false)
|
||||
public MimirMoveProofEvidenceRingLease(
|
||||
CultMeshSharedMemoryFrameRing ring,
|
||||
bool ownsRing = false,
|
||||
IDisposable? transportOwner = null)
|
||||
{
|
||||
Ring = ring ?? throw new ArgumentNullException(nameof(ring));
|
||||
this.ownsRing = ownsRing;
|
||||
this.transportOwner = transportOwner;
|
||||
}
|
||||
|
||||
public CultMeshSharedMemoryFrameRing Ring { get; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
transportOwner?.Dispose();
|
||||
if (ownsRing)
|
||||
{
|
||||
Ring.Dispose();
|
||||
|
|
@ -42,7 +48,7 @@ public sealed record MimirMoveProofEvidenceFrameSnapshotDocument(
|
|||
[property: Key(1)] string EvidenceStreamId,
|
||||
[property: Key(2)] string FrameId,
|
||||
[property: Key(3)] string ProducerPeerId,
|
||||
[property: Key(4)] long PublishedAtNs,
|
||||
[property: Key(4)] ulong PublishedAtNs,
|
||||
[property: Key(5)] ulong CapturedAtNs,
|
||||
[property: Key(6)] byte[] Payload);
|
||||
|
||||
|
|
@ -65,7 +71,7 @@ public static class MimirMoveProofEvidenceFrameSnapshot
|
|||
EvidenceStreamId: StreamIdFromFrameId(frame.FrameId),
|
||||
FrameId: frame.FrameId,
|
||||
ProducerPeerId: frame.ProducerPeerId,
|
||||
PublishedAtNs: frame.PublishedAtNs,
|
||||
PublishedAtNs: checked((ulong)frame.PublishedAtNs),
|
||||
CapturedAtNs: capturedAtNs,
|
||||
Payload: payload);
|
||||
}
|
||||
|
|
@ -192,6 +198,14 @@ public sealed class MimirConfiguredMoveProofEvidenceRingProvider : IMimirMovePro
|
|||
out string diagnostic)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
if (!string.IsNullOrWhiteSpace(configuration.OdinCultMeshUri))
|
||||
{
|
||||
return MimirOdinMoveProofEvidenceRingProvider.Instance.TryOpenEvidenceRing(
|
||||
configuration,
|
||||
out lease,
|
||||
out diagnostic);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(configuration.EvidenceSnapshotPath))
|
||||
{
|
||||
return MimirUnavailableMoveProofEvidenceRingProvider.Instance.TryOpenEvidenceRing(
|
||||
|
|
@ -219,7 +233,7 @@ public sealed class MimirConfiguredMoveProofEvidenceRingProvider : IMimirMovePro
|
|||
var decoded = MimirMuninnMoveEvidenceAdapter.DeserializeStreamFrame(snapshot.Payload);
|
||||
if (!string.Equals(decoded.FrameId, snapshot.FrameId, StringComparison.Ordinal) ||
|
||||
!string.Equals(decoded.ProducerPeerId, snapshot.ProducerPeerId, StringComparison.Ordinal) ||
|
||||
decoded.PublishedAtNs != snapshot.PublishedAtNs)
|
||||
decoded.PublishedAtNs != checked((long)snapshot.PublishedAtNs))
|
||||
{
|
||||
lease = null;
|
||||
diagnostic = "Move proof evidence snapshot metadata does not match its encoded Muninn frame payload.";
|
||||
|
|
@ -230,7 +244,7 @@ public sealed class MimirConfiguredMoveProofEvidenceRingProvider : IMimirMovePro
|
|||
configuration.EvidenceStreamId,
|
||||
slotCount: 1,
|
||||
slotByteLength: snapshot.Payload.Length);
|
||||
if (!ring.TryPublishCopy(snapshot.Payload, snapshot.PublishedAtNs, durationNs: 0, out _))
|
||||
if (!ring.TryPublishCopy(snapshot.Payload, checked((long)snapshot.PublishedAtNs), durationNs: 0, out _))
|
||||
{
|
||||
ring.Dispose();
|
||||
lease = null;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@ public sealed class MimirMoveProofRuntimeConfiguration
|
|||
|
||||
public string EvidenceSnapshotPath { get; init; } = "";
|
||||
|
||||
public string OdinCultMeshUri { get; init; } = "";
|
||||
|
||||
public string MuninnProviderId { get; init; } = "muninn.telemetry.nightwing";
|
||||
|
||||
public string NativeReservoirPath { get; init; } = "";
|
||||
|
||||
public string MimirEvidenceSourceId { get; init; } = "mimir:starfire:move-evidence";
|
||||
|
|
@ -37,6 +41,10 @@ public sealed class MimirMoveProofRuntimeConfiguration
|
|||
|
||||
var errors = new List<string>();
|
||||
Require(EvidenceStreamId, nameof(EvidenceStreamId), errors);
|
||||
if (!string.IsNullOrWhiteSpace(OdinCultMeshUri))
|
||||
{
|
||||
Require(MuninnProviderId, nameof(MuninnProviderId), errors);
|
||||
}
|
||||
Require(NativeReservoirPath, nameof(NativeReservoirPath), errors);
|
||||
Require(MimirEvidenceSourceId, nameof(MimirEvidenceSourceId), errors);
|
||||
Require(MimirEvidenceFramePrefix, nameof(MimirEvidenceFramePrefix), errors);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,216 @@
|
|||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using GameCult.Mesh;
|
||||
using GameCult.Networking;
|
||||
using MessagePack;
|
||||
|
||||
namespace Mimir.Runtime.Synchronization;
|
||||
|
||||
public sealed record MimirDiscoveredCultMeshInputStream(
|
||||
string ProviderId,
|
||||
string StreamId,
|
||||
string Schema,
|
||||
string Address,
|
||||
string Channel,
|
||||
uint ConnectionId);
|
||||
|
||||
public sealed class MimirOdinMoveProofEvidenceRingProvider : IMimirMoveProofEvidenceRingProvider
|
||||
{
|
||||
private const uint OdinSnapshotConnectionId = 0x0d1d0002;
|
||||
public static MimirOdinMoveProofEvidenceRingProvider Instance { get; } = new();
|
||||
|
||||
private MimirOdinMoveProofEvidenceRingProvider() { }
|
||||
|
||||
public bool TryOpenEvidenceRing(
|
||||
MimirMoveProofRuntimeConfiguration configuration,
|
||||
out MimirMoveProofEvidenceRingLease? lease,
|
||||
out string diagnostic)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
lease = null;
|
||||
MimirMoveEvidenceRudpPump? pump = null;
|
||||
CultMeshSharedMemoryFrameRing? ring = null;
|
||||
try
|
||||
{
|
||||
var stream = Discover(configuration.OdinCultMeshUri, configuration.MuninnProviderId, configuration.EvidenceStreamId);
|
||||
ring = new CultMeshSharedMemoryFrameRing(configuration.EvidenceStreamId, slotCount: 4, slotByteLength: 256 * 1024);
|
||||
pump = new MimirMoveEvidenceRudpPump(stream, ring);
|
||||
pump.Start();
|
||||
lease = new MimirMoveProofEvidenceRingLease(ring, ownsRing: true, transportOwner: pump);
|
||||
diagnostic = $"Odin discovered {stream.ProviderId}/{stream.StreamId}; lowering {stream.Address}/{stream.Channel} into local CultMesh ring";
|
||||
return true;
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
pump?.Dispose();
|
||||
ring?.Dispose();
|
||||
diagnostic = error.Message;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static MimirDiscoveredCultMeshInputStream Discover(string odinCultMeshUri, string providerId, string streamId)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(odinCultMeshUri);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(providerId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(streamId);
|
||||
var endpoint = CultMesh.ResolveRudpEndpoint(odinCultMeshUri);
|
||||
using var transport = CultMesh.CreateRudpClient("mimir-odin-discovery", OdinSnapshotConnectionId, endpoint);
|
||||
if (!transport.ConnectAndWait([], TimeSpan.FromSeconds(5), TimeSpan.FromMilliseconds(10)))
|
||||
{
|
||||
throw new TimeoutException($"Timed out connecting to Odin at {odinCultMeshUri}.");
|
||||
}
|
||||
|
||||
var messageId = $"mimir-muninn-discovery:{Guid.NewGuid():N}";
|
||||
transport.SendSchemaMessage(new CultNetSnapshotRequestMessage
|
||||
{
|
||||
MessageId = messageId,
|
||||
SchemaIds = ["gamecult.eve.surface_state.v1"],
|
||||
RecordKeys = ["surface:gamecult.network.status"]
|
||||
});
|
||||
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(5);
|
||||
while (DateTime.UtcNow < deadline)
|
||||
{
|
||||
var message = transport.ReceiveSchemaMessage(TimeSpan.FromMilliseconds(250), TimeSpan.FromMilliseconds(10));
|
||||
if (message is CultNetErrorMessage error)
|
||||
{
|
||||
throw new InvalidOperationException(error.Error);
|
||||
}
|
||||
if (message is CultNetSnapshotResponseRawMessage response &&
|
||||
string.Equals(response.MessageId, messageId, StringComparison.Ordinal))
|
||||
{
|
||||
return SelectStream(response, providerId, streamId);
|
||||
}
|
||||
}
|
||||
throw new TimeoutException($"Timed out waiting for Odin provider snapshot from {odinCultMeshUri}.");
|
||||
}
|
||||
|
||||
public static MimirDiscoveredCultMeshInputStream SelectStream(
|
||||
CultNetSnapshotResponseRawMessage response,
|
||||
string providerId,
|
||||
string streamId)
|
||||
{
|
||||
var document = response.Documents.FirstOrDefault(candidate =>
|
||||
string.Equals(candidate.SchemaId, "gamecult.eve.surface_state.v1", StringComparison.Ordinal) &&
|
||||
string.Equals(candidate.RecordKey, "surface:gamecult.network.status", StringComparison.Ordinal))
|
||||
?? throw new InvalidOperationException("Odin snapshot omitted surface:gamecult.network.status.");
|
||||
using var json = JsonDocument.Parse(MessagePackSerializer.ConvertToJson(document.Payload));
|
||||
if (!json.RootElement.TryGetProperty("providerCatalog", out var providers))
|
||||
{
|
||||
throw new InvalidOperationException("Odin network status omitted providerCatalog.");
|
||||
}
|
||||
foreach (var provider in providers.EnumerateArray())
|
||||
{
|
||||
if (!string.Equals(Text(provider, "id"), providerId, StringComparison.Ordinal) ||
|
||||
!provider.TryGetProperty("inputStreams", out var streams))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
foreach (var stream in streams.EnumerateArray())
|
||||
{
|
||||
if (!string.Equals(Text(stream, "streamId"), streamId, StringComparison.Ordinal) ||
|
||||
!string.Equals(Text(stream, "schema"), MimirMuninnMoveEvidenceAdapter.StreamMetadataSchemaId, StringComparison.Ordinal) ||
|
||||
!string.Equals(Text(stream, "transport"), "cultnet.transport.rudp.v0", StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
return new MimirDiscoveredCultMeshInputStream(
|
||||
providerId,
|
||||
streamId,
|
||||
Text(stream, "schema"),
|
||||
Text(stream, "address"),
|
||||
Text(stream, "channel"),
|
||||
stream.GetProperty("connectionId").GetUInt32());
|
||||
}
|
||||
throw new InvalidOperationException($"Odin provider '{providerId}' has no compatible input stream '{streamId}'.");
|
||||
}
|
||||
throw new InvalidOperationException($"Odin has no provider '{providerId}'.");
|
||||
}
|
||||
|
||||
private static string Text(JsonElement value, string name) =>
|
||||
value.TryGetProperty(name, out var property) ? property.GetString() ?? "" : "";
|
||||
}
|
||||
|
||||
internal sealed class MimirMoveEvidenceRudpPump : IDisposable
|
||||
{
|
||||
private readonly MimirDiscoveredCultMeshInputStream stream;
|
||||
private readonly CultMeshSharedMemoryFrameRing ring;
|
||||
private readonly CancellationTokenSource stopping = new();
|
||||
private readonly ManualResetEventSlim started = new(false);
|
||||
private Thread? thread;
|
||||
private Exception? startupError;
|
||||
|
||||
public MimirMoveEvidenceRudpPump(MimirDiscoveredCultMeshInputStream stream, CultMeshSharedMemoryFrameRing ring)
|
||||
{
|
||||
this.stream = stream;
|
||||
this.ring = ring;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
thread = new Thread(Run) { IsBackground = true, Name = "mimir-muninn-move-evidence" };
|
||||
thread.Start();
|
||||
if (!started.Wait(TimeSpan.FromSeconds(6)))
|
||||
{
|
||||
throw new TimeoutException($"Timed out starting Move evidence receiver for {stream.Address}.");
|
||||
}
|
||||
if (startupError is not null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Could not connect Odin-discovered Move evidence stream {stream.StreamId} at {stream.Address}: {startupError.Message}",
|
||||
startupError);
|
||||
}
|
||||
}
|
||||
|
||||
private void Run()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var transport = CultMesh.CreateRudpClient(
|
||||
"mimir-move-evidence-consumer",
|
||||
stream.ConnectionId,
|
||||
CultMesh.ResolveRudpEndpoint($"rudp://{stream.Address}"));
|
||||
if (!transport.ConnectAndWait([], TimeSpan.FromSeconds(5), TimeSpan.FromMilliseconds(5)))
|
||||
{
|
||||
throw new TimeoutException($"Timed out connecting to {stream.Address}.");
|
||||
}
|
||||
transport.Send("hid.subscribe", Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new { streamId = stream.StreamId })));
|
||||
started.Set();
|
||||
while (!stopping.IsCancellationRequested)
|
||||
{
|
||||
transport.PollResends();
|
||||
var frame = transport.ReceiveOnce();
|
||||
if (frame is null || !string.Equals(frame.ChannelId, stream.Channel, StringComparison.Ordinal))
|
||||
{
|
||||
Thread.Sleep(2);
|
||||
continue;
|
||||
}
|
||||
var decoded = MimirMuninnMoveEvidenceAdapter.DeserializeStreamFrame(frame.Payload);
|
||||
if (!string.Equals(decoded.FrameId, stream.StreamId, StringComparison.Ordinal) &&
|
||||
!decoded.FrameId.StartsWith($"{stream.StreamId}:", StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
ring.TryPublishCopy(frame.Payload, decoded.PublishedAtNs, durationNs: 0, out _);
|
||||
}
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
if (!started.IsSet)
|
||||
{
|
||||
startupError = error;
|
||||
started.Set();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
stopping.Cancel();
|
||||
thread?.Join(TimeSpan.FromSeconds(2));
|
||||
started.Dispose();
|
||||
stopping.Dispose();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue