Toolingcom.zoa.content · v0.1.0

ZOA Content

Pack manifests, Addressables conventions, and the browser that installs a whole genre in one click.

Content is two systems that meet in the middle. Underneath sits a small, Unity-free core: a manifest describing a pack of assets, case-insensitive identifiers for packs and assets, a runtime registry, a dependency graph, and the rules for what an Addressables key is allowed to look like. On top sits the authoring pipeline: ScriptableObject packs, curated Feature Sets, an installer that walks a pack's action list, and a browser that renders the whole catalogue.

The organising idea is the dimension. A Feature Set is a themed preset that names one content pack per dimension: UI theme, input bindings, weapons, AI roster, audio palette, environment, genre bundle, items, loot tables, recipes, loadout, demo scene. Twelve slots, each optional. Installing a Feature Set installs each dimension independently, so two sets compose instead of excluding one another. Take Cyberpunk's UI, Wild West's weapons and Ancient World's AI in one pass and nothing conflicts, because no dimension knows about the others.

The shipped sample bundles nine flagship Feature Sets: Modern Warfare, Sci Fi, Dieselpunk, Cyberpunk, Fantasy/Medieval, Wild West, Post-Apocalypse, Ancient World, and Cosmic Horror/Gaslight. Each ships a complete stack, so installing one produces a coherent playable experience rather than a partial theme with holes in it.

Depended on by (1)

How it works

Concepts

The pack manifest

ContentPackManifest is a plain serialisable class in ZOA.Content.Core, free of any Unity dependency so it round-trips to JSON. It carries an Id, DisplayName, Version defaulting to 0.1.0, Author, Description, a list of ContentPackId dependencies, and a list of ContentAssetEntry values.

The manifest is also the query surface. GetAssetsByType filters by ContentAssetType (Definition, Prefab, Material, Texture, Audio, Animation, Scene, Data, Other) and GetAssetsByTag filters by a tag string, so a consumer can ask a pack for its prefabs or for everything tagged sniper without knowing how the pack was authored. AddDependency refuses empty ids and silently skips duplicates, and AddAsset refuses an entry with an empty AssetId, so a manifest cannot accumulate junk through the normal API.

ContentPackId and ContentAssetId are readonly structs that normalise to trimmed lowercase on construction and compare with OrdinalIgnoreCase. Both define implicit conversions in each direction, so a string literal is a valid id at any call site and an id can be used wherever a string is expected. Both expose IsEmpty and a TryParse that returns false for null or whitespace.

One authority on key shape

AddressableConvention is the single authority on key shape. A valid key matches ^[a-z0-9_-]+(/[a-z0-9_-]+)*$: lowercase letters, digits, underscores and hyphens, separated by forward slashes. ValidateKey is the check; a key with an uppercase letter, a space or a dot fails.

SuggestKey builds pack_id/asset_id, and SuggestKeyWithGroup builds pack_id/group/asset_id when you want a category segment such as materials or prefabs. Both normalise every segment: lowercase, spaces and dots become underscores, anything not alphanumeric or underscore or hyphen is dropped, runs of underscores collapse, and leading and trailing underscores are trimmed. com.zoa.WeaponPack SciFi therefore normalises to com_zoa_weaponpack_scifi instead of being rejected.

GroupNameFor returns the Addressables group name for a pack, which is just the normalised pack id, falling back to default for an empty id. One group per pack is the convention the rest of the pipeline assumes.

Dependencies and validation

ContentDependencyResolver builds a ContentDependencyNode per asset in a manifest. A node holds its dependency list, a MissingDependencies list, and an IsBroken flag that flips true the moment a dependency is marked missing and back to false when the last missing entry is resolved. FindBrokenReferences filters a graph down to the nodes that will not load.

