ZOA Diagnostics Contracts
Two types, no dependencies, so any package can publish a debug tab without depending on Nucleon.
This package exists to remove one edge from the dependency graph. The debug contributor contract used to live in com.zoa.nucleon, which meant a package that wanted to publish a diagnostic tab had to take a dependency on the player package. For inventory or armory or networking that edge is architecturally wrong, and it was the only thing forcing it.
Lifting the contract into its own package solves it. IDebugContributor and AutoRegisterDebugContributorAttribute now sit in a package with an empty dependency list and an assembly definition with no references at all. Any package can depend on it, author its contributor inside its own Editor directory, and appear in the Workbench Diagnostics capability without a single new upstream edge.
There is nothing else in the package. A contract package that grows an implementation starts acquiring dependencies of its own, which would push them back onto every consumer.
Depends on (0)
Depended on by (1)
How it works
Concepts
The namespace does not match the package name
The types live in ZOA.Nucleon.Debugging, not ZOA.Diagnostics.Contracts. That preserves source compatibility: the contributors that already shipped under com.zoa.nucleon keep resolving their existing using directive without a source change, and the assembly's rootNamespace is set to match.
External consumers that referenced the types through the old assembly name are covered by a [TypeForwardedTo] emitted from the nucleon runtime assembly, so the move is invisible on both sides.
How a contributor is found
Discovery is reflection-driven and editor-only, and it lives in com.zoa.nucleon rather than here. DebugContributorRegistry walks every assembly in the AppDomain for types carrying AutoRegisterDebugContributorAttribute, checks that each one implements IDebugContributor and is neither abstract nor an interface, and logs a warning and skips anything that fails those checks rather than throwing. An assembly that fails to load its full type list contributes whatever types it could resolve.
Results are sorted by the attribute's Order ascending and tie-broken by the type's full name, so the tab layout is stable regardless of assembly load order. The registry caches its result and invalidates the cache after every assembly reload.
DebugContributorBridge listens for play-mode transitions. On entering play mode it instantiates every discovered contributor and registers it with the debug UI controller; on exiting it clears the registrations it owns so the registry stays authoritative.
What an implementation owes the host
RegisterUI is called once at attach time and builds the contributor's surface into the supplied VisualElement. It must be idempotent under repeated attach, because the preview renderer can re-attach the same contributor to a new panel.
UpdateUI runs on the Workbench refresh tick. It should avoid allocation and early-exit when the underlying state has not changed, because it runs whether or not the tab is the one on screen.
TabName groups contributors. Two contributors returning the same TabName render into one tab, which is how several packages can contribute to a single subject area without coordinating.
In the editor
Screens
Screenshot pending
/screenshots/diagnostics-contracts-tab.png
The Workbench Diagnostics capability in play mode with several contributor tabs across the top, one selected, showing a per-package inspector surface built by that package's own IDebugContributor.
Setup
Workflow
- 01
Reference the package
Add com.zoa.diagnostics.contracts to your package.json dependencies. It has no dependencies of its own, so the edge pulls nothing else into your graph.
- 02
Author the contributor in your own Editor directory
Implement IDebugContributor in your package's editor assembly, next to the system it inspects. The contributor then sits with the code it reports on, and ships and versions with it.
- 03
Decorate it
Add [AutoRegisterDebugContributor] with an Order that places your tab sensibly relative to the others. Contributors sharing a TabName merge into one tab.
- 04
Enter play mode
The bridge in com.zoa.nucleon discovers, instantiates and registers the contributor on entering play mode, and clears it on exit. There is nothing to wire by hand.
Surface
Key types
IDebugContributor
interface
The contract a package implements to publish a diagnostic tab into the Workbench. It has three members, all editor-facing.
- string TabName { get; }
- void RegisterUI(VisualElement root)
- void UpdateUI()
AutoRegisterDebugContributorAttribute
class
Marks a contributor for auto-registration at play-mode entry. Order gives stable layout; Label optionally overrides the tab placement name.
- AutoRegisterDebugContributorAttribute(int order = 0, string label = null)
- int Order { get; }
- string Label { get; }
Usage
Examples
using UnityEngine.UIElements;
using ZOA.Nucleon.Debugging;
namespace MyStudio.Ballistics.Editor.Diagnostics
{
// Order gives the tab a stable position: discovery sorts ascending
// and ties break on full type name, so layout never depends on
// assembly load order.
[AutoRegisterDebugContributor(order: 40, label: "Ballistics")]
public sealed class BallisticsDebugContributor : IDebugContributor
{
// Contributors sharing a TabName render into one tab.
public string TabName => "Combat";
private Label _lastShot;
private int _lastRenderedShotId = -1;
public void RegisterUI(VisualElement root)
{
// Must be idempotent: the preview renderer can re-attach
// this contributor to a fresh panel.
root.Clear();
root.Add(new Label("Ballistics"));
_lastShot = new Label("No shots this session.");
root.Add(_lastShot);
}
public void UpdateUI()
{
// Runs on the refresh tick whether or not this tab is visible,
// so early-exit when nothing has changed and allocate nothing.
var shotId = BallisticsProbe.LastShotId;
if (shotId == _lastRenderedShotId) return;
_lastRenderedShotId = shotId;
_lastShot.text = BallisticsProbe.DescribeLastShot();
}
}
}Read this
Notes and caveats
See also