GGua REFERENCE日本語GitHub ↗
DOCS / GUA.RUNTIME

Gua.Runtime implementation guide

Connect a managed engine adapter to Gua semantic frames, actions, screenshots, and the Inspector bridge.

Concrete use cases

SituationGua.Runtime providesYour adapter provides
Support another game engineManaged C ABI wrapper, request queues, versions, bridgeUI enumeration, state conversion, and input delivery
Support a custom UI frameworkSemantic node registration and correlated action resultsWidget-to-role mapping and real widget operations
Expose an internal toolkit to Inspector/MCPWebSocket bridge, UI-tree JSON, logs, screenshot requestsRendering-loop and capture integration
Inspector / MCP / Testsobserve and requestnative gua_runtimestate, queues, bridgeGua.Runtimemanaged APICustom adapterengine translationGame UIreal widgets
Figure 1: Gua.Runtime is the managed boundary between the protocol runtime and engine-specific UI APIs.

Install and deploy

Add the latest stable package to an adapter projectpowershell
dotnet add package Gua.Runtime

The 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

BeginFramelogical screenRegisterNode × Ncurrent UI stateEndFramecommit snapshotConsume actionsdeliver to engineEmit resultcorrelate requestId
Figure 2: Publish observed state each frame, then consume requests and report what the host actually did.
Minimal adapter loopcsharp
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 outcomeErrorExample
Target disappearedNodeNotFoundScene changed before processing
Hidden or disabledHidden / DisabledHost state changed after the snapshot
Unsupported operationUnsupportedClicking a read-only text node
Invalid host valueInvalidValueNon-numeric slider value

Complete screenshot requests

Capture after renderingcsharp
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.