Validation returns a ContentValidationResult rather than throwing. Adding an Error issue flips IsValid to false automatically, so a caller cannot forget to check. Issues carry a machine-readable Code alongside the human message, plus the AssetId they relate to, which may be empty for pack-level problems. ErrorCount, WarningCount and InfoCount, and the matching GetErrors, GetWarnings and GetInfo, are there so a UI can group without re-filtering by hand.

IContentValidator is a contract with no shipped default implementation. Provide one when you want project-specific rules; the model types it returns are complete.

Content packs as assets, actions as data

ContentPackDefinition is the authoring shape: a DefinitionBase ScriptableObject wrapping a ContentPackManifest, plus cover artwork, a tagline, and a [SerializeReference] list of IContentPackAction. Create one from Assets > Create > ZOA > Content > Content Pack Definition.

An action is data, not a scene object. Each domain ships its own implementations in its own editor assembly, and the wizard discovers them through TypeCache and persists them polymorphically inside the Feature Set asset. The contract is three properties and one method: DisplayName, Description, IsInstalled for a cheap existence check, and TryApply(out string reason). Implementations must be [Serializable], must be idempotent so a re-apply is a fast no-op, and must be failure-tolerant, because the orchestrator catches an exception from one action and carries on with the rest.

The action list is guarded by #if UNITY_EDITOR and the Actions property returns an empty array in a player build. The guard is a correctness fix: the action types do not exist at runtime, so serialising them into a runtime ScriptableObject changes the asset's layout and makes every player start log a serialization-layout warning.

Feature Sets: twelve dimensions, all optional

FeatureSetDefinition names a ContentPackDefinition per dimension: UiTheme, Input, Weapons, AiRoster, Audio, Environment, GenreBundle, Items, LootTables, Recipes, Loadout, DemoScene. TotalDimensionSlots is 12 and CoveredDimensionCount counts the non-null ones, and the coverage badge on a browser card reads from that count.

A null dimension means use the project's current default for that dimension, so a partial theme stays usable while it is still being authored. EnumerateDimensions yields the non-null packs in declared order, and that order is the apply order.

Alongside the dimensions a Feature Set carries a ThemeKey, the stable snake_case identifier such as modern_warfare or cosmic_horror used by save data and telemetry; a Cover texture and a short and long tagline for the browser; a HudStyle from Fps, Moba, Rpg or Strategy; and a DependsOn list of other Feature Sets that are guaranteed to install first. Most sets are self-contained and leave DependsOn empty.

Apply is idempotent by construction

ContentPackInstaller.ApplyFeatureSet walks the dimensions, invokes every action on every pack, and returns a List<ContentPackInstallResult>. Each result names the pack and action and records Success, WasAlreadyInstalled, and a Reason on failure. The browser reads that distinction to report an accurate per-row status after apply, instead of claiming work it skipped.

Most install actions route through ContentDeployerUtil, which copies source assets into a canonical destination under Assets/ZOA/Content/<dimension>/<theme>/. It skips anything already at its destination and skips anything the author has since edited in place, so re-running Apply preserves local changes.

One subtlety the deployer handles for you: AssetDatabase.CopyAsset preserves both the Unity GUID and the DefinitionBase id inside the YAML. Preserving the GUID keeps scene references alive. Preserving the definition id would make the source under Samples and the deployed clone collide on Duplicate DefinitionId the next time the registry builds, so the deployer regenerates the id on the deployed copy.

The browser is extensible from other packages

ContentPackBrowserWindow is a two-pane UI Toolkit window: a grid of Feature Set cards on the left, a per-dimension detail panel with install buttons on the right. Cards without authored cover art get a procedural 16:9 cover painted from a hash of the theme key, so a freshly imported set is never a blank tile.

Other packages contribute their own tabs. IContentBrowserTabProvider declares a TabId, display and pane labels, an empty-state message, a SortOrder, an ItemCount, a Refresh, and BuildGrid and BuildDetail methods that render into the browser's shared themed chrome. Providers register into ContentBrowserTabRegistry from an [InitializeOnLoad] initialiser, and registering the same TabId twice replaces the earlier provider rather than duplicating it.

