A3: brokkr-command, the caller CLI, with every intent field and a real CultLib root
Re-landed from the donor as a fresh file (read with git show, not merged). Changes from the donor: the CultLibRoot default is ..\..\CultLib instead of a directory that does not exist; every field of brokkr.unity.command_intent.v0 has a --kebab option (the donor mapped six of seventeen), and unknown options are refused before anything is written; keys come from BrokkrCommandLedger; exit is 0 accepted, 1 other receipt, 2 usage error or timeout; readHost opens the store read-only and prints the agent-commands flag; a timeout says when the editor reports the sink off. The intent gains the capture fields at keys 13-16, which the CLI needs and A4's executor will read. Tests run the built CLI as a process against the editor probe: field mapping, fresh id per invocation, exit code from the receipt, timeout leaves only the intent, unknown option writes nothing, and every action name reaches the editor once with one receipt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
4224a82df9
commit
1e46483fe5
8 changed files with 390 additions and 6 deletions
16
brokkr-command/Brokkr.Command.csproj
Normal file
16
brokkr-command/Brokkr.Command.csproj
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<CultLibRoot Condition="'$(CultLibRoot)' == ''">..\..\CultLib</CultLibRoot>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\brokkr-contracts\Brokkr.Contracts.csproj" />
|
||||
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Caching\GameCult.Caching.csproj" />
|
||||
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Caching.MessagePack\GameCult.Caching.MessagePack.csproj" />
|
||||
<PackageReference Include="MessagePack" Version="3.1.7" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
158
brokkr-command/Program.cs
Normal file
158
brokkr-command/Program.cs
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
// brokkr-command: a caller. It writes one Unity command intent into the project's directory store and reads the
|
||||
// receipt the editor service answers with. It never writes receipts, host snapshots or the agent-commands flag,
|
||||
// and it never opens the store as a single file: the editor owns execution and its own state.
|
||||
using System.Globalization;
|
||||
using GameCult.Brokkr;
|
||||
using GameCult.Caching;
|
||||
using GameCult.Caching.MessagePack;
|
||||
|
||||
try
|
||||
{
|
||||
return await Cli.RunAsync(args);
|
||||
}
|
||||
catch (Exception error) when (error is ArgumentException or TimeoutException or InvalidOperationException)
|
||||
{
|
||||
Console.Error.WriteLine(error.Message);
|
||||
return 2;
|
||||
}
|
||||
|
||||
internal static class Cli
|
||||
{
|
||||
// Every writable field of brokkr.unity.command_intent.v0 except the schema tag, the id and the action, which
|
||||
// have their own options.
|
||||
private static readonly Dictionary<string, Action<BrokkrUnityCommand, string>> Fields =
|
||||
new(StringComparer.Ordinal)
|
||||
{
|
||||
["--target-object-id"] = (command, value) => command.targetObjectId = value,
|
||||
["--name"] = (command, value) => command.name = value,
|
||||
["--component-type"] = (command, value) => command.componentType = value,
|
||||
["--property-path"] = (command, value) => command.propertyPath = value,
|
||||
["--value"] = (command, value) => command.value = value,
|
||||
["--asset-path"] = (command, value) => command.assetPath = value,
|
||||
["--parent-object-id"] = (command, value) => command.parentObjectId = value,
|
||||
["--local-position"] = (command, value) => command.localPosition = value,
|
||||
["--local-euler-angles"] = (command, value) => command.localEulerAngles = value,
|
||||
["--local-scale"] = (command, value) => command.localScale = value,
|
||||
["--view"] = (command, value) => command.viewKind = value,
|
||||
["--output"] = (command, value) => command.outputPath = value,
|
||||
["--width"] = (command, value) => command.width = Integer("--width", value),
|
||||
["--height"] = (command, value) => command.height = Integer("--height", value)
|
||||
};
|
||||
|
||||
internal static async Task<int> RunAsync(string[] args)
|
||||
{
|
||||
var options = Parse(args);
|
||||
var cachePath = Path.GetFullPath(Required(options, "--unity-cache"));
|
||||
var action = Required(options, "--action");
|
||||
var readHost = string.Equals(action, "readHost", StringComparison.Ordinal);
|
||||
|
||||
using var cache = await CultCacheMessagePack.OpenAsync(cachePath, new CultCacheOpenOptions
|
||||
{
|
||||
UseDirectoryStore = true,
|
||||
ReadOnly = readHost
|
||||
});
|
||||
return readHost ? await ReadHostAsync(cache) : await WriteIntentAsync(cache, action, options);
|
||||
}
|
||||
|
||||
private static async Task<int> ReadHostAsync(CultCache cache)
|
||||
{
|
||||
await cache.PullAllBackingStoresAsync();
|
||||
var host = ReadHostSnapshot(cache)
|
||||
?? throw new InvalidOperationException("Brokkr host snapshot 'unity/host/current' is unavailable.");
|
||||
Console.WriteLine($"project: {host.projectPath}");
|
||||
Console.WriteLine($"observed: {host.observedAt}");
|
||||
Console.WriteLine($"scene: {host.activeScenePath}");
|
||||
Console.WriteLine($"agent-commands: {(host.agentCommandsEnabled ? "on" : "off")}");
|
||||
Console.WriteLine($"objects: {host.sceneObjects.Length}");
|
||||
foreach (var sceneObject in host.sceneObjects.OrderBy(item => item.path, StringComparer.Ordinal))
|
||||
{
|
||||
var components = string.Join(", ", sceneObject.components.Select(component => component.typeName));
|
||||
Console.WriteLine($"- {sceneObject.path} active={sceneObject.activeSelf} layer={sceneObject.layer} components=[{components}]");
|
||||
foreach (var component in sceneObject.components)
|
||||
foreach (var property in component.properties.Where(property =>
|
||||
!property.path.StartsWith("m_", StringComparison.Ordinal) &&
|
||||
!string.IsNullOrWhiteSpace(property.value)))
|
||||
Console.WriteLine($" {component.typeName}.{property.path}={property.value}");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static async Task<int> WriteIntentAsync(CultCache cache, string action, Dictionary<string, string> options)
|
||||
{
|
||||
var command = new BrokkrUnityCommand
|
||||
{
|
||||
// A command id is single-use, so every invocation is a new act with a new id. --command-id exists for
|
||||
// callers that need to name their own; reusing an answered id is a caller defect.
|
||||
commandId = options.TryGetValue("--command-id", out var commandId) && !string.IsNullOrWhiteSpace(commandId)
|
||||
? commandId
|
||||
: Guid.NewGuid().ToString("N"),
|
||||
action = action
|
||||
};
|
||||
foreach (var (name, value) in options)
|
||||
if (Fields.TryGetValue(name, out var assign))
|
||||
assign(command, value);
|
||||
|
||||
await cache.AddAsync(command, new CultRecordHandle<BrokkrUnityCommand>(BrokkrCommandLedger.CommandKey(command.commandId)));
|
||||
await cache.FlushAsync();
|
||||
|
||||
var waitMilliseconds = options.TryGetValue("--wait-ms", out var wait) ? Integer("--wait-ms", wait) : 10000;
|
||||
if (waitMilliseconds <= 0)
|
||||
{
|
||||
Console.WriteLine(command.commandId);
|
||||
return 0;
|
||||
}
|
||||
|
||||
var receiptKey = BrokkrCommandLedger.ReceiptKey(command.commandId);
|
||||
var deadline = DateTimeOffset.UtcNow.AddMilliseconds(waitMilliseconds);
|
||||
while (DateTimeOffset.UtcNow < deadline)
|
||||
{
|
||||
await Task.Delay(100);
|
||||
await cache.PullAllBackingStoresAsync();
|
||||
if (!cache.TryGet(receiptKey, out BrokkrUnityCommandReceipt? receipt) || receipt == null)
|
||||
continue;
|
||||
|
||||
Console.WriteLine($"{receipt.status}: {receipt.message}");
|
||||
if (!string.IsNullOrWhiteSpace(receipt.objectId))
|
||||
Console.WriteLine(receipt.objectId);
|
||||
// The exit code is the receipt's verdict, never the write's success.
|
||||
return string.Equals(receipt.status, "accepted", StringComparison.Ordinal) ? 0 : 1;
|
||||
}
|
||||
|
||||
var host = ReadHostSnapshot(cache);
|
||||
var why = host is { agentCommandsEnabled: false }
|
||||
? " The editor reports agent commands off; the operator enables them in the Brokkr window."
|
||||
: "";
|
||||
throw new TimeoutException(
|
||||
$"Timed out waiting for Brokkr receipt '{receiptKey.Value}'; the intent stays written.{why}");
|
||||
}
|
||||
|
||||
private static BrokkrHostSnapshot? ReadHostSnapshot(CultCache cache) =>
|
||||
cache.TryGet(new CultRecordKey("unity/host/current"), out BrokkrHostSnapshot? host) ? host : null;
|
||||
|
||||
private static Dictionary<string, string> Parse(string[] args)
|
||||
{
|
||||
var values = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
for (var index = 0; index < args.Length; index += 2)
|
||||
{
|
||||
if (index + 1 >= args.Length || !args[index].StartsWith("--", StringComparison.Ordinal))
|
||||
throw new ArgumentException("Arguments must be --name value pairs.");
|
||||
var name = args[index];
|
||||
if (name is not ("--unity-cache" or "--action" or "--command-id" or "--wait-ms") && !Fields.ContainsKey(name))
|
||||
throw new ArgumentException($"Unknown option {name}.");
|
||||
values[name] = args[index + 1];
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
private static string Required(Dictionary<string, string> options, string name) =>
|
||||
options.TryGetValue(name, out var value) && !string.IsNullOrWhiteSpace(value)
|
||||
? value
|
||||
: throw new ArgumentException($"{name} is required.");
|
||||
|
||||
private static int Integer(string name, string value) =>
|
||||
int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed)
|
||||
? parsed
|
||||
: throw new ArgumentException($"{name} must be an integer.");
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\brokkr-contracts\Brokkr.Contracts.csproj" />
|
||||
<ProjectReference Include="..\brokkr-command\Brokkr.Command.csproj" />
|
||||
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Caching.MessagePack\GameCult.Caching.MessagePack.csproj" />
|
||||
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Mesh\GameCult.Mesh.csproj" />
|
||||
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Networking\GameCult.Networking.csproj" />
|
||||
|
|
|
|||
188
brokkr-contracts.Tests/BrokkrCommandCliTests.cs
Normal file
188
brokkr-contracts.Tests/BrokkrCommandCliTests.cs
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
using System.Diagnostics;
|
||||
using GameCult.Brokkr;
|
||||
using GameCult.Caching;
|
||||
|
||||
namespace Brokkr.Contracts.Tests;
|
||||
|
||||
// brokkr-command as a process, against the real directory store and the editor probe.
|
||||
public sealed class BrokkrCommandCliTests
|
||||
{
|
||||
private static readonly string CliDll = Path.Combine(AppContext.BaseDirectory, "Brokkr.Command.dll");
|
||||
|
||||
private static async Task<(int Exit, string Out, string Error)> RunAsync(params string[] args)
|
||||
{
|
||||
var start = new ProcessStartInfo("dotnet")
|
||||
{
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true
|
||||
};
|
||||
start.ArgumentList.Add(CliDll);
|
||||
foreach (var arg in args) start.ArgumentList.Add(arg);
|
||||
using var process = Process.Start(start)!;
|
||||
var stdout = process.StandardOutput.ReadToEndAsync();
|
||||
var stderr = process.StandardError.ReadToEndAsync();
|
||||
await process.WaitForExitAsync();
|
||||
return (process.ExitCode, await stdout, await stderr);
|
||||
}
|
||||
|
||||
private static BrokkrUnityCommand[] StoredIntents(ScratchProject project)
|
||||
{
|
||||
using var audit = project.OpenCache();
|
||||
return audit.AllStoredDocuments.Select(entry => entry.Document).OfType<BrokkrUnityCommand>().ToArray();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EveryIntentFieldReachesTheStore()
|
||||
{
|
||||
using var project = new ScratchProject();
|
||||
|
||||
var (exit, output, _) = await RunAsync(
|
||||
"--unity-cache", project.StorePath, "--action", "setGameObjectTransform", "--command-id", "all-fields",
|
||||
"--wait-ms", "0",
|
||||
"--target-object-id", "obj-1", "--name", "Widget", "--component-type", "Light", "--property-path", "m_Range",
|
||||
"--value", "12", "--asset-path", "Assets/x.prefab", "--parent-object-id", "parent-1",
|
||||
"--local-position", "1,2,3", "--local-euler-angles", "4,5,6", "--local-scale", "7,8,9",
|
||||
"--view", "scene", "--output", "artifacts/shot.png", "--width", "640", "--height", "480");
|
||||
|
||||
Assert.Equal(0, exit);
|
||||
Assert.Equal("all-fields", output.Trim());
|
||||
var intent = Assert.Single(StoredIntents(project));
|
||||
Assert.Equal(
|
||||
new[]
|
||||
{
|
||||
"all-fields", "setGameObjectTransform", "obj-1", "Widget", "Light", "m_Range", "12", "Assets/x.prefab",
|
||||
"parent-1", "1,2,3", "4,5,6", "7,8,9", "scene", "artifacts/shot.png"
|
||||
},
|
||||
new[]
|
||||
{
|
||||
intent.commandId, intent.action, intent.targetObjectId, intent.name, intent.componentType,
|
||||
intent.propertyPath, intent.value, intent.assetPath, intent.parentObjectId, intent.localPosition,
|
||||
intent.localEulerAngles, intent.localScale, intent.viewKind, intent.outputPath
|
||||
});
|
||||
Assert.Equal((640, 480), (intent.width, intent.height));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EveryInvocationMintsAFreshId()
|
||||
{
|
||||
using var project = new ScratchProject();
|
||||
|
||||
var first = await RunAsync("--unity-cache", project.StorePath, "--action", "refreshAssets", "--wait-ms", "0");
|
||||
var second = await RunAsync("--unity-cache", project.StorePath, "--action", "refreshAssets", "--wait-ms", "0");
|
||||
|
||||
Assert.Equal(0, first.Exit);
|
||||
Assert.Equal(0, second.Exit);
|
||||
Assert.NotEqual(first.Out.Trim(), second.Out.Trim());
|
||||
Assert.Equal(2, StoredIntents(project).Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExitCodeIsTheReceiptsVerdict()
|
||||
{
|
||||
using var project = new ScratchProject();
|
||||
using var editor = await EditorProbe.StartAsync(project.StorePath);
|
||||
editor.StatusFor = command => command.action == "createGameObject" ? "failed" : "accepted";
|
||||
using var stop = new CancellationTokenSource();
|
||||
var loop = Task.Run(async () =>
|
||||
{
|
||||
while (!stop.IsCancellationRequested)
|
||||
{
|
||||
await editor.TickAsync();
|
||||
await Task.Delay(50);
|
||||
}
|
||||
});
|
||||
|
||||
var accepted = await RunAsync("--unity-cache", project.StorePath, "--action", "refreshAssets", "--wait-ms", "20000");
|
||||
var failed = await RunAsync("--unity-cache", project.StorePath, "--action", "createGameObject", "--wait-ms", "20000");
|
||||
stop.Cancel();
|
||||
await loop;
|
||||
|
||||
Assert.Equal(0, accepted.Exit);
|
||||
Assert.StartsWith("accepted:", accepted.Out);
|
||||
Assert.Equal(1, failed.Exit);
|
||||
Assert.StartsWith("failed:", failed.Out);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NoEditorAnswerIsATimeoutAndLeavesOnlyTheIntent()
|
||||
{
|
||||
using var project = new ScratchProject();
|
||||
|
||||
var result = await RunAsync("--unity-cache", project.StorePath, "--action", "refreshAssets", "--wait-ms", "600");
|
||||
|
||||
Assert.Equal(2, result.Exit);
|
||||
Assert.Contains("Timed out", result.Error);
|
||||
using var audit = project.OpenCache();
|
||||
var documents = audit.AllStoredDocuments.ToArray();
|
||||
Assert.Single(documents);
|
||||
Assert.IsType<BrokkrUnityCommand>(documents[0].Document);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UnknownOptionIsRefusedBeforeAnythingIsWritten()
|
||||
{
|
||||
using var project = new ScratchProject();
|
||||
|
||||
var result = await RunAsync("--unity-cache", project.StorePath, "--action", "refreshAssets", "--bogus", "1");
|
||||
|
||||
Assert.Equal(2, result.Exit);
|
||||
Assert.Contains("Unknown option --bogus", result.Error);
|
||||
Assert.False(Directory.Exists(Path.GetDirectoryName(project.StorePath)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadHostShowsWhetherTheSinkIsEnabled()
|
||||
{
|
||||
using var project = new ScratchProject();
|
||||
using var editor = await EditorProbe.StartAsync(project.StorePath);
|
||||
await editor.TickAsync();
|
||||
|
||||
var result = await RunAsync("--unity-cache", project.StorePath, "--action", "readHost");
|
||||
|
||||
Assert.Equal(0, result.Exit);
|
||||
Assert.Contains("agent-commands: on", result.Out);
|
||||
Assert.Contains("project: probe", result.Out);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("createGameObject")]
|
||||
[InlineData("attachComponent")]
|
||||
[InlineData("setGameObjectTransform")]
|
||||
[InlineData("setGameObjectActive")]
|
||||
[InlineData("setGameObjectParent")]
|
||||
[InlineData("setComponentProperty")]
|
||||
[InlineData("instantiatePrefab")]
|
||||
[InlineData("createPrefabVariant")]
|
||||
[InlineData("assignMaterial")]
|
||||
[InlineData("createScriptableObject")]
|
||||
[InlineData("refreshAssets")]
|
||||
[InlineData("setEditorPlayState")]
|
||||
[InlineData("setEditorPaused")]
|
||||
[InlineData("captureEditorView")]
|
||||
public async Task EveryActionNameReachesTheEditorOnceWithOneReceipt(string action)
|
||||
{
|
||||
using var project = new ScratchProject();
|
||||
using var editor = await EditorProbe.StartAsync(project.StorePath);
|
||||
using var stop = new CancellationTokenSource();
|
||||
var loop = Task.Run(async () =>
|
||||
{
|
||||
while (!stop.IsCancellationRequested)
|
||||
{
|
||||
await editor.TickAsync();
|
||||
await Task.Delay(50);
|
||||
}
|
||||
});
|
||||
|
||||
var result = await RunAsync("--unity-cache", project.StorePath, "--action", action, "--wait-ms", "20000");
|
||||
await Task.Delay(200);
|
||||
stop.Cancel();
|
||||
await loop;
|
||||
|
||||
Assert.Equal(0, result.Exit);
|
||||
var executed = Assert.Single(editor.Executed);
|
||||
Assert.Equal(action, executed.action);
|
||||
using var audit = project.OpenCache();
|
||||
var receipt = Assert.Single(audit.AllStoredDocuments.Select(entry => entry.Document).OfType<BrokkrUnityCommandReceipt>());
|
||||
Assert.Equal(executed.commandId, receipt.commandId);
|
||||
}
|
||||
}
|
||||
|
|
@ -9,13 +9,16 @@ namespace Brokkr.Contracts.Tests;
|
|||
// The scenario harness: an editor without Unity. It opens the store exactly as BrokkrCultMeshMirror does (a CultMesh
|
||||
// node with StartServer=false over a directory store and durable shard logs) and runs one service tick in the
|
||||
// service order: pull, drain pending intents through the ledger, receipt, soft flush, host snapshot, soft flush.
|
||||
// "Executing" an intent only records its id, so the scenarios measure delivery and once-only execution, not Unity.
|
||||
// "Executing" an intent only records it, so the scenarios measure delivery and once-only execution, not Unity.
|
||||
internal sealed class EditorProbe : IDisposable
|
||||
{
|
||||
private readonly CultMeshNode node;
|
||||
private readonly BrokkrCommandLedger ledger;
|
||||
|
||||
internal List<string> Executed { get; } = new();
|
||||
internal List<BrokkrUnityCommand> Executed { get; } = new();
|
||||
|
||||
// What the "editor" answers with. Real Unity answers accepted or failed per the executor.
|
||||
internal Func<BrokkrUnityCommand, string> StatusFor { get; set; } = _ => "accepted";
|
||||
|
||||
private EditorProbe(CultMeshNode node)
|
||||
{
|
||||
|
|
@ -41,13 +44,13 @@ internal sealed class EditorProbe : IDisposable
|
|||
await ledger.PullAsync();
|
||||
while (ledger.TryNextPending(out var command))
|
||||
{
|
||||
Executed.Add(command.commandId);
|
||||
Executed.Add(command);
|
||||
await node.Database.PutAsync(
|
||||
BrokkrCommandLedger.ReceiptKey(command.commandId),
|
||||
new BrokkrUnityCommandReceipt
|
||||
{
|
||||
commandId = command.commandId,
|
||||
status = "accepted",
|
||||
status = StatusFor(command),
|
||||
message = "probe",
|
||||
observedAt = DateTime.UtcNow.ToString("O")
|
||||
});
|
||||
|
|
@ -56,7 +59,7 @@ internal sealed class EditorProbe : IDisposable
|
|||
|
||||
await node.Database.PutAsync(
|
||||
new CultRecordKey("unity/host/current"),
|
||||
new BrokkrHostSnapshot { observedAt = DateTime.UtcNow.ToString("O"), projectPath = "probe" });
|
||||
new BrokkrHostSnapshot { observedAt = DateTime.UtcNow.ToString("O"), projectPath = "probe", agentCommandsEnabled = true });
|
||||
await node.FlushAsync(soft: true);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ public sealed class EditorScenarioTests
|
|||
await editorLoop;
|
||||
|
||||
Assert.Equal(commands, editor.Executed.Count);
|
||||
Assert.Equal(ids.OrderBy(id => id), editor.Executed.OrderBy(id => id));
|
||||
Assert.Equal(ids.OrderBy(id => id), editor.Executed.Select(command => command.commandId).OrderBy(id => id));
|
||||
using var audit = project.OpenCache();
|
||||
var receipts = audit.AllStoredDocuments.Select(entry => entry.Document).OfType<BrokkrUnityCommandReceipt>().ToArray();
|
||||
Assert.Equal(ids.OrderBy(id => id), receipts.Select(receipt => receipt.commandId).OrderBy(id => id));
|
||||
|
|
|
|||
|
|
@ -72,9 +72,17 @@ Primary sync documents:
|
|||
|
||||
Operational commands:
|
||||
|
||||
- `dotnet run --project brokkr-command/Brokkr.Command.csproj -- --unity-cache .brokkr/unity-editor.ccmp --action readHost`
|
||||
- `dotnet run --project brokkr-command/Brokkr.Command.csproj -- --unity-cache .brokkr/unity-editor.ccmp --action createGameObject --name Marker --local-position 0,1,0`
|
||||
- `brokkr-daemon sync-once --unity-cache .brokkr/unity-editor.ccmp --blender-cache .brokkr/blender-editor.ccmp`
|
||||
- `brokkr-daemon sync-loop --unity-cache .brokkr/unity-editor.ccmp --blender-cache .brokkr/blender-editor.ccmp --interval-ms 500`
|
||||
|
||||
`brokkr-command` is a caller. Each invocation writes one intent through the
|
||||
directory store (every intent field has a `--kebab-case` option; `--view` and
|
||||
`--output` fill the capture fields), waits for the receipt, and exits 0 for
|
||||
`accepted`, 1 for any other status, and 2 for a usage error or a timeout. It
|
||||
never writes receipts, host snapshots or the agent-commands flag.
|
||||
|
||||
`sync-loop` owns scheduling only. It repeatedly invokes the same sync decision
|
||||
primitive as `sync-once`, writes command intents and sync receipts through the
|
||||
mirrors, and prints per-pass telemetry to stdout.
|
||||
|
|
|
|||
|
|
@ -57,6 +57,11 @@ namespace GameCult.Brokkr
|
|||
[Key(10)] public string localPosition = "";
|
||||
[Key(11)] public string localEulerAngles = "";
|
||||
[Key(12)] public string localScale = "";
|
||||
// Editor view capture (A4): viewKind is scene or game; outputPath is relative to the project root.
|
||||
[Key(13)] public string viewKind = "";
|
||||
[Key(14)] public string outputPath = "";
|
||||
[Key(15)] public int width;
|
||||
[Key(16)] public int height;
|
||||
[Key(13)] public string[] materialNames = Array.Empty<string>();
|
||||
}
|
||||
|
||||
|
|
@ -112,6 +117,11 @@ namespace GameCult.Brokkr
|
|||
[Key(10)] public string localPosition = "";
|
||||
[Key(11)] public string localEulerAngles = "";
|
||||
[Key(12)] public string localScale = "";
|
||||
// Editor view capture (A4): viewKind is scene or game; outputPath is relative to the project root.
|
||||
[Key(13)] public string viewKind = "";
|
||||
[Key(14)] public string outputPath = "";
|
||||
[Key(15)] public int width;
|
||||
[Key(16)] public int height;
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
|
|
|
|||
Loading…
Reference in a new issue