# Plan: iClone 8 Python API bridge — control iClone from the terminal **Status:** ready for implementation · **Author:** Fable 5 session 2026-07-21 · **Implementer:** GLM session ## 0. Context (you have no other context — read this fully) You are on Jeremy's **Windows 11 PC**. This repo (`C:\Users\Jeremy\tinqs\animation`) is the animation bridge for the ariki-game project: animations are authored in iClone 8, exported as FBX into `exchange/`, and retargeted with `tools/cc_retarget.py`. Today that pipeline is entirely manual on the iClone side (Jeremy clicks Export in the UI). **Goal:** establish a programmatic connection to iClone 8 so terminal agents (Claude, GLM, scripts) can drive it — load motions, query the scene, trigger FBX exports — without Jeremy clicking through the UI each time. **Why a bridge is needed:** iClone 8 has an embedded Python API (`RLPy` module) but **no external/network API of any kind**. Scripts only run *inside* iClone, loaded either from the Script menu or as auto-loading plugins. The standard pattern (used by MotionLIVE etc.) is a plugin that runs a localhost socket server inside iClone and executes commands sent to it. That is what you will build. ### Verified environment facts (checked 2026-07-21 — trust these) - iClone 8 install: `A:\Program Files (x86)\iClone 8\` (note: **A: drive**, not C:). - Plugin auto-load folder: `A:\Program Files (x86)\iClone 8\Bin64\OpenPlugin\` — **verified writable without elevation**. Currently contains only stock plugins (AIStudio, MotionLIVE, VideoMocap). Do not touch those. - API stub for reference: `A:\Program Files (x86)\iClone 8\Bin64\RLPy.py` — this file lists every RLPy class/method with docstrings. **It is your API ground truth**; grep it before using any RLPy call. Do not trust API names from memory or old forum posts. - Embedded Python: `Bin64` contains BOTH `python38.dll` and `python310.dll` — do not assume which one hosts plugins. Write the plugin in **3.8-compatible syntax** (no `match`, no `X | Y` unions, no 3.9+ stdlib) and report the real version via the bridge itself (`sys.version`) in your results. - Qt: iClone embeds Qt 5.15 / PySide2 (`shiboken2` available in-process). - Client-side Python: 3.12.10 on PATH (`python`). Client must be **stdlib-only**. - iClone was RUNNING when this plan was written. A restart is required to load the new plugin — **Jeremy must do the restart himself** (he may have unsaved work). Never kill the iClone process. ### Reference docs (fetch only if stuck; the RLPy.py stub usually suffices) - Plugin structure: https://wiki.reallusion.com/IC_Python_API:Your_First_iClone_Python_Plugin - IC8 API wiki: https://wiki.reallusion.com/IC8_Python_API - Official samples (QTimer/menu/PySide2 patterns): https://github.com/reallusion/iClone ## 1. Architecture ``` ┌─ this repo ──────────────────┐ ┌─ inside iClone 8 (Qt main thread) ─────────┐ │ tools/iclone_bridge.py │ TCP │ OpenPlugin\TinqsBridge\main.py │ │ (client, py3.12, stdlib) │◄──────►│ socket thread: accept + read/write ONLY │ │ │ 127.0. │ queue → QTimer(50ms, main thread) drains, │ │ tools/iclone_bridge/ │ 0.1: │ exec()s code with RLPy in namespace, │ │ TinqsBridge/main.py (source)│ 18800 │ posts JSON reply back to socket thread │ │ tools/install_iclone_bridge │ │ │ │ .ps1 (copies to OpenPlugin) │ └────────────────────────────────────────────┘ └──────────────────────────────┘ ``` **Non-negotiable threading rule:** RLPy is not thread-safe and must only be called on iClone's Qt main thread. The socket thread NEVER touches RLPy. It pushes requests onto a `queue.Queue`; a PySide2 `QTimer` created in `initialize_plugin()` (which runs on the main thread) fires every ~50 ms, drains the queue, executes, and hands the response back (a second queue or per-request `threading.Event` — your choice). ## 2. Wire protocol (keep it this simple) Newline-delimited JSON over TCP, localhost only, one request/response pair at a time. - Request: `{"id": 1, "code": ""}` - Success: `{"id": 1, "ok": true, "result": , "stdout": ""}` - Failure: `{"id": 1, "ok": false, "error": "", "stdout": "..."}` Execution semantics: - `exec(code, ns, ns)` — one **persistent** namespace dict `ns` shared across all requests for the life of the iClone session, pre-seeded with `RLPy` imported. Same dict as globals AND locals (avoids the exec-scoping trap where nested functions can't see top-level names). - If the code sets a variable named `result`, that is the response `result` (JSON-serialize; on `TypeError` fall back to `repr()`). Otherwise `result` is null. Clear `ns["result"]` before each exec so stale values don't leak. - Capture stdout/stderr during exec (`contextlib.redirect_stdout/stderr`) into `stdout`. - Any exception → `ok:false` with `traceback.format_exc()`; the bridge itself must survive and serve the next request. ## 3. Deliverables 1. **`tools/iclone_bridge/TinqsBridge/main.py`** — the plugin (source of truth lives in the repo; the installer copies it out). Requirements: - `initialize_plugin()` (mandatory — iClone refuses to load the plugin without it). Wrap its entire body in try/except that writes `traceback.format_exc()` to the log file — plugin load failures are otherwise near-silent. - TCP server on `127.0.0.1:18800`, `SO_REUSEADDR`, port overridable via env var `TINQS_ICLONE_BRIDGE_PORT` read inside iClone. If the bind fails, log it clearly and return without crashing the plugin loader. - Socket-accept loop on a `daemon=True` thread; QTimer executor per §1. Keep module-level references to the timer and thread (a GC'd QTimer silently stops). - Log to `%TEMP%\tinqs_iclone_bridge.log` (append, timestamped lines: startup, bind result, each request id + ok/error, shutdown). - Optional nice-to-have, skip if it costs you more than ~20 min: a **Plugins ▸ Tinqs Bridge** menu entry showing status (port, requests served) via `RLPy.RUi.AddMenu` + `shiboken2.wrapInstance` (see the GitHub samples). The log file is the required status surface; the menu is garnish. 2. **`tools/iclone_bridge.py`** — client, Python 3.12, stdlib only. API: `run(code, port=18800, timeout=30.0) -> dict` and `ping(port, timeout) -> dict`. CLI (match the repo's argparse style): ``` python tools/iclone_bridge.py --ping python tools/iclone_bridge.py --exec "result = 1 + 1" python tools/iclone_bridge.py --file some_script.py python tools/iclone_bridge.py --port 18801 --timeout 120 --exec "..." ``` `--ping` sends a snippet returning `{"product": ..., "version": ..., "python": sys.version}` — find the real product/version getters by grepping `RLPy.py` (look at `RApplication` and `RGlobal`; do not guess). Exit 0 on ok:true, 1 on ok:false or connection failure, with a readable message either way (mention "is iClone running? was it restarted after install?" on connection-refused). 3. **`tools/install_iclone_bridge.ps1`** — copies `tools/iclone_bridge/TinqsBridge/` → `A:\Program Files (x86)\iClone 8\Bin64\OpenPlugin\TinqsBridge\` (overwrite), prints what it did and reminds that iClone must be restarted. Idempotent. 4. **`docs/iclone-bridge.md`** — short usage doc: what it is, install/update procedure, CLI examples, the `result` convention, the persistent-namespace behavior, the log file location, the threading rule for anyone extending the plugin, and the "long-running code freezes the iClone UI" warning. 5. **Results report `plans/iclone-api-bridge-results-2026-07-21.md`**: actual embedded Python version discovered, RLPy calls used for ping, smoke-test transcript (§5), any deviations from this plan with reasons, and known limitations. ## 4. Implementation order 1. Grep `A:\Program Files (x86)\iClone 8\Bin64\RLPy.py` for the calls you need (`RApplication`, `RGlobal`, `RScene`, `RFileIO`) and note exact signatures. 2. Write plugin + client + installer. 3. Run the installer. Verify the files landed in OpenPlugin. 4. **Stop and ask Jeremy to save his iClone work and restart iClone.** Do not proceed to smoke tests until he confirms. Never kill or restart the process yourself. 5. Run smoke tests (§5). If the plugin didn't load: check the log file first, then iClone's **Script ▸ Console Log** window (ask Jeremy to read it out if needed). 6. Write docs + results report. ## 5. Acceptance criteria (all must pass, transcript goes in the results report) 1. `python tools/iclone_bridge.py --ping` → ok, prints product name, iClone version, and embedded Python version. 2. Scene query: `--exec` a snippet listing scene object names via `RScene` (e.g. avatars + props; exact API from the stub) returns a JSON list without error. 3. Namespace persistence: `--exec "x = 41"` then `--exec "result = x + 1"` → `42`. 4. Error resilience: `--exec "1/0"` → `ok:false` with a ZeroDivisionError traceback, AND a follow-up `--ping` still succeeds. 5. stdout capture: `--exec "print('hi'); result = True"` → `stdout` contains `hi`. 6. Bridge survives ≥ 2 sequential client connections (each CLI call is a new connection). ## 6. Guardrails — do NOT - Do not kill, restart, or send input to the running iClone process. Restarts are Jeremy's job (step 4.4). - Do not modify anything in `OpenPlugin\` other than creating/updating `TinqsBridge\`. - Do not modify existing repo tools (`cc_retarget.py`, `loop_qc.py`, `loop_fix.py`, `dance_profile.py`, etc.), anything in `.claude/`, `docs/` (except the new `docs/iclone-bridge.md`), or `exchange/`. - Do not bind to anything other than `127.0.0.1`. This is an arbitrary-code-execution endpoint by design; localhost-only is the security model. - Do not install any packages — plugin uses iClone's embedded stdlib + PySide2; client uses Python 3.12 stdlib. - Do not commit or push. Leave everything in the working tree for Jeremy's review. ## 7. Known traps - **RLPy off the main thread** crashes or corrupts iClone. The QTimer pattern in §1 is mandatory; the socket thread only moves bytes. - **QTimer lifetime:** keep a module-level reference; also create it *in* `initialize_plugin()` (main thread), never in the socket thread. - `initialize_plugin()` missing, misnamed, or raising → plugin silently fails to load. Hence the try/except-to-logfile around everything. - **exec scoping:** pass the same dict as globals and locals, or nested functions/comprehensions in user code will throw NameError on top-level names. - Commands run synchronously on the Qt main thread → **iClone's UI freezes for the duration**. Fine for seconds-long operations (an FBX export blocks anyway); document it. - JSON can't serialize RLPy objects — always try/except the dumps and fall back to repr. - Stale port from a previous iClone instance: `SO_REUSEADDR` + a clear log line beats a cryptic bind exception. - Windows Firewall shouldn't prompt for a 127.0.0.1 bind, but if a prompt appears Jeremy should allow it (note in docs). - Socket recv: requests may arrive fragmented — read until `\n`, decode UTF-8. - Both python38.dll and python310.dll exist in Bin64 — write for 3.8, verify via ping. - Old wiki pages mix iClone 7 and 8 APIs; several IC7 calls were renamed/removed. The local `RLPy.py` stub outranks every web source. - After the bridge is up once, you can iterate on *executor* behavior by exec-ing new code through the bridge itself (hot-swap) — but changes to `main.py` on disk still need an iClone restart to load. Batch your plugin edits to minimize restart requests.