Discovery is through AssetDatabase.FindAssets, and Samples~ folders are invisible to AssetDatabase until imported. So the browser can look empty on a fresh clone. ContentSampleAutoImporter closes that gap: it walks a hand-curated table of every ZOA-shipped Package Manager sample, imports what is missing, and rewires the action lists so Apply actually has work to do.

Feature Set HUDs

Each Feature Set names a HudStyle, and ZOAFeatureSetHudController is the runtime that honours it. It sits alongside a UIDocument, loads the matching UXML for Fps, Moba, Rpg or Strategy, and rebinds the panel binders. It touches no editor type, so the same HUD surface the browser previews is the one that ships.

IZOAFeatureSetHudService is the contract to program against rather than the controller itself. Scene generators, player adorners and authoring tools go through TryApplyFeatureSet, TryApplyStyle and Rebind, and read CurrentStyle, IsStyleLoaded and CurrentResourcePath for status.

Panels are described by ZOAHudComponentDefinition assets: a component id, the gameplay system the panel consumes, the binder type name that supplies data, a preferred slot and default normalised rect, and the required and optional USS class tokens a UXML template must expose. FeatureSetHudContractRegistry is the editor-time authority mapping shipped panels to their binders and selector contracts, and FeatureSetHudCompletenessValidator surfaces that same check on the Workbench dashboard so a template that has drifted from its contract shows up as an issue rather than an empty panel at runtime.

In the editor

Screens

Screenshot pending

/screenshots/content-browser-grid.png

Content Browser with all nine flagship cards visible, each showing cover art, tagline and a coverage badge reading something like 12/12 dimensions. One card selected.

The Feature Set grid

Screenshot pending

/screenshots/content-browser-detail.png

The right-hand detail panel for a selected Feature Set, listing every dimension (UI Theme, Input, Weapons, AI Roster, Audio, Environment, Genre Bundle, Items, Loot, Recipes, Loadout, Demo Scene) with a per-dimension install button and mixed installed/pending status indicators.

Per-dimension detail

Screenshot pending

/screenshots/content-pack-definition.png

Inspector on a ContentPackDefinition showing the manifest fields (id, version, author, dependencies) above the polymorphic action list with two or three concrete install actions expanded.

A content pack asset

Screenshot pending

/screenshots/content-startup-health.png

The Startup Health dashboard after a scan, with a mix of green and amber rows and the Generate All Content action visible.

Startup Health

Screenshot pending

/screenshots/content-hud-composer.png

The composer canvas with several panels enabled and one being dragged, the panel list showing binder and gameplay-system names beside each entry.

Feature Set HUD Composer

Setup

Workflow

  1. 01

    Populate the catalogue

    On a fresh clone the browser grid is empty, because Samples~ folders are invisible to AssetDatabase until imported. Run Tools > ZOA > Advanced > Generate > Content > Import All Content (Bulk). It walks the curated sample table across every ZOA package, imports what is missing, refreshes the database, and rewires the Feature Set action lists so Apply has real work to do.

  2. 02

    Browse and apply

    Open Tools > ZOA > Advanced > Generate > Content > Open Content Browser, or reach the same window from the Workbench Content module's Content Browser capability. Pick a Feature Set card, review the per-dimension breakdown in the detail panel, and apply either the whole set or one dimension at a time.

  3. 03

    Mix dimensions

    Apply is per pack, so applying a second Feature Set only overrides the dimensions it covers. Install one set's UI theme and another set's weapons; nothing in the model treats sets as exclusive.

  4. 04

    Author your own pack

    Create a ContentPackDefinition, fill in the manifest identity and version, and add asset entries. Then add the install actions the pack needs. Actions are chosen from whatever implementations of IContentPackAction are present in the project, so a pack can install a theme, a weapon pack and a loot table without you writing any of those installers.

  5. 05

    Compose a Feature Set

    Create a FeatureSetDefinition, set the ThemeKey and taglines, pick a HudStyle, and assign a ContentPackDefinition to each dimension you cover. Leave the rest null; they fall back to the project default at apply time and the card reports honest coverage against the twelve slots.

  6. 06

    Check the Addressables keys

    Before shipping a pack, run every entry's AddressableKey through AddressableConvention.ValidateKey, and use SuggestKey or SuggestKeyWithGroup to generate the ones that are missing. Publish the pack's assets into the group named by GroupNameFor.

  7. 07

    Author the HUD

    Tools > ZOA > Advanced > Build > UI > Feature Set HUD Composer opens the visual layout surface. Enable the panels the style needs and drag their normalised regions on the canvas. The panel registry stays the source of truth for defaults; the layout profile assets under Assets/ZOA/Bundled/UI/HUD/Layouts are the editable records designers clone and tune.

