GGua 日本語リファレンスEnglishGitHub ↗
英語版が正式な参照です。内容に差異がある場合は、英語版を優先してください。
DOCS / GUA.RUNTIME

Gua.Runtime実装ガイド

独自のmanaged engine adapterからSemantic UI Tree、操作、screenshot、Inspector bridgeをGuaへ接続するための実践ガイドです。

実際の利用場面

場面Gua.Runtimeが担当することadapter側が担当すること
新しいgame engineへ対応C ABIのmanaged wrapper、request queue、version、bridgeengineのUI列挙、座標・状態変換、入力配送
独自UI frameworkへ対応Semantic node登録、action要求、完了eventWidgetをroleへ写像し、実際のWidget APIを呼ぶ
社内toolkitをInspector/MCPへ公開WebSocket bridge、UI Tree JSON、log、screenshot要求toolkitの描画loopとcapture APIへ接続
Inspector / MCP / Tests観測・操作requestnative gua_runtimestate・queue・bridgeGua.Runtimemanaged APICustom adapterengine固有変換Game UI実際のWidget
図1: Gua.Runtimeはprotocol stateを所有するnative runtimeと、engine固有UI APIの間にあるmanaged境界です。

導入とnative library

最新安定版をadapter projectへ追加powershell
dotnet add package Gua.Runtime

Gua.Runtimenet10.0netstandard2.1を対象にします。実行時には対象platform/architectureと一致するgua.dllgua_runtime.dllも配置します。不一致や欠落はconstructorで明示的なload errorになります。

adapterの1 frame

BeginFramescreen名を開始RegisterNode × N現在のUI状態EndFramesnapshot確定Consume actionsengineへ配送Emit resultrequestIdで完了
図2: nodeは差分ではなく、そのframeで観測した状態を登録します。操作はsnapshot確定後に消費し、hostで処理した結果を返します。
最小adapter loopcsharp
using System;
using System.Collections.Generic;
using Gua.Core;
using Gua.Runtime;

public sealed class CustomUiGuaAdapter(ICustomUi ui) : IDisposable
{
    // 最長の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は外部test hostがGUA_BRIDGE_PORTで割り当てたportを優先し、範囲を検証します。overrideがない場合だけ8765を使います。ICustomUiは説明用のhost固有interfaceです。password、token等を登録前にSensitiveで識別し、TextValueの両方を省略することで、secretがsnapshot、Inspector/MCP応答、retained diagnosticsへ入るのを防ぎます。IDはframeをまたいで安定させ、boundsは物理viewport pixel・左上原点で渡します。

actionを正しく完了させる

TryConsumeActionはaction typeとnode IDの両方が一致する要求を取り出します。直前snapshotを見たclientのrequestは、Controlが消えた最初のframeでdrainした後に到着する可能性があるため、一度のdrainだけでは不十分です。最後に観測した各IDを有限のstaleness windowだけ保持し、その間はdrainを継続して期限後に除去します。StaleIdRetentionは最長のremote action timeoutとtransportの余裕以上に設定します。この例は30秒です。これにより許可されたclientを途中で見捨てず、動的画面のmemoryとframe処理量を直近のID生成rateへ制限できます。空IDはglobal key要求用に別途drainします。

結果返すerror
対象が消えたNodeNotFoundrequest後、処理前にsceneが変わった
非表示・無効Hidden / Disabledhost状態が最新snapshotから変化した
roleが操作非対応Unsupportedtext nodeへのclick
値を解釈できないInvalidValuesliderへ数値以外を設定

screenshot要求

描画後にscreenshotを完了csharp
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);
    }
}

PNG captureを試す前に、headlessとrendering disabledを別々に判定します。captureは可能なら描画完了後に行います。CompleteScreenshotがdocumented completion APIです。consumeしたrequestごとに一度だけ呼びます。timeout・cancel・context reset後のlate completionはruntimeが無視するため、再試行しません。

実装チェックリスト

  • SetAdapterVersionへlowercase ASCIIの安定したadapter名とversionを設定する。
  • node ID、role、parent、state、action対応をframe間で一貫させる。
  • 外部actionをengineの通常input/listener経路へ配送し、処理後に必ず成功または失敗を返す。
  • 秘密値をlog、diagnostics、完了eventへ書かない。
  • headless・rendering disabled・stale sessionをscreenshot結果として区別する。
  • 終了時にDispose()し、bridgeとnative handleを解放する。