Bridge API
Describes the local bridge that connects DousenDesktop, the DCC addons and the Unreal plugin on one workstation, and how a studio tool can register with it, publish events and receive them.
The bridge is an authenticated RPyC service inside DousenDesktop. It listens on 127.0.0.1, port
8766 by default, so only programs on the same machine can reach it. Clients send string-keyed requests
and receive events over a long-lived push stream. The bridge doesn’t use HTTP. The
dousen.bridge.BridgeClient Python class that ships with Desktop handles both. For data on the studio
server, use the DousenCore REST API.
Check the normal workflow first
Before you test your tool, check that a normal publish works on the same workstation. Artists publish from the DCC panels described in Publishing assets.
- Start Desktop, sign in, and select the project. Launch a supported DCC from Desktop, and start Unreal Editor with the DousenUE plugin enabled.
- In the Publish history tab, check that the DCC and Unreal appear under Connected apps.
- Publish one instance from the DCC and wait for Imported and saved in Unreal.
- Run your tool. After it publishes, check Publish history and the asset in Unreal.
Connect and authenticate
BridgeClient() connects to 127.0.0.1 on the port in DOUSEN_BRIDGE_PORT, or
on 8766 if the variable isn’t set. You can pass an address instead, such as
rpyc://127.0.0.1:8766 or a loopback host and port. The client raises an error for any other host.
Every request carries a bridge token, which is separate from the DousenCore bearer token. Each time Desktop starts,
it generates a new 64-character token and saves it in a bridge.token file that only the current user can
read. Desktop deletes the file when the bridge stops.
| System | Token file |
|---|---|
| Windows | %APPDATA%\Dousen\DousenDesktop\bridge.token |
| Linux | ~/.local/share/Dousen/DousenDesktop/bridge.token, or under $XDG_DATA_HOME if it is
set |
| macOS | ~/Library/Application Support/Dousen/DousenDesktop/bridge.token |
The client reads the token again for every request, so your tool keeps working after Desktop restarts with a new
token, unless you set DOUSEN_BRIDGE_TOKEN. Don’t copy the token into scripts or documentation.
| Environment variable | What it does |
|---|---|
DOUSEN_BRIDGE_PORT | The bridge port. The default is 8766. |
DOUSEN_BRIDGE_TOKEN |
The bridge token. If it is set, the client uses it as is instead of reading bridge.token, so it
stops working when Desktop restarts with a new token. |
DOUSEN_PYTHON_PATH |
Set for the tools that Desktop launches. It is the folder to add to sys.path for the
dousen package. |
DOUSEN_RPYC_PYTHON_PATH |
The folder that contains your own copy of rpyc, if you don’t use the one in the Desktop
package. |
Python client example
This example registers a tool called studio_tool, lists the connected apps, and closes the connection.
It needs the dousen package and rpyc 6.0.1. The package that ships with Desktop includes
rpyc.
PYTHONfrom dousen.bridge import BridgeClient
with BridgeClient() as bridge:
if not bridge.register("studio_tool", "Studio Tool"):
raise RuntimeError("Bridge registration failed")
apps = bridge.list_apps()
print([app["app_id"] for app in apps])
HttpBridgeClient is an alias for BridgeClient that keeps old imports working. It uses RPyC
too.
Client methods
| Method | What it does |
|---|---|
register(app_id, name='', **capabilities) |
Registers your app and returns True or False. Extra keyword arguments, such as
command_port, are sent as request fields. A registration expires after 90 seconds, so register
again within that time. subscribe and subscribe_async do this every 10 seconds. |
deregister(app_id=None) |
Removes a registration by sending unregister. Without an argument, it uses the ID from your last
successful register. It returns nothing and doesn’t raise an error if Desktop can’t be reached. |
list_apps() |
Returns a list of dictionaries with app_id and subscription_count (always 0) for the
apps in Desktop’s app list and the apps registered over the bridge. The list is empty if the bridge can’t be
reached. |
publish(event_type, source_app, file_path, target_app='', metadata=None, task_id='', changelist_id='', event_id=None) |
Publishes one event. Copies task_id and changelist_id into metadata,
and sets metadata["event_id"] to the event_id argument, to the
event_id already in metadata, or to a random UUID, in that order. Returns False if Desktop rejects the event or the request fails. See
Limits and failure handling before you publish again. |
subscribe(subscriber_app, event_type_filter='', on_event=None) |
Registers your app, opens the event stream, and blocks until stop() or close() is
called from another thread. |
subscribe_async(subscriber_app, event_type_filter='', on_event=None, reconnect=True) |
Runs subscribe on a background thread. The stream reconnects if it drops, whatever the value of
reconnect. |
wait_until_connected(timeout=5) |
Waits until the event stream is connected and returns True or False. A registration
doesn’t mean the stream is connected. |
request(operation, payload=None, timeout=None) |
Sends one request and returns the response body. The default timeout is 15 seconds. For any status other
than 2xx, it raises BridgeError, which carries the status and the response body. See
Read an error response. |
stop() / close() |
stop() ends the subscription. close() calls deregister() and then
stop(). The with statement calls close() when the block ends. |
Receive events
PYTHONfrom dousen.bridge import BridgeClient
bridge = BridgeClient()
def on_event(event):
# Queue work onto the host application's main thread.
# Don't call Maya, Blender or Houdini scene APIs from this callback.
print(event.event_type, event.file_path)
bridge.subscribe_async("studio_tool", "mesh.*", on_event)
if not bridge.wait_until_connected():
bridge.close()
raise RuntimeError("Subscription handshake failed")
# Keep bridge alive while your tool is running, and call bridge.close()
# when your tool shuts down.
An empty filter matches every event type. A filter that ends in .*, such as mesh.*, matches
every event type with that prefix. Any other filter must match the event type exactly. Desktop doesn’t send an app
its own events, and it delivers an event that has a target_app only to that app.
Your callback runs on a background thread. Move scene changes to the DCC’s main thread, and Qt UI changes to the Qt main thread through a queued call.
When your stream connects, BridgeClient first calls your handler with an empty
task.context event, if your filter matches it. Desktop then sends the latest task.context,
if it has one. The same empty event arrives if the stream drops, reconnects or overflows. Each time, discard the task
context you cached, and wait for a new one before you do work that depends on the task.
Event fields
| Field | Meaning |
|---|---|
event_id |
Identifies the event. Desktop uses it to drop duplicates, and to match an import result to a publish. |
event_type |
What happened. See Event types. |
source_app / target_app |
The app that sent the event, and an optional single recipient. If target_app is empty, every
subscriber can receive the event. Otherwise it must be an app ID of 1 to 64 letters, digits, _ or
-. Desktop accepts any other value in a publish request, but never delivers the
event. If source_app is empty, Desktop sets it to rpyc. |
file_path | The file the event is about, if there is one. |
metadata |
The event-specific fields, as a dictionary. On the wire this is a JSON string named
metadata_json. |
task_id / changelist_id |
The task and changelist the event belongs to, as top-level fields or as keys in metadata. The
publish request accepts only metadata_json, so publishers put them in
metadata. |
timestamp | The time of the event, in Unix seconds. |
Event types
The relay accepts any event_type. The events below are the ones that DousenDesktop, the DCC addons and
the Unreal plugin send and act on. Validation rule IDs such as mesh.poly_count look similar, but they
belong to asset validation and are never bridge events.
Export events from DCCs
A DCC addon sends one of these events for each instance you publish, and the Unreal plugin reads it to import the
asset. The Desktop install defines these fields in python/dousen/addons/_ue_contract.py.
| Event | Required metadata | Optional metadata | What it publishes |
|---|---|---|---|
mesh.export |
suggested_name, source_file |
suggested_ue_path, convention_tags, cv_tags, references,
material_assignment |
A static or skeletal mesh FBX. A skeletal mesh also produces its skeleton. |
texture.export |
suggested_name (the target mesh), textures (a list of absolute paths) |
suggested_ue_path, master_material, material_assignment,
import_mode, mesh_slot |
A set of textures. |
animation.export |
suggested_name, source_file |
suggested_ue_path, skeleton, convention_tags, cv_tags,
references |
An animation clip FBX. |
anim.export is an older name for animation.export. Desktop and the plugin still accept it,
but new tools should use animation.export. The metadata fields work as follows:
| Field | How it is used |
|---|---|
source_file and the event’s file_path |
Set the event’s file_path to the exported file. Unreal reads source_file from the
metadata, or file_path if source_file is missing. |
suggested_name |
If it is missing, Unreal uses the base name of the exported file. |
convention_tags, cv_tags |
Objects with string values. Unreal reads the asset type from convention_tags.asset_type and
stamps the tags on the imported asset as metadata. Convention tags are stored as
Dousen.Convention.<key>, and CV tags as dousen_<key>, for example
dousen_cv_poly_tier. The cv_tags are searchable in the Content Browser. Desktop adds
cv_tags to a mesh or animation export itself when the metadata has
geometry_stats. See Auto-tagging on export. |
references |
The files the scene references. Desktop reports them to DousenCore as asset dependencies. For an animation, Unreal also uses them to find the skeleton. |
skeleton |
For animation.export. If it isn’t set, Unreal follows references from the rig scene
to the skeletal mesh imported from it, and from there to its skeleton. If that finds nothing, Unreal uses the
*_Skeleton asset in the destination folder. |
import_mode, mesh_slot |
For texture.export. With mesh_materials, the default, Unreal binds the textures into
the material slots of the target mesh, and mesh_slot names the slot to bind. With
textures, Unreal imports them as standalone texture assets. |
jira_key, source_scene_path, source_depot_path,
source_repo_role, sync_manifest |
Optional. Where the source files live in version control. Desktop uses them to register the source asset with DousenCore, which needs an active project. See Source repositories. |
published_depot_path, published_repo_role |
Optional. Desktop also uses them to register the source asset. Unreal stamps them on the imported asset for the Reimport (sync source) menu item. |
The material_assignment object
material_assignment assigns catalog materials to mesh slots. It is an alternative to the flat
textures and master_material fields, and Unreal checks it first. Each entry in
assignments describes one material slot:
JSON"material_assignment": {
"catalog_version": 1,
"assignments": [
{
"mesh_slot": "CrateBody",
"material_id": "coated_metal",
"material_path": "/Game/Materials/M_CoatedMetal",
"textures": {"BaseColor": "C:/Work/Textures/T_Crate_BaseColor.tga"},
"scalar_params": {"RoughnessMult": 0.6},
"vector_params": {"Tint": [1, 0.9, 0.9, 1]}
}
]
}
| Field | Rules |
|---|---|
mesh_slot |
The material slot or texture set name. Use an empty string for an asset with one slot. |
material_id, material_path |
Each assignment needs one of them. Unreal skips an assignment that has neither. Unreal looks up
material_id in the project’s material catalog first. If that finds no entry, it loads
material_path. If that fails too, it uses the default master material. |
vector_params |
Each vector needs at least three numbers. If you leave out the fourth, Unreal sets the alpha to 1. |
allow_nanite |
Unreal never reads it from the event. The Nanite setting comes from Allow Nanite on the project’s catalog entry. |
The catalog is created in Unreal. See Materials and textures.
Tracked and untracked exports
An export event (mesh.export, texture.export or animation.export) is a
tracked publish when its metadata contains both event_id and instance_id. A tracked
publish appears in Publish history and can be retried. It is marked Imported and saved in
Unreal only after Desktop receives a matching import receipt from Unreal.
Desktop sends a tracked
publish to Unreal itself, either to an open editor or through a headless import, so other subscribers never receive
it.
An export event without those two keys is untracked. Desktop sends it to subscribers and registers the source asset, but it creates no history entry, so Desktop can’t offer a retry and never reports whether an import worked. Desktop doesn’t import an untracked event in the background. An open Unreal editor that receives it reimports an existing asset, or offers an Import prompt for a new one.
To make a tracked publish, send these metadata fields:
| Metadata field | Requirement |
|---|---|
event_id |
A UUID, or 32 to 128 hexadecimal characters. Use a new value for each publish attempt. Desktop rejects a
repeated event_id unless the event is identical to the first one. With
BridgeClient.publish(), pass it as the event_id argument or put it in
metadata. |
instance_id |
The ID of the instance you are publishing, up to 256 characters. Desktop shows it in the history entry. |
bundle_id |
Optional, up to 128 characters. Use the same value for instances you publish together. |
publish_identity |
An object with credential_id, server_url, account_id and
tenant_id that matches the user signed in to Desktop. Desktop rejects the publish if it is missing
or different. The DCC addons read these values from the credential file that Desktop passes to the processes it
launches, through DOUSEN_CREDENTIAL_FILE, DOUSEN_CREDENTIAL_ID and
DOUSEN_CORE_URL. |
publish_history |
Optional. {"validated": true} records that validation ran before the publish. |
Set file_path to the exported file, and leave target_app empty. Desktop records the
file’s size and modification time, and doesn’t offer a retry if the file changes. The whole
metadata_json value must be 16 KiB or smaller.
Desktop keeps only the metadata fields below in the history entry and in the event it sends to Unreal, and drops any others. Maps hold at most 128 entries, and lists at most 256.
| Group | Fields kept |
|---|---|
| Publish tracking | event_id, parent_event_id, bundle_id, instance_id,
publish_history, publish_identity |
| Unreal import | suggested_name, suggested_ue_path, source_file, skeleton,
asset_type, format, references |
| Materials and textures | master_material, import_mode, mesh_slot, textures,
material_assignment |
| Tags | convention_tags, cv_tags |
| Source files | jira_key, source_scene_path, source_depot_path,
published_depot_path, source_repo_role, published_repo_role,
sync_manifest, storage_type, storage_ref |
The event has no field for the target project. Desktop sends a tracked publish to the Unreal project configured in Desktop, and records the publish as unconfirmed if none is configured. Before it sends the import, Desktop checks that the connected editor has that project open.
If your tool can’t export an instance, send a publish.failed event instead of an export event. Use the
event_id, instance_id, bundle_id and publish_identity that the
export would have used, and leave file_path empty. With BridgeClient.publish(), pass the
event_id as the argument and the other three in metadata. Desktop lists the instance as
failed in Publish history.
Events from Unreal
When you save an asset in the editor, the Unreal plugin publishes an event with source_app set to
unreal. These events reuse the names of DCC export events, but their metadata is different.
| Event | Sent when you save |
|---|---|
mesh.export | A static mesh or skeletal mesh. |
texture.export | A Texture2D. |
material.export | A material or material instance. |
All three events carry the same metadata:
| Metadata field | Meaning |
|---|---|
asset_path | The package path in Unreal. |
asset_class | The asset class, for example StaticMesh. |
package_file | The path of the .uasset file. |
They have no suggested_name, source_file or event_id, so they are never
tracked. If you handle mesh.export or texture.export, check whether
source_app is unreal to tell the two kinds apart.
dcc.open
dcc.open asks an app to open a file. Desktop and Unreal handle it in two forms.
| Sent by | Metadata | What happens |
|---|---|---|
The Open in DCC menu in the Unreal Content Browser. It leaves target_app
empty. |
jira_key, asset_name, preferred_dcc |
Desktop handles every dcc.open event, whatever its target_app. It fetches the asset’s
source record from DousenCore, finds the file on the local disk, and launches the app whose ID is
preferred_dcc. If preferred_dcc is empty, it launches the DCC recorded when the asset
was published. Desktop ignores the event if jira_key or asset_name is empty. |
Any client, with target_app set to unreal or left empty |
asset_path |
Unreal looks up the asset in the Asset Registry and shows it in the Content Browser. If
asset_path is missing, the plugin uses file_path. No built-in addon sends this
form. |
Houdini, Substance Painter and Substance Designer also subscribe to dcc.open. A running session
opens the event’s file_path, or metadata.file_path if you set it. Blender,
Maya and 3ds Max don’t handle dcc.open.
Desktop launches only the apps in its own app list (Settings → DCC Apps). A tool that only calls
register still appears in the dccs response, in Connected apps and in
Unreal’s Open in DCC menu, but choosing it in that menu does nothing. To make the tool a launch
target, add it to the app list. See Launching DCCs.
task.context
Each time you select a different task in My tasks, Desktop publishes task.context
with source_app set to dousen_desktop. Other apps use it to tag their output with the
active task without asking Jira. task_id and changelist_id are both top-level event fields
and metadata keys. The metadata has these fields:
| Field | Meaning |
|---|---|
task_id | The ID of the selected task. |
task_title | The task’s title. |
jira_key | The task’s Jira key, for example DOU-123. |
changelist_id | The DousenCore ID of the task’s changelist. |
vcs_changelist_id | The Perforce changelist number. |
vcs_changelists |
An object with a ue entry and a source entry. Each has a
changelist_id and a vcs_changelist_id. In a project without a separate source
repository, both entries have the same values. |
project_id | The ID of the project that is active in Desktop. |
When you sign out, Desktop publishes an empty context. The DCC addons keep the latest context they receive instead of relying on environment variables set when the DCC started.
import.result
After a tracked publish is imported, Desktop gets an import receipt from Unreal. For an open editor, the receipt is
the reply to the import that Desktop sends. For a background import, Desktop reads it from the headless import.
Desktop uses the receipt to update the history entry. It accepts the same receipt as an import.result
event, and doesn’t forward that event to subscribers. Your tool doesn’t need to send it.
| Metadata field | Meaning |
|---|---|
event_id, bundle_id, instance_id |
Copied from the publish. Desktop rejects the event if event_id is missing or no history entry
matches. |
dispatch_generation |
A number that Desktop adds to each delivery and each retry. Desktop rejects the event if it is missing or doesn’t match the latest delivery. |
ok, saved, asset |
The publish becomes Imported and saved in Unreal only if ok and
saved are true and asset holds the path of the imported asset. |
may_have_imported, retry_safe, error |
If the import isn’t confirmed and may_have_imported is true, Desktop records the publish as
unconfirmed. A missing may_have_imported counts as true. Otherwise it records a failure, and
retry_safe says whether a retry is allowed. |
Request operations
Send a request with BridgeClient.request(operation, payload). The bridge accepts the operations in the
table below. For registration and publishing, use the client methods. The events operation returns a
short diagnostic history. Live events arrive on the stream.
Request payload reference
| Operation | Payload fields | Response |
|---|---|---|
register |
id, and optionally name, command_port and
capabilities. |
{"ok": true}. Status 400 unless the ID is 1 to 64 letters, digits, underscores or hyphens, the
name is up to 256 characters, and the command port is from 0 to 65535. Status 429 when 64 apps are already
registered. An expired registration still counts toward the 64 until Desktop next answers
dccs. exclusive_publish_dispatch is the only capability Desktop reads, and only the Unreal
plugin sends it. |
unregister |
id. |
{"ok": true}, whether or not the app was registered. |
dccs |
An empty object. | A JSON array of {id, name} objects for the apps in Desktop’s app list and the apps registered over
the bridge. A registered app has a command_port if it sent one that isn’t 0. |
status |
An empty object. | {"apps": [{"app_id", "subscription_count"}]}, a diagnostic list of the apps that published an
event in the last 30 seconds, including apps that publish over the bridge and dousen_desktop. An app
stays listed for 30 seconds after its last publish. To find the registered apps, use dccs or
list_apps().
subscription_count is always 0. |
publish |
event_type, and optionally source_app, target_app,
file_path and metadata_json. All of them must be strings. |
{"ok": true}, or status 409 with {"ok": false, "error_code": "publish_rejected"}.
There are no task_id or changelist_id fields, so put them in
metadata_json. |
events |
Optional since, in Unix seconds. |
{"events": [...], "timestamp": ...} with up to the last 200 events newer than
since, except events that Desktop dispatches itself. |
history |
Optional limit. The default is 20, and values outside 1 to 50 are clamped to that range. |
{"version": 1, "entries": [...], "capabilities": {"headless_auto_import": ...}}. The entries are
the publishes of the user signed in to Desktop. Each has event_id, bundle_id,
instance_id, event_type, state, error_code,
hint, validated_at, exported_at, delivered_at,
imported_at, dispatch_generation and retry_allowed. The state is
validated, exported, delivered, imported,
failed or unconfirmed. |
retry |
event_id of the original publish, in the same format as for a publish. |
Status 202 with {"ok": true, "error_code": "started", "dispatch_generation": ...} when the retry
starts. Otherwise status 404 (not_found), 409 or 503 (persistence_failed), with
error_code saying why. For 409 it is one of session_invalid,
already_imported, reconciliation_required, not_retryable,
already_in_flight, source_missing, source_changed and
target_project_mismatch. |
source/ensure-mapped |
path, a depot path or a local path. |
Maps the folder for the path’s repository in the Perforce workspace, so you can add a new file there.
Returns ok, role, depot_path and, if known, local_path. |
source/sync |
depot_path, and optionally role. Without role, Desktop works it out from
the path. |
Syncs the file and its .dousen_meta.json sidecar, if the depot has one. Returns
ok, local_path and synced. Fails if the local file has unsubmitted
edits. |
Besides the statuses in the table above, the bridge returns these:
| Status | Meaning |
|---|---|
| 400 | Malformed JSON or an invalid field. |
| 404 | Unknown operation. |
| 503 | Desktop can’t handle the request yet, or didn’t answer within 15 seconds. |
Once Desktop accepts a source/* request, it reports a Perforce or routing failure as status 200 with
"ok": false and an error message. A missing path or depot_path
returns 400. A request that takes longer than 15 seconds returns 503, but the sync may still finish. Use these
operations with the source-repository workflow.
Read an error response
BridgeClient.request returns the response body for any 2xx status. A successful retry
returns status 202, so you can read error_code and dispatch_generation from the return
value. For any other status it raises dousen.bridge.BridgeError, a subclass of OSError.
Its status and body attributes hold the status and the response body, so an existing
except OSError handler still catches it.
PYTHONfrom dousen.bridge import BridgeClient, BridgeError
with BridgeClient() as bridge:
try:
reply = bridge.request("retry", {"event_id": event_id})
print(reply.get("error_code"))
except BridgeError as error:
print(error.status, error.body)
Limits and failure handling
- A request or response value can be up to 4 MiB of JSON.
BridgeClientraisesValueErrorfor a larger request before it sends it. The bridge allows only the operations on this page and the event stream. It doesn’t allow pickle, remote attribute access or remote execution. - At most 64 apps can be registered, and registrations expire after 90 seconds.
- Each subscriber has a mailbox limited by the number of events and their total size. If it overflows, the
subscriber receives a gap marker, which
BridgeClientturns into an emptytask.context. - Each stream has a session ID and a sequence number.
BridgeClientuses them to skip events it already received. - A request is sent once. If the response is lost, the result is unknown, and the client doesn’t send the request again.
A True result from publish means that Desktop accepted the event. It doesn’t mean that
Unreal imported the asset. After a timeout or a False result, Desktop may still have accepted the event,
so don’t publish again automatically. Check Publish history and the destination in Unreal first. See
Recover from a failed publish.
If Desktop restarts, it generates a new bridge token and closes the stream. Your subscription reconnects with the new token. Discard any task context you cached.
Source files
The Desktop install includes the Python client, python/dousen/bridge.py, and its RPyC transport,
python/dousen/rpc.py. To start a tool with a DCC, see
Loading studio tools.