Surface

Key types

ContentPackManifest

class

The Unity-free description of a pack: identity, version, dependencies, and asset entries. Serialisable, so it round-trips to JSON.

  • ContentPackId Id
  • string DisplayName, Version, Author, Description
  • List<ContentPackId> Dependencies
  • List<ContentAssetEntry> Assets
  • void AddDependency(ContentPackId packId)
  • void RemoveDependency(ContentPackId packId)
  • void AddAsset(ContentAssetEntry asset)
  • bool RemoveAsset(ContentAssetId assetId)
  • bool TryGetAsset(ContentAssetId assetId, out ContentAssetEntry asset)
  • IEnumerable<ContentAssetEntry> GetAssetsByType(ContentAssetType type)
  • IEnumerable<ContentAssetEntry> GetAssetsByTag(string tag)

ContentPackId

struct

Readonly, case-insensitive pack identifier normalised to trimmed lowercase. Implicitly converts to and from string, so a literal works at any call site.

  • static ContentPackId Empty { get; }
  • bool IsEmpty { get; }
  • static bool TryParse(string value, out ContentPackId id)
  • implicit operator ContentPackId(string value)
  • implicit operator string(ContentPackId id)

ContentAssetId

struct

The same contract as ContentPackId, scoped to one asset within a pack. Comparable and orderable, so asset lists sort deterministically.

  • static ContentAssetId Empty { get; }
  • bool IsEmpty { get; }
  • static bool TryParse(string value, out ContentAssetId id)

ContentAssetEntry

struct

One asset in a pack: its id, its project path, its type, its tags, and the Addressables key it publishes under.

  • ContentAssetId AssetId
  • string AssetPath
  • ContentAssetType AssetType
  • List<string> Tags
  • string AddressableKey
  • bool HasTag(string tag)
  • void AddTag(string tag)

ContentAssetType

enum

Definition, Prefab, Material, Texture, Audio, Animation, Scene, Data, Other. Definition covers any ScriptableObject authoring asset.

AddressableConvention

class

Validates and generates Addressables keys. One group per pack, keys shaped pack_id/asset_id or pack_id/group/asset_id.

  • static bool ValidateKey(string key)
  • static string SuggestKey(ContentAssetEntry asset, ContentPackId packId)
  • static string SuggestKeyWithGroup(ContentAssetEntry asset, ContentPackId packId, string group)
  • static string GroupNameFor(ContentPackId packId)

IContentPackRegistry

interface

Runtime catalogue of registered manifests, with events on register and unregister so listeners can react without polling.

  • event ContentPackRegisteredEventHandler PackRegistered
  • event ContentPackUnregisteredEventHandler PackUnregistered
  • bool Register(ContentPackManifest manifest)
  • bool Unregister(ContentPackId packId)
  • bool TryGet(ContentPackId packId, out ContentPackManifest manifest)
  • IReadOnlyList<ContentPackManifest> GetAll()
  • IReadOnlyList<ContentPackManifest> GetByTag(string tag)
  • bool IsRegistered(ContentPackId packId)
  • void Clear()

