Skip to content
cccskills
FreePublished on GitHub

krkrz-webui

吉里吉里Z (krkrz) の -replweb HTTP+SSE サーバ (WebServer クラス) にブラウザ UI を載せて、本体アプリの操作/編集/観測パネルをブラウザ側に組み込む方法論。

ゲーム本体は 3D 表示やゲーム内 UI に専念させ、編集ツール・インスペクター・ダッシュボード・REPL コンソールをブラウザ (別ウィンドウ/別PC) から使う構成を作るときに読む。

プラグインや TJS がサーバへエンドポイントを追加公開する手順 (WebServer.register / serveStatic / broadcast)、ハンドラ呼び出し規約 (req %[method,path,query,body,bytes] → 文字列/octet/整数/辞書)、状態同期パターン (fetch POST + SSE /sub push + throttle + 差分/tick 配信)、既存 REPL コンソール (/events + /cmd) の UI 埋め込み、ブラウザのアプリモード起動 (Chromium --app / -webui)、json.dll 連携、TJS2 由来のハマりどころ (ローカル関数クロージャ不在→クラス化 / ブロックコメントのネスト誤爆 / startup 例外の致命性 / ハンドラは必ずメインスレッド実行) を網羅。

エンジン側 WebServer クラスそのものの仕様は krkrz core doc/REPL.md、REPL/Agent 駆動は skill krkrz-repl、Elements ネイティブ UI は skill elements、TJS2 言語は skill tjs2、本体 API は skill krkrz を参照。

The English summary is being prepared. Showing the original description.

See how to install

Files included (1)

  • SKILL.md20.6 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

パスの基点: 本文の相対パスは engine ルート基準 (krkrz_dev では src/core/ を前置。下記参照)。作業ディレクトリが krkrz_dev 以外 (krkrz_android / krkrz_ios などの外枠や案件フォルダ) のときは ${KRKRZ_BASE}/krkrz_dev/ を前置して読む (echo $KRKRZ_BASE で実パスを確認。マシンごとに値が違うので絶対パスは書き込まない)。

krkrz replweb ベース ブラウザ UI 組み込み方法論

krkrz エンジンの -replweb HTTP+SSE サーバ (KRKRZ_REPL_WEB ビルド) に乗せて、 本体アプリの操作・編集・観測 UI をブラウザ側に組み込むための方法論。 「ゲーム本体 = 表示 + ゲーム内 UI、ブラウザ = 編集ツール + インスペクター + REPL」 という分離を作れる。Elements ネイティブ UI (skill elements) の制約 (レイアウト/ 入力/コピペ/フォント) から解放され、UI 反復にエンジン再ビルドが要らないのが利点。

エンジン側 WebServer クラスそのものの仕様は krkrz core doc/REPL.md の 「ブラウザ REPL / Web サーバ」節が SSOT。本スキルはその上に UI を構築する側の 設計パターンをまとめる。

本文のエンジンパスは engine ルート相対 (doc/..., common/..., data/...)。 本スキルが置かれている umbrella (krkrz_dev) では src/core/ を前置して読む (例: src/core/doc/REPL.md)。engine 単独チェックアウトならそのまま。

いつ使うか / 使わないか

  • 使う: VRM/3D エディタ、ステージ/シーン編集、パラメータ調整パネル、ライブ インスペクター/ダッシュボード、リモート (別PC) 編集、REPL を UI に同居させたい。
  • 使わない: ゲーム内で完結する常駐 UI・モーダルダイアログ (→ skill elements)。 ゲームの一部として配布する対話 UI は Elements、開発/編集ツールはブラウザ、が目安。

前提

  • engine が KRKRZ_REPL_WEB ビルドであること (WebServer クラスが登録される)。
  • サーバ起動は WebServer.start([port]) をスクリプトから呼ぶのが基本 (既定 8899)。 -replweb オプションは不要 = 利用者に毎回付けさせなくてよい (アプリの startup で if (!WebServer.active) WebServer.start(8899);)。-replweb[=port] を明示した場合は 本体が先に起動するので、WebServer.start は二重起動を避けて no-op になる。 0.0.0.0:<port> で LAN 越し可 (WebServer.startAt("0.0.0.0", port)。信頼ネットワーク 限定・起動ログに警告)。待受 URL は WebServer.url / System.replWebURL。
  • JSON I/O を使うなら json.dll (Scripts.toJSONString / evalJSON)。下記参照。
  • 検証には krkrz-repl (-replfile チャネル / /cmd) と curl が便利。

