Tier 1 · Applications
Character

Character Setup

dcc/maya/character/setup_tool/main_window.pyCFCharacterSetupWindow (WINDOW_OBJECT_NAME = "CF_CharacterSetupWindow"), launched from Character > Character Setup. The authoring UI for this project's own character-aware data model — a lightweight, tagged data node that other applications (chiefly AnimHub and its export pipeline) key off of.

Its Maya-menu/QuickLaunch icon sits next to its launcher module — the in-app window doesn't set its own window icon.
Design goals
1
Problem

Every downstream tool that needs to reason about "which rig is this," "what's its export target," or "what's attached to what" needs a shared, discoverable notion of a character - without one, every application ends up inventing its own ad hoc way to find and identify rigs, and none of them agree with each other.

2
Architectural decision

Represent a character as data on a native Maya node - real typed attributes (enums, message connections, a compound multi-attribute for sockets) applied generically from one declarative schema table, not a custom node type and not a hand-written cmds.addAttr call per field. A stable UUID, not a node name, is the join key every other application uses to reference a character, so a rename never breaks a downstream reference.

3
Production benefit

Any application can discover every character in a scene with one function call, resolve a stored reference back to a live node even after a rename, and self-heal a partially-created or corrupted node automatically on refresh - and a one-directional import path adopts characters authored in a prior pipeline without disturbing whatever that older tool still expects to find.

Character Node stable UUID Control Rig Export Asset Sockets (compound) Attachments AnimHub & other applications, by UUID

Every relationship is a real Maya connection or compound attribute on the node itself - not a side table or cache - so two wrapper instances for the same node are always in sync.

What a "Character" is

A CFCharacter registers a rig as something other applications can discover and reason about, and records the metadata the export pipeline needs:

It's a clean-room redesign of an earlier, unrelated character-authoring tool, informed by years of that tool's own real production use — not a port of its code. The two concrete design changes: real Python enums instead of sentinel-string encoding, and a declarative attribute table (AttrSpec, same pattern as animation/schema.py) that gets walked generically instead of hand-written cmds.addAttr calls per field.

Data model

Storage

A CFCharacter lives on a native Maya character set node (cmds.character(name=..., empty=True)) — not a custom node type, not a locator — carrying a set of CF_Char* custom attributes. A node only "counts" as a CF character once it carries CF_CharUUID; an ordinary artist-made character set, or one built by an older tool, is ignored by list_all_characters() until explicitly imported (see Legacy Data Migration).

CFCharacter itself (model.py) is a thin, cache-free adapter: an instance holds only the node name, and every property read/write goes straight to the live scene. Two CFCharacter instances wrapping the same node are always in sync with each other.

Schema (schema.py)

AttributeKindPurpose
CF_CharNamestringDisplay name (.name). rename() keeps the Maya node name in sync, sanitized for legal node names — Maya may still uniquify further on a collision.
CF_CharExportNamestringSeparate name used specifically at export time.
CF_CharUUIDstringStable identifier — the join key other tools use (find_by_uuid), and the marker attribute that makes a node "a registered character" at all. Self-heals: regenerated if empty or colliding.
CF_CharTypestringCharacter / Attachment / Weapon / Prop / Custom by default, but plain text. Setting this to Attachment automatically forces bake mode to Origin.
CF_CharBakeModeenumWorld (default) or Origin.
CF_CharControlRigmessageSingle connection to the control rig node.
CF_CharExportAssetmessageSingle connection to the export/geometry root — deliberately one connection, not a fan-out list.
CF_CharAttachmentsmessageBuilds the parent → child attachment chain between characters.
CF_CharacterMapmessageSchema slot only — no UI built on it yet, reserved for a feature not yet ported. Gap
CF_CharSocketscompound, multiOne element per socket.
CF_CharSocketName / CF_CharSocketJointstring / messageSocket's name and its joint connection — children of the sockets compound.

Lifecycle / self-healing

UI walkthrough

Menu bar — File (Refresh, Close), Tools (Import Legacy Setup..., the only entry point for legacy import — see below), Help (reserved, currently empty).

Characters panel (top)

Form fields (disabled with nothing selected; every field persists immediately on edit, no Save step):

Attach rowAttach To... (pick any other character in the scene as parent; forces type to Attachment) / Detach.

Sockets panel (bottom, labeled "SOCKETS — <character name>")

Public API other tools use

character/model.py is the shared data-access layer, typically imported as from CoreTools.dcc.maya.character import model as character_model.

Legacy Data Migration

Tools > Import Legacy Setup... lists every character-like node in the scene that predates this project's own schema and lets you adopt one into it — carrying over its existing name, export name, type, bake mode, control rig, first export asset, and attachment relationships (mapped onto this project's own socket model) — without touching or removing any of the original tool's own data, so a migrated node keeps working with both toolsets side by side. This lives only in the Tools menu (not duplicated as a toolbar button), by design: a rarely-used, one-directional migration action gets one authoritative entry point, not several.

The real engineering challenge wasn't the copy itself — it was reverse-engineering how the legacy system actually represented its data before any of it could be translated, since none of that structure was documented anywhere. Three problem classes came up repeatedly: relationships stored in the opposite connection direction from this project's own convention (detected and corrected automatically rather than assumed fixed, since a naive one-direction importer would have silently imported nothing); relationships that weren't simple attributes at all but lived in a more indirect, multi-hop structure elsewhere in the rig, which had to be walked and reconstructed into a proper socket-based attachment here rather than copied as-is; and value sets that looked compatible but were confirmed field-by-field rather than assumed, since a silent mismatch there fails quietly instead of loudly. Where the legacy schema allowed something this one doesn't — multiple export assets on one character — the import takes the first and documents the limit as a deliberate scope decision, not silent data loss.

Design notes worth knowing