ContentPackRegistry

class

The default in-memory registry, dictionary-backed. Register returns false rather than throwing when the id is already taken.

IContentDependencyResolver

interface

Builds a dependency graph over a manifest's assets and reports which nodes cannot resolve.

  • IReadOnlyList<ContentDependencyNode> BuildDependencyGraph(ContentPackManifest manifest)
  • IReadOnlyList<ContentDependencyNode> FindBrokenReferences(IEnumerable<ContentDependencyNode> nodes)
  • bool HasBrokenDependencies(ContentDependencyNode node)

ContentDependencyNode

class

One asset's node in the graph. IsBroken is maintained for you: it flips true on the first missing dependency and false when the last one resolves.

  • ContentAssetId AssetId { get; }
  • List<ContentAssetId> Dependencies { get; }
  • List<ContentAssetId> MissingDependencies { get; }
  • bool IsBroken { get; set; }
  • void AddDependency(ContentAssetId dependencyId)
  • void MarkMissing(ContentAssetId dependencyId)
  • void ResolveMissing(ContentAssetId dependencyId)
  • bool HasDependency(ContentAssetId assetId)

IContentValidator

interface

Single-method contract for pack validation. No default implementation ships; supply one when you have project rules to enforce.

  • ContentValidationResult ValidatePack(ContentPackManifest manifest)

ContentValidationResult

class

Accumulates issues and tracks validity. Adding an Error issue sets IsValid false automatically, so the flag cannot drift from the contents.

  • bool IsValid { get; }
  • List<ContentValidationIssue> Issues { get; }
  • void AddIssue(ContentValidationIssue issue)
  • int ErrorCount { get; }
  • int WarningCount { get; }
  • int InfoCount { get; }
  • IEnumerable<ContentValidationIssue> GetErrors()
  • IEnumerable<ContentValidationIssue> GetWarnings()
  • IEnumerable<ContentValidationIssue> GetInfo()

ContentValidationIssue

struct

One finding: severity, a machine-readable code, a human message, and the asset it concerns. AssetId is empty for pack-level issues.

  • ContentValidationIssueSeverity Severity
  • string Code
  • string Message
  • ContentAssetId AssetId

IContentPackAction

interface

One installable step inside a content pack. Implementations live in the owning domain's editor assembly, must be [Serializable], idempotent, and failure-tolerant.

  • string DisplayName { get; }
  • string Description { get; }
  • bool IsInstalled { get; }
  • bool TryApply(out string reason)

ContentPackActionBase

class

Optional [Serializable] base for actions, with a FormatException helper so TryApply can swallow a Unity throw and return a stable message.

ContentPackInstaller

class

The apply orchestrator. Walks a Feature Set's dimensions or a single pack's actions and returns a per-action report; one failing action never blocks the rest.

  • static List<ContentPackInstallResult> ApplyFeatureSet(FeatureSetDefinition set)
  • static List<ContentPackInstallResult> ApplyPack(ContentPackDefinition pack)

ContentPackInstallResult

struct

One row of the install report. WasAlreadyInstalled separates a no-op from work actually done, so the post-apply status reflects what happened.

  • string Pack
  • string Action
  • bool Success
  • bool WasAlreadyInstalled
  • string Reason

ContentDeployerUtil

class

Idempotent ScriptableObject deployer used by most install actions. Copies into Assets/ZOA/Content/<dimension>/<theme>/ and regenerates the definition id on the copy so source and clone do not collide.

  • const string ContentRoot = "Assets/ZOA/Content"
  • static bool DeployAll<T>(string dimensionFolder, string themeFolder, IList<T> sources, out List<string> deployed, out string reason) where T : Object
  • static bool AreAllInstalled<T>(string dimensionFolder, string themeFolder, IList<T> sources) where T : Object
  • static void EnsureFolder(string folderPath)