4 層アーキテクチャ

層実体役割
① サーバengine common/utils/ReplWebServer.cpp + TJS クラス WebServer (ReplWebIntf.cpp)HTTP+SSE。ルーティング・静的配信・SSE チャネル。ハンドラは常にメインスレッド実行
② ハンドラネイティブ (プラグイン) or TJS/api/… や /app/… を処理。重い/型変換が要る処理はネイティブ、細かい操作は TJS
③ アプリブリッジTJS (アプリ側スクリプト)アプリのモデル層 (状態取得/操作関数) を HTTP ルートへ橋渡し + 静的配信登録 + SSE 配信
④ ブラウザ UIdata/ui/** の静的 HTML/JS単一ページ (ビルドレス vanilla JS で十分)。fetch POST + SSE 購読

最小手順

1. 静的 UI を配信する

// アプリ起動後 (startup.tjs 等) に一度
if (typeof global.WebServer != "undefined") {
    WebServer.serveStatic("/ui/", "ui/");   // data/ui/** を /ui/** で配信 (json.dll 不要)
}

data/ui/index.html を置けば http://127.0.0.1:8899/ui/ で開ける。 静的配信は Storages 経由 (xp3 内でも可)。.. はトラバーサル拒否 (403)。

2. 操作エンドポイントを登録する

// TJS2 はローカル関数のレキシカルクロージャを持たない (下記ハマりどころ)。
// ハンドラはクラスのメンバにして objthis 経由で相互参照させる。
class AppBridge {
    function AppBridge() {}
    function handler(req) {
        // req = %[ method, path, query, body(文字列), bytes(octet: body 非空時) ]
        if (req.path == "/app/state") return Scripts.toJSONString(buildState());
        var arg = (req.body != "") ? Scripts.evalJSON(req.body) : %[];
        if (req.path == "/app/do") { app.doSomething(arg.x); push(); return "{\"ok\":true}"; }
        return %[ "status" => 404, "body" => "{}" ];
    }
    function buildState() { return %[ /* アプリ状態 */ ]; }
    function push() { WebServer.broadcast("state", Scripts.toJSONString(buildState())); }
}
global.bridge = new AppBridge();
WebServer.register("/app/", bridge.handler);   // プレフィックス最長一致

3. ブラウザから叩く (index.html 内)

// 取得
const st = await (await fetch('/app/state')).json();
// 操作
await fetch('/app/do', {method:'POST', body: JSON.stringify({x:1})});
// 変更購読 (サーバ push)
const es = new EventSource('/sub/state');
es.onmessage = e => render(JSON.parse(e.data));

WebServer API 早見表

メンバ用途
WebServer.register(prefix, handler)動的ハンドラ登録 (最長一致・上書き)。サーバ未起動でも保持され起動時から有効
WebServer.unregister(prefix)解除
WebServer.serveStatic(prefix, storageDir)Storages 経由の静的配信。例 ("/ui/","ui/")
WebServer.unserveStatic(prefix)静的マウント解除
WebServer.broadcast(channel, text)/sub/<channel> 購読者へ SSE 配信 (改行可)
WebServer.registerPanel(id, label, path)組み込み UI へタブを 1 枚足す (下記)
WebServer.unregisterPanel(id)パネルを外す
WebServer.start([port]) / startAt(host, port)サーバをスクリプトから起動 (-replweb 不要)。既に稼働中なら no-op。戻り値=稼働中か
WebServer.stop()サーバ停止
WebServer.openBrowser([url [, appMode=true]])url をブラウザで開く。appMode 時 Edge/Chrome を --app で試し不可なら既定ブラウザへ。url 省略で稼働中 URL。SDL 版でもアプリモード可
WebServer.active / WebServer.url稼働中か / 待受 URL

