Concrete use cases
| Situation | Gua.Runtime provides | Your adapter provides |
|---|---|---|
| Support another game engine | Managed C ABI wrapper, request queues, versions, bridge | UI enumeration, state conversion, and input delivery |
| Support a custom UI framework | Semantic node registration and correlated action results | Widget-to-role mapping and real widget operations |
| Expose an internal toolkit to Inspector/MCP | WebSocket bridge, UI-tree JSON, logs, screenshot requests | Rendering-loop and capture integration |
Install and deploy
dotnet add package Gua.RuntimeThe package targets net10.0 and netstandard2.1. Deploy matching gua.dll and gua_runtime.dll files for the target platform and architecture. Missing or mismatched libraries produce an explicit load error in the constructor.
One adapter frame
using System;
using System.Collections.Generic;
using Gua.Core;
using Gua.Runtime;
public sealed class CustomUiGuaAdapter(ICustomUi ui) : IDisposable
{
// Keep this at least as long as the largest remote action timeout.
private static readonly TimeSpan StaleIdRetention = TimeSpan.FromSeconds(30);
private readonly GuaRuntime runtime = new();
private readonly Dictionary<string, DateTimeOffset> retainedNodeIds =
new(StringComparer.Ordinal);
public void Start()
{
runtime.SetAdapterVersion("custom_ui", "1.0.0");
if (!runtime.StartInspectorBridge(GetBridgePort()))
throw new InvalidOperationException("Gua bridge could not start.");
}
private static int GetBridgePort()
{
const int defaultPort = 8765;
var value = Environment.GetEnvironmentVariable("GUA_BRIDGE_PORT");
if (string.IsNullOrWhiteSpace(value))
return defaultPort;
if (!int.TryParse(value, out var port) || port is < 1 or > 65535)
throw new InvalidOperationException("GUA_BRIDGE_PORT must be between 1 and 65535.");
return port;
}
public void Tick()
{
var retainUntil = DateTimeOffset.UtcNow + StaleIdRetention;
runtime.BeginFrame(ui.ScreenName);
foreach (var control in ui.Controls)
{
retainedNodeIds[control.Id] = retainUntil;
runtime.RegisterNode(new GuaNodeDescriptor(
control.Id, control.Role, control.Label,
new GuaBounds(control.X, control.Y, control.Width, control.Height),
Visible: control.Visible, Enabled: control.Enabled,
ParentId: control.ParentId,
Text: control.Sensitive ? null : control.Text,
Value: control.Sensitive ? null : control.Value,
Focused: control.Focused,
Checked: control.Checked));
}
runtime.EndFrame();
foreach (var nodeId in retainedNodeIds.Keys)
DrainActions(nodeId);
DrainActions(null); // global press_key requests
var now = DateTimeOffset.UtcNow;
var expiredNodeIds = new List<string>();
foreach (var pair in retainedNodeIds)
if (pair.Value <= now)
expiredNodeIds.Add(pair.Key);
foreach (var expired in expiredNodeIds)
retainedNodeIds.Remove(expired);
}
private void DrainActions(string? nodeId)
{
foreach (GuaActionType action in Enum.GetValues(typeof(GuaActionType)))
{
while (runtime.TryConsumeAction(action, nodeId, out var request))
{
if (request.Action == GuaActionType.Click)
{
var error = TryClick(request.NodeId);
runtime.EmitActionResult(request,
error == GuaActionError.None, error);
}
else
{
runtime.EmitActionResult(request, false, GuaActionError.Unsupported);
}
}
}
}
private GuaActionError TryClick(string? nodeId)
{
if (nodeId is null || !ui.TryGetControl(nodeId, out var control))
return GuaActionError.NodeNotFound;
if (!control.Visible)
return GuaActionError.Hidden;
if (!control.Enabled)
return GuaActionError.Disabled;
return ui.TryClick(nodeId)
? GuaActionError.None
: GuaActionError.Unsupported;
}
public void Dispose() => runtime.Dispose();
}GetBridgePort honors the port allocated by an external test host through GUA_BRIDGE_PORT, validates its range, and falls back to 8765 only when no override is supplied. ICustomUi is a placeholder for the host-specific API. Its Sensitive flag must identify passwords, tokens, and similar controls before registration; omit both Text and Value so secrets never enter snapshots, Inspector/MCP responses, or retained diagnostics. Keep IDs stable across frames and express bounds as physical viewport pixels with a top-left origin.
Complete actions correctly
TryConsumeAction matches both action type and node ID. A request based on the preceding snapshot may arrive after the first frame where its control is absent, so one drain pass is not enough. Retain each last-seen ID for a bounded staleness window, continue draining it during that window, then remove it. Configure StaleIdRetention to be at least the longest remote action timeout plus transport margin; the example uses 30 seconds. This bounds dynamic-screen memory and per-frame work by the recent ID rate without abandoning callers that are still allowed to submit. Drain the empty ID separately for global key requests.
| Host outcome | Error | Example |
|---|---|---|
| Target disappeared | NodeNotFound | Scene changed before processing |
| Hidden or disabled | Hidden / Disabled | Host state changed after the snapshot |
| Unsupported operation | Unsupported | Clicking a read-only text node |
| Invalid host value | InvalidValue | Non-numeric slider value |
Complete screenshot requests
if (runtime.TryConsumeScreenshotRequest(out var request))
{
if (ui.IsHeadless)
runtime.CompleteScreenshot(request, GuaScreenshotAvailability.Headless);
else if (ui.IsRenderingDisabled)
runtime.CompleteScreenshot(request, GuaScreenshotAvailability.RenderingDisabled);
else
{
var png = ui.CapturePngAfterDraw();
var dataUri = "data:image/png;base64," + Convert.ToBase64String(png.Bytes);
runtime.CompleteScreenshot(request, GuaScreenshotAvailability.Available,
dataUri, png.Width, png.Height);
}
}Check headless and rendering-disabled states separately before attempting PNG capture. CompleteScreenshot is the documented completion API. Call it once for every consumed request. If timeout, cancellation, or context reset already invalidated the request, the runtime ignores the late completion; do not retry it.
Adapter checklist
- Publish a stable lowercase adapter name and version.
- Keep IDs, roles, parents, states, and supported actions consistent.
- Use the host's real input/listener path and always emit success or failure.
- Never copy sensitive values into logs, diagnostics, or completion events.
- Distinguish headless, rendering-disabled, and stale screenshot outcomes.
- Dispose the runtime to release the bridge and native handle.