IContentBrowserTabProvider

interface

Extension point for a package-owned tab in the Content Browser. The provider owns its catalogue state; the browser owns the chrome.

  • string TabId { get; }
  • string DisplayName { get; }
  • string GridTitle { get; }
  • string DetailTitle { get; }
  • string EmptyStateText { get; }
  • int SortOrder { get; }
  • int ItemCount { get; }
  • void Refresh()
  • void BuildGrid(VisualElement gridContainer, Action requestRepaint)
  • void BuildDetail(VisualElement detailPanel, Action requestRepaint)

ContentBrowserTabRegistry

class

Lock-guarded provider list, sorted by SortOrder. Registering an existing TabId replaces the previous provider rather than duplicating the tab.

  • static void Register(IContentBrowserTabProvider provider)
  • static void Unregister(string tabId)
  • static IReadOnlyList<IContentBrowserTabProvider> GetProviders()

IZOAFeatureSetHudService

interface

The runtime HUD contract. Target this rather than the controller so scene generators and authoring tools do not depend on implementation details.

  • UIDocument Document { get; }
  • FeatureSetDefinition FeatureSet { get; set; }
  • FeatureSetDefinition.HudStyleKind CurrentStyle { get; }
  • bool IsStyleLoaded { get; }
  • string CurrentResourcePath { get; }
  • bool TryApplyFeatureSet(FeatureSetDefinition featureSet)
  • bool TryApplyStyle(FeatureSetDefinition.HudStyleKind style)
  • void Rebind()

ZOAFeatureSetHudController

component

The shipped IZOAFeatureSetHudService. Requires a UIDocument on the same GameObject, loads the UXML for the active style, and references no editor type.

Surface

Authoring assets

ContentPackDefinition

asset

The authoring asset for one installable pack: a manifest plus cover art, tagline, and the editor-only action list. Assets > Create > ZOA > Content > Content Pack Definition.

  • ContentPackManifest Manifest { get; }
  • Texture2D Cover { get; }
  • string Tagline { get; }
  • IReadOnlyList<IContentPackAction> Actions { get; }

FeatureSetDefinition

asset

A curated themed bundle naming one content pack per dimension. Assets > Create > ZOA > Content > Feature Set Definition.

  • string ThemeKey { get; }
  • Texture2D Cover { get; }
  • string TaglineShort { get; }
  • string TaglineLong { get; }
  • HudStyleKind HudStyle { get; }
  • ContentPackDefinition UiTheme, Input, Weapons, AiRoster, Audio, Environment { get; }
  • ContentPackDefinition GenreBundle, Items, LootTables, Recipes, Loadout, DemoScene { get; }
  • IReadOnlyList<FeatureSetDefinition> DependsOn { get; }
  • IEnumerable<ContentPackDefinition> EnumerateDimensions()
  • int CoveredDimensionCount { get; }
  • const int TotalDimensionSlots = 12

ZOAHudComponentDefinition

asset

Describes one HUD panel: the gameplay system it reads, the binder that feeds it, its slot and default rect, and the USS class tokens its template must expose. Assets > Create > ZOA > UI > HUD Component Definition.

  • string ComponentId { get; }
  • string DisplayName { get; }
  • string GameplaySystem { get; }
  • string BinderTypeName { get; }
  • ZOAHudSlot PreferredSlot { get; }
  • string LayoutRegionId { get; }
  • Rect DefaultRect { get; }
  • bool EnabledByDefault { get; }
  • IReadOnlyList<string> RequiredClassTokens { get; }
  • IReadOnlyList<string> OptionalClassTokens { get; }

Usage

Examples

Building a manifest and validating its keyscsharp
using System.Collections.Generic;
using ZOA.Content.Core;

var manifest = new ContentPackManifest(new ContentPackId("zoa.weaponpack.scifi"))
{
    DisplayName = "Sci-Fi Weapon Pack",
    Version = "1.2.0",
    Author = "ZOA LLC",
    Description = "Plasma and railgun platforms with matching attachments.",
};