組み込みルート: GET / = 埋め込み UI (Console / Watch のタブ) / GET /events = ログ SSE / POST /cmd = TJS 評価 / GET /sub/<ch> = 汎用 SSE / GET|POST /watch + /sub/watch = 監視式 / POST /pad/exec + GET|POST /pad/file = Pad / GET|POST /state + /sub/state = コントローラ / POST /bye (下記)。 組み込みルートは WebServer.register より先に判定されるので、これらのパスは 自前ハンドラで上書きできない (別名を使う)。

案件パネル (registerPanel) — 組み込み UI にタブを足す

自前 UI を丸ごと立てる前にこれを検討する。 serveStatic だけで別ページを 作ると Console / Watch / Pad とコントローラを失う。 パネルとして足せば同居できる。

WebServer.serveStatic("/tool/", "web/");
WebServer.registerPanel("mytool", "案件ツール", "/tool/tool.html");
  • 中身は iframe。組み込みページへスクリプトを注入する方式ではないので、 組み込み UI の内部 DOM に依存しない (向こうが変わっても壊れない)
  • 同一オリジンなのでパネルから /cmd /watch /pad/exec や自前 register エンドポイントを普通に叩ける
  • 同じ id で上書き。登録/解除は /sub/panels で開いているページへ即反映
  • 中身はタブを初めて開いたときに読む (重いツールでも起動は遅くならない)
  • path は / 始まりのサーバ上パス。不正なら登録されずログに理由が出る (⚠ msys2 の bash から curl で叩くと /tool/... が Windows パスへ 自動変換されて弾かれる。ファイル body か PowerShell を使う)

ブラウザ UI とアプリの寿命をそろえる (両方向)

ブラウザを UI にする構成では片方だけ残るのがいちばん困る。engine 側で 両方向を閉じてある。

本体 → ブラウザが居なくなったら終了 (-replwebidle)

  • 既定 5 秒で有効。-replwebidle=no で無効、=<秒> で変更
  • 一度でも SSE 購読が来てから武装する。だから購読を張らない エージェント駆動 / API 面利用は落ちない。自前 UI は EventSource を 1 本張っておけばそれが «ブラウザが居る» 印になる
  • 複数タブは購読数で扱うので最後の 1 枚まで落ちない
  • ページは pagehide/beforeunload で navigator.sendBeacon('/bye') を投げると 猶予が ~2 秒へ前倒しされる (自前 UI でも同じことをすると閉じが速くなる)。 投げなくても <秒> で畳まれる

ブラウザ → 本体が居なくなったら閉じる

  • /sub/state に {"exiting":true} が来たら即 window.close() (engine が Stop() で流す)
  • SSE が切れて 5 秒復帰しなければ close (クラッシュ / 強制終了)
  • close が効かない通常タブは全面オーバーレイへフォールバック
  • 自前 UI もこの 2 つを実装しておくと «本体だけ終わってページが残る» を防げる

ブラウザの自動オープン (-replwebopen)

既定は「ループバック束縛 かつ コンソール無し (GUI 起動)」のときだけ app モード。 端末から起動すると開かないので、開かせたいときは -replwebopen=app (tab = 通常ウィンドウ / no = 抑止)。TJS からは WebServer.openBrowser。

⚠ engine の app モードは msedge.exe --app=<url> を ユーザの通常プロファイル で起動する (プロファイル指定なし)。検証で --user-data-dir=<使い捨て> を 自分で渡すと Edge のオンボーディング (同期を促すダイアログ) がユーザの画面に 出るので、捨てプロファイルを使うなら --disable-sync --no-first-run --no-default-browser-check を添え、終わったら必ずウィンドウごと片付ける。

監視式 API (/watch)### Pad API (/pad/*) — 複数行スクリプトの実行と保存

  • POST /pad/exec — body をまるごと 1 回実行 (/cmd の 1 行実行と違い、 関数定義やループをそのまま流せる)。{"ok","result","error"}
  • GET /pad/file?path= — ストレージから読む (text/plain)
  • POST /pad/file?path= — ストレージへ書く。既定は 403。 本体を -replwebpad=<dir> で起動したときだけ、その接頭辞の配下へ書ける (⚠ セキュリティ境界ではなく «[保存] のうっかり» を防ぐ柵。/cmd で任意 TJS が 実行できる時点で全権限は開いている)

監視式 API (/watch) — 状態を張り込んで観測する