// Ids convert implicitly from string, so a literal is a valid id.
manifest.AddDependency("zoa.core.ammo");

var entry = new ContentAssetEntry(
    assetId: "plasma_rifle",
    assetPath: "Assets/ZOA/ContentPacks/zoa.weaponpack.scifi/PlasmaRifle.prefab",
    assetType: ContentAssetType.Prefab,
    tags: new List<string> { "weapon", "primary" });

// Generates "zoa_weaponpack_scifi/weapons/plasma_rifle": every segment is
// lowercased, dots become underscores, and invalid characters are dropped.
entry.AddressableKey = AddressableConvention.SuggestKeyWithGroup(
    entry, manifest.Id, group: "weapons");

if (!AddressableConvention.ValidateKey(entry.AddressableKey))
    throw new System.InvalidOperationException("Key failed convention check.");

manifest.AddAsset(entry);
GroupNameFor(manifest.Id) gives the Addressables group these entries belong in: one group per pack.
Registering packs and finding broken referencescsharp
using ZOA.Content.Core;

IContentPackRegistry registry = new ContentPackRegistry();

registry.PackRegistered += (packId, m) =>
    UnityEngine.Debug.Log($"Registered {m.DisplayName} ({packId}) with {m.Assets.Count} assets.");

// Register returns false rather than throwing when the id is taken.
if (!registry.Register(manifest))
    UnityEngine.Debug.LogWarning("A pack with that id is already registered.");

IContentDependencyResolver resolver = new ContentDependencyResolver();
var graph = resolver.BuildDependencyGraph(manifest);

foreach (var node in resolver.FindBrokenReferences(graph))
{
    foreach (var missing in node.MissingDependencies)
        UnityEngine.Debug.LogError($"{node.AssetId} is missing {missing}.");
}
Writing an install actioncsharp
using System;
using UnityEngine;
using ZOA.Content.Unity.Actions;

[Serializable]
public sealed class InstallFactionBannersAction : ContentPackActionBase
{
    [SerializeField] private Texture2D[] banners;
    [SerializeField] private string themeFolder = "Cyberpunk";

    public override string DisplayName => $"Install Faction Banners: {themeFolder}";

    public override string Description =>
        "Deploys faction banner textures into Assets/ZOA/Content/Banners/.";

    // Cheap existence check only: this drives the wizard's status dot and
    // runs on every repaint, so it must not do a full validation pass.
    public override bool IsInstalled =>
        ZOA.Content.Unity.Editor.Actions.ContentDeployerUtil
            .AreAllInstalled("Banners", themeFolder, banners);

    public override bool TryApply(out string reason)
    {
        try
        {
            // Idempotent by construction: the deployer skips anything already
            // at its canonical path, so re-applying is a fast no-op.
            return ZOA.Content.Unity.Editor.Actions.ContentDeployerUtil.DeployAll(
                "Banners", themeFolder, banners, out _, out reason);
        }
        catch (Exception e)
        {
            reason = FormatException(e);
            return false;
        }
    }
}
An exception escaping TryApply is caught by the orchestrator and treated as a non-fatal error, but returning a reason gives the browser something useful to show.
Applying a Feature Set and reading the reportcsharp
using UnityEditor;
using UnityEngine;
using ZOA.Content.Unity.Definitions;
using ZOA.Content.Unity.Editor.Browser;

var set = AssetDatabase.LoadAssetAtPath<FeatureSetDefinition>(
    "Assets/Samples/ZOA Content/0.1.0/Flagship Feature Sets/FeatureSet_Cyberpunk.asset");

Debug.Log($"{set.DisplayName}: {set.CoveredDimensionCount} of " +
          $"{FeatureSetDefinition.TotalDimensionSlots} dimensions covered.");