エンジン組み込み。式のリストを保持して、まとめて評価して返す。自前の インスペクターを書く前に、これで足りないか見ると早い。

ルート説明
GET /watch一覧+現在値 (JSON)。評価しないのでポーリング安全。?eval=1 で評価してから返す
POST /watchform-urlencoded。op=add&expr=… / op=rm&id=… / op=edit&id=…&expr=… / op=clear / op=interval&ms=… / op=eval。成功なら GET と同じ JSON
GET /sub/watch自動更新の push。値が変わったときだけ流れる (定数式を張っても無駄な配信は出ない)
await fetch('/watch', {method:'POST', body:new URLSearchParams({op:'add', expr:'System.getTickCount()'})});
await fetch('/watch', {method:'POST', body:new URLSearchParams({op:'interval', ms:'500'})});
new EventSource('/sub/watch').onmessage = e => render(JSON.parse(e.data));

payload = {"interval":500,"entries":[{"id":1,"expr":"…","value":"…","error":false}]}。 interval は -1=off / 0=毎フレーム / 正値=ms (下限 100ms)。式が例外を投げても value が "(error) …" になるだけで一覧は返る。

ハンドラ呼び出し規約

  • 引数 req = %[ method, path, query, body, bytes ] (bytes は body 非空時のみ octet)。
  • 戻り値の解釈:
    • 文字列 → 200 application/json
    • octet → 200 application/octet-stream
    • 整数 → そのステータスで空ボディ
    • void → 204
    • 辞書 %[status, mime, body(文字列 or octet)] → 明示指定
  • ハンドラ内の TJS 例外 → 500 (本文=メッセージ) + WebServer: handler error: ログ。
  • ハンドラは必ずメインスレッドで呼ばれる (ReplMainQueue タスク)。GL/TJS/エンジン API を自由に触ってよい。長い処理はフレームを止めるので注意 (Elements 版と同じ制約)。

状態同期パターン (実践)

  • client → server: fetch POST (body=JSON)。スライダ等の連続入力は throttle (例 50ms、キー単位で最新値のみ送る)。連続反映系は push() しない (操作元が真実。SSE 逆流で入力が上書きされるのを防ぐ)。
  • server → client: SSE /sub/state。
    • 各操作ルートの直後に push() (辞書一発)。
    • フレームループから ~4Hz で差分検知 push (前回 JSON と一致なら送らない)。 これで REPL 等 UI 外からの変更もブラウザに自動反映される。
  • エディタ DOM の再構築を壊さない: 高頻度で変わる観測データ (fps/ログ等) と 編集状態を分け、編集状態が変わったときだけ DOM を作り直す (観測は別 DOM を差分更新)。 スライダ操作中フラグを持ち、操作中は SSE 由来の再描画を抑止する。

REPL コンソールを UI に同居させる

エンジン組み込みの /events (ログ SSE、バックログ2000行・レベル別 cls 付き) と /cmd (POST body=1行を TJS 評価、応答 body は継続入力中なら "1") をそのまま ブラウザ UI 側で購読・POST するだけ。エンジン改変なしで編集 UI に REPL を統合できる。 履歴 (↑↓) と継続プロンプト切替はブラウザ側で実装する。

ブラウザをアプリモードで開く

Chromium 系の --app=<URL> でタブ/アドレスバー無しウィンドウになる。 本体の WebServer.openBrowser(url) を使うのが基本 (Edge → Chrome を --app で試し、 不可なら既定ブラウザへフォールバック)。エンジンが「URL を開く」(TVPShellExecute= SDL は SDL_OpenURL) と「プログラムを引数付きで実行」(TVPExecuteProgram= デスクトップ Windows は Win32 ShellExecute) を分けて実装しているので、SDL 版でもアプリモードで 開ける (System.shellExecute 直呼びは SDL で --app 引数が捨てられるため不可だった)。

WebServer.openBrowser(("" + WebServer.url) + "ui/");   // url 省略で稼働中サーバ URL

WebServer.start() を先に呼んでいればサーバは同期的に稼働 (WebServer.active=true) なので、 開くのを Timer で待つ必要はない (起動直後にそのまま openBrowser してよい)。 旧エンジン (openBrowser 未実装) 向けにフォールバックを書くなら typeof WebServer.openBrowser != "undefined" で分岐し、無い場合のみ従来の System.shellExecute("msedge.exe", "--app=" + url) 列を使う。