foreach (var result in ContentPackInstaller.ApplyFeatureSet(set))
{
    if (!result.Success)
        Debug.LogError($"{result.Pack} / {result.Action} failed: {result.Reason}");
    else if (result.WasAlreadyInstalled)
        Debug.Log($"{result.Pack} / {result.Action} was already installed.");
    else
        Debug.Log($"{result.Pack} / {result.Action} installed.");
}
Contributing a tab to the Content Browsercsharp
using System;
using UnityEditor;
using UnityEngine.UIElements;
using ZOA.Content.Unity.Editor.Browser;

[InitializeOnLoad]
internal static class TerrainProfilesTabBootstrap
{
    static TerrainProfilesTabBootstrap()
    {
        ContentBrowserTabRegistry.Register(new TerrainProfilesTabProvider());
    }
}

internal sealed class TerrainProfilesTabProvider : IContentBrowserTabProvider
{
    public string TabId => "terrain-profiles";
    public string DisplayName => "Terrain Profiles";
    public string GridTitle => "Available profiles";
    public string DetailTitle => "Profile detail";
    public string EmptyStateText => "No terrain profiles found in this project.";
    public int SortOrder => 400;
    public int ItemCount => _profiles.Count;

    public void Refresh() { /* rebuild _profiles from AssetDatabase */ }

    public void BuildGrid(VisualElement gridContainer, Action requestRepaint)
    {
        gridContainer.Clear();
        foreach (var profile in _profiles)
            gridContainer.Add(new Button(requestRepaint) { text = profile.name });
    }

    public void BuildDetail(VisualElement detailPanel, Action requestRepaint)
    {
        detailPanel.Clear();
        detailPanel.Add(new Label("Select a profile."));
    }

    private readonly System.Collections.Generic.List<UnityEngine.Object> _profiles = new();
}

Tooling

Editor tools

Content Browser

Tools > ZOA > Advanced > Generate > Content > Open Content Browser

The two-pane Feature Set browser: card grid on the left, per-dimension detail and install buttons on the right, plus any tabs other packages have registered. Also embedded in the Workbench Content module.

Import All Content (Bulk)

Tools > ZOA > Advanced > Generate > Content > Import All Content (Bulk)

Imports every ZOA-shipped Package Manager sample the browser needs, refreshes the asset database, and rewires the Feature Set action lists. Idempotent and safe to re-run.

Generate All Content

Tools > ZOA > Advanced > Generate > Content > Generate All Content

The full pipeline behind Startup Health: import samples, rewire actions, build foundation assets, apply every Feature Set, regenerate demo scenes and the demo catalogue, then verify generated scenes for missing script references.

Startup Health

Tools > ZOA > Advanced > Validate > Startup Health > Open

The default startup dashboard. Runs a non-destructive scan of content and project readiness and offers install and fix actions for what is missing. Toggle its auto-open under Advanced > Maintain > Startup Health.

Content Pack Wizard

Window > ZOA > Content Pack Wizard

Four-step manifest authoring: Identity, Assets, Dependencies, Review. Also available as a capability inside the Workbench Content module.

Feature Set HUD Composer

Tools > ZOA > Advanced > Build > UI > Feature Set HUD Composer

Visual HUD authoring against the same panel and binder contracts the runtime uses. Choose panels and drag their normalised layout regions with snapping.

Wire Flagship Feature Set Actions

Tools > ZOA > Advanced > Build > Content > Wire Flagship Feature Set Actions

Populates the [SerializeReference] action lists on the shipped content pack shells so an imported Feature Set can actually install. Force Rewire All under Advanced > Maintain > Content clears and rebuilds them.

Force Reimport All Samples

Tools > ZOA > Advanced > Maintain > Content > Force Reimport All Samples (Repair Corruption)

Re-pulls every sample from scratch when an import has gone wrong. Reset Auto-Import Flag and Reset First-Launch Flag sit alongside it for re-testing the first-run path.

Read this

Notes and caveats

See also