json.dll

/app/ の JSON I/O には json.dll (Scripts.toJSONString(v,indent) / evalJSON(str) / evalJSONStorage(path,utf8) / saveJSON(path,v,utf8,indent)) が要る。 exe と同世代のものを plugin フォルダへ。ビューア/最小起動では未リンクのことがあるので、 ブリッジ側で自力 try { Plugins.link("json.dll"); } catch(e){} を試み、不可なら 静的配信 (/ui/) だけ登録して操作系を無効化するとフォールバックが綺麗。

★ハマりどころ (TJS2/krkrz 由来)

  1. TJS2 にローカル関数のレキシカルクロージャは無い: IIFE 内の関数同士の相互参照は 実行時「メンバが見つかりません」。→ ハンドラ/ヘルパはクラスのメンバにして objthis (インスタンス) 経由で解決する。WebServer.register("/x/", obj.method) で渡すと呼び出し時 objthis がそのインスタンスになる。
  2. TJS2 のブロックコメントはネスト可能: コメント文中の /ui/ + * のような並びが /* と誤認され「コメントが終わらないまま終端に達した」。→ ブリッジ/ハンドラを書く .tjs は行コメント (//) のみにすると安全。
  3. startup スクリプト中の例外は致命: REPL 保護 (TVPReplActive) は TVPCreateREPL() 後に立つため、startup から呼ぶ登録スクリプトのロード失敗は プロセスごと落ちる。登録処理は例外を出さないよう防御的に書く/切り分ける。
  4. ハンドラはメインスレッド実行だが、HTTP スレッドは複数接続を捌く。共有状態は ハンドラ内 (メインスレッド) でのみ触る前提で設計する (engine が SubmitTask で 直列化してくれる)。
  5. 静的配信は json.dll 非依存。UI ページだけ先に出したいときは serveStatic を register より先に無条件で呼ぶ。

検証

  • ハンドラ登録は -replfile チャネル (skill krkrz-repl) か /cmd から eval し、 curl http://127.0.0.1:<port>/<path> で応答確認 (ブラウザ不要で往復テストできる)。
  • SSE は curl -N http://127.0.0.1:<port>/sub/<ch> で購読しつつ別で操作を投げる。
  • 画面反映は Agent.captureScreen(path) → 画像を目視 (skill krkrz-repl)。
  • UI の JS 構文チェックは <script> 部を抜き出して node --check。

リファレンス実装 (krkr_threepp)

VRM 立ち絵/箱庭エディタの完全な実例:

  • engine: common/utils/ReplWebServer.cpp (サーバ) / ReplWebIntf.cpp (TJS クラス) / doc/REPL.md (仕様 SSOT)
  • プラグイン: main.cpp の ThreeWebApi (/api/three/ をネイティブ公開。 リンク時に POST_REGIST から WebServer.register、unlink 時に解除)。UI 向け一括 JSON ダンプ Object3D.treeJson / VRM.expressionListJson / morphListJson (UTF-8→ttstr 変換ヘルパで日本語対応。既定 std::string コンバータは ASCII 限定)
  • アプリ+UI: data/webui.tjs (WebUIBridge, /app/… ブリッジ) + data/ui/index.html (5 モード編集 + サイドバー + REPL コンソール)。設計詳細は threepp/docs/webui.md

関連スキル: krkrz-repl (REPL/Agent 駆動・-replweb 起動) / elements (ネイティブ UI) / krkrz (本体 API: Storages/System/Plugins) / tjs2 (言語仕様) / dev-toolkit:msys2 (シェル)。

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

吉里吉里Z 上の Elements ベース汎用ダイアログ/画面 UI (cycfi/elements + elements_modal) の作り方リファレンス。TJS で JSON / Dictionary 定義のダイアログを作る・出す・イベントを受ける・複数画面フロー(navigator)や常駐メニューを組む・入力/フォーカス/モーダルを制御する・Agent で動作検証する、といった場面で使う。基本的なダイアログ画面の作り方(最小例・レイアウト JSON スキーマ・ウィジェット一覧)から、モーダル/非モーダル/独立ウィンドウ/常駐フロー、複数インスタンス/z-order、DrawDevice 登録タイミング等のハマりどころまで網羅。Win32 ネイティブの WIN32Dialog(win32dialog プラグイン)とは別物。TJS2 言語仕様は skill `tjs2`、本体クラス API は `krkrz`、REPL/Agent 駆動は `krkrz-repl` を参照。

The English summary is being prepared. Showing the original description.

wamsoft/krkrz_dev10Updated Oct 16, 2026

krkrz

Free

吉里吉里Z (kirikiri Z) 本体クラス API のリファレンス。TJS2 で吉里吉里Z 上のスクリプトを書く、レビューする、デバッグするときに使う。Layer / Window / Bitmap / System / Storages / Font / Plugins / Timer / Debug / AsyncTrigger / Scripts / BinaryStream / Matrix32 / Matrix44 / Rect / ImageFunction のコア API、サウンド系 (WaveSoundBuffer / SoundBuffer / VideoOverlay)、DrawDevice (BasicDrawDevice / SDLDrawDevice / OGLDrawDevice / GPUDrawDevice / NullDrawDevice)、OpenGL 描画系 (Canvas / Texture / ShaderProgram / Offscreen / VertexBinder / VertexBuffer)、および主要プラグイン提供クラス (HttpRequest / GdiPlus.* / WIN32Dialog / CSVParser / LineParser / Process / Pad / MenuItem / Unzip / Zip / PSD / SimpleHTTPServer) を網羅。**呼び出されたら必ず「共通パターン」と「クロスカッティング概念」を確認し、必要な詳細クラスは doc/reference/*.md を Read しに行くこと。** TJS2 言語そのものや組み込みクラス (Array / Dictionary / Math 等) は別 skill (`tjs2`) を参照。エンジン内部構造 (C++ 実装、レンダリングパイプライン詳細) は対象外。

The English summary is being prepared. Showing the original description.

wamsoft/krkrz_dev10Updated Oct 16, 2026

吉里吉里Z (krkrz) の SDL3 / WINVER ビルドを REPL 経由でエージェントから駆動するためのリファレンス。krkrz を起動して TJS スクリプトを評価・検証・デバッグする、startup.tjs を介さず明示的に処理を開始する、入力イベント (キー/マウス) を注入する、画面をキャプチャして目視確認する、Elements ダイアログを観測・操作する、例外やダイアログ表示をコンソールで観測する、コアデモ全シーンを自動巡回してキャプチャで表示確認する (-demotest / -demotestcap)、といった場面で使う。**外部エージェントは console(CONIN$) に打てないので -replfile ファイルチャネルが本命**。起動フラグ (-repl / -replfile / -nostartup / -loglevel / -display / -ignoremouse)、ファイルチャネルのプロトコル、Agent API (入力注入 / captureScreen / dialogs / dialogClick)、ドットコマンド (.cap/.dlg/.click/.mem 等)、REPL 駆動時の挙動変更 (例外で即終了しない / inform と例外ダイアログがコンソールに出る) を網羅。TJS2 言語仕様そのものは skill `tjs2`、本体クラス API は skill `krkrz` を参照。

The English summary is being prepared. Showing the original description.

wamsoft/krkrz_dev10Updated Oct 16, 2026

tjs2

Free

TJS2 (吉里吉里Z 内蔵スクリプト言語) の言語仕様と組み込みクラスのリファレンス。.tjs ファイル / *.ks (KAG) 内の埋め込みスクリプト / TJS2 コード断片 を扱う、書く、レビューする、デバッグするときに使う。JavaScript / TypeScript に似ているが文法と意味論が違うので、JS の感覚で書くと壊れる場合が多い。組み込みクラス (Array / Dictionary / Date / Math / RegExp / Exception) の API もここに集約。**呼び出されたら必ず「JS との主な違い」セクションを最初に確認し、その上で必要な詳細リファレンスを Read で取りに行くこと。** 吉里吉里Z 本体のクラス API (Window / Layer / System / Storages / Bitmap 等) や engine 内部はこのスキルの対象外。

The English summary is being prepared. Showing the original description.

wamsoft/krkrz_dev10Updated Oct 16, 2026

See all skills by wamsoft

Report a problem with this skill