Pre-alphaFoundry is in development. Tools, APIs, and sample content may change.

Browse documentation
Shared infrastructurecom.zoa.foundation · v0.4.0

ZOA Foundation

Give authored content stable identities and shared primitives.

Foundation supplies DefinitionId, grid coordinates, and the Unity authoring bases used by Foundry packages. Its GameDatabase resolves authored definitions, while shared helpers cover asset paths, named physics layers, and prefab pooling.

Definition IDs remain stable when an asset moves or is renamed. Keep the shared definition separate from each item’s runtime state. The event bus and FoundryServiceRegistry live in Messaging.

Tooling

Editor tools

Rebuild GameDatabase

Tools > ZOA > Advanced > Define > Foundation > Data > Rebuild GameDatabase (Default Path)

Scans the project for DefinitionBase assets and writes the index to Assets/Resources/ZOA/GameDatabase.asset. A Choose Path variant targets a different location for test or mod databases. GameDatabaseBuilder.BuildOrUpdate is the headless entry point automation should call, since it suppresses dialogs and does not change the selection.

Regenerate Definition IDs (Selected)

Tools > ZOA > Advanced > Maintain > Foundation > Data

Assigns a fresh DefinitionId to each selected DefinitionBase asset, recorded through Undo. The menu item validates the selection, so it stays greyed out unless a definition is selected. Use it after duplicating an asset that should become a separate definition.

Repair Duplicate Definition IDs (Project-Wide)

Tools > ZOA > Advanced > Maintain > Foundation > Data

Scans every definition asset for colliding ids, keeps the first occurrence so existing references keep resolving, and regenerates the rest. This is the repair pass for assets produced by a copy-based deploy that preserved the source id alongside a new file GUID.

Migrate Legacy Asset Layout

Tools > ZOA > Advanced > Maintain > Migration

Moves content out of legacy ZOA folders into the canonical roots declared by ZoaAssetPaths. Idempotent and conservative: it moves rather than copies, so GUID bindings survive and existing serialised references keep resolving, then prunes the emptied folders.

Icon generation pipeline

ZOAIconGenerator renders square icon textures from 3D prefabs across Built-in, URP, and HDRP without a hard render-pipeline dependency, with orthographic framing by default for consistent sizing across items. ZOAIconPostProcessor crops to alpha, rescales to the item footprint, and captures a silhouette before background and effects are composited. Both are editor APIs; the Icon Studio window that drives them ships in Equipment.

ZOAFoundationSceneInstaller

The per-package scene installer other packages call during a scene regenerate sweep. It creates a ZOA Foundation Runtime host and installs a PrefabPoolSceneInstaller, optionally under a supplied parent and inside an undo group. Idempotent, so re-running reuses the existing host and component.

ProjectTagUtility

Registers missing tags into ProjectSettings/TagManager.asset through SerializedObject, so a generator can assign GameObject.tag without the author having to pre-create the tag by hand. Idempotent and editor-only.

FoundryGenerationSession

Shared batch-generation state used by Generate All passes. Reference-counted via an IDisposable scope, it coalesces AssetDatabase save and refresh requests to the end of the batch and tracks how many were deferred, so a multi-generator sweep does not pay a synchronous import per asset.

Declared ZOA dependencies (0)

No ZOA dependencies declared in the package manifest.

How it works

Concepts

Ids are normalised GUIDs, and normalisation is the validation

DefinitionId and InstanceId are readonly string-backed structs. Their constructors parse GUIDs and normalize valid values to 32 lowercase hexadecimal characters. Invalid input becomes the empty ID; check validity before using an ID in a lookup or save record.

That normalisation is also what makes the two ids cheap to compare and safe to serialise. Equality is ordinal string comparison on an already-canonical form, so a definition authored with braces and one authored without collide correctly. Both types implement IComparable, which gives tooling a stable sort order without inventing one.

DefinitionBase.EnsureId is called from OnBeforeSerialize and OnValidate, so any definition asset acquires an id the first time Unity touches it and keeps it thereafter. Regeneration never runs automatically: duplicates are reported, never silently repaired, because a duplicate id usually means an asset was copied and the correct fix depends on which copy the existing saves point at.

The GameDatabase replaces Resources.LoadAll

Discovering every definition at runtime by scanning Resources is slow, non-deterministic in ordering, and pulls the entire folder into the build whether or not it is referenced. GameDatabase is a single ScriptableObject holding an explicit list of DefinitionBase references, built in the editor and loaded from Resources/ZOA/GameDatabase. One Resources.Load call, one asset, and a list whose order is stable across machines.

DefinitionRegistry.Build turns that list into three indexes in one pass: by id, by exact concrete type, and by tag with case-insensitive keys. GetAll<T> has a fast path for sealed types that reads the exact-type bucket directly, and a general path that filters the authoritative all-list so a query for ItemDefinition also returns WeaponDefinition and ContainerItemDefinition. Because the general path filters the authoritative all-list, the returned order stays deterministic across machines.

Duplicate ids are settled at build time. DefinitionRegistryBuildOptions.Strict throws, which is the right default during development, and KeepFirst / KeepLast exist for shipped builds that should log and carry on. The registry is a plain C# object with no singleton accessor, so a composition root can own one, a test can construct one from a fixture database, and two of them can coexist for a mod sandbox.

Grid geometry models footprints

GridCoord is a row/column pair and GridSize is a width/height pair, both readonly structs with value equality. Orientation has exactly two states, Normal and Rotated, because ninety degrees is the only rotation a rectangular footprint needs: rotating twice returns the same footprint, so a four-state enum would encode a distinction the collision test cannot see.

The whole rotation model reduces to one extension method. GridSizeExtensions.Oriented swaps width and height when the orientation is Rotated and returns the size unchanged otherwise. Every consumer that has to ask "does this fit" calls Oriented once and then works in plain rectangle arithmetic, which is why the inventory package can test a 2x4 rifle against a grid without a special case for rotated items.

GridCoord also exposes X and Y aliases mapping to Row and Col. They exist for tooling that thinks in XY, and they map through the constructor's argument order rather than through a screen-space convention, so X is the row. Prefer Row/Col in new code; the aliases are a compatibility surface for older tooling.

Diagnostics ship silent by default

ZOADiagnosticsGate is a static gate holding a ZOAProductionMode and a ZOADebugCategory mask, and both default to the quiet end: Production and None. A build with no production manager present therefore emits nothing diagnostic, which is the correct failure mode for a shipped game. A RuntimeInitializeOnLoadMethod at SubsystemRegistration resets both before the first scene loads, so an editor session with domain reload disabled cannot leak a previous play session's Development gate into the next one.

ZOALog is the facade every diagnostic call site goes through. Diag, Info, and Warn check the gate before touching Debug, while Error and Exception always emit: a release build stays silent about tracing but still surfaces real faults. For per-frame call sites there is a Func<string> overload of Diag whose factory only runs when the category is enabled, so in production the string is never built at all.

The same category mask drives debug HUDs. A HUD implements IZOADebugComponent, declares which ZOADebugCategory it belongs to, and receives SetDiagnosticsActive when the gate moves. One control then silences per-frame network-sync tracing while weapon diagnostics keep reporting, with no per-system toggles to hunt down.

Pooling is a shared service keyed by prefab

IPrefabPoolService keys pools by prefab asset reference, so a decal prefab spawned from the weapon system, the AI system, and a VFX graph shares one instance budget instead of fragmenting into three. GetOrCreate is first-call-wins on capacity: the second caller's initialCapacity and maxSize arguments are ignored, which means a scene installer that cares about the bounds must pre-register the pool before consumers reach for it.

Overshoot is bounded. Spawns beyond maxSize still succeed, but their despawn destroys the instance instead of returning it, so a runaway emitter costs frames rather than leaking memory permanently. The service also tracks an instance-to-pool map, so Despawn resolves the right pool without the caller remembering which one spawned what; an unknown instance is destroyed and logged.

IPooledLifecycle exists because Awake runs once per instance but a pooled object spawns many times, and OnEnable conflates reuse with Unity's own enable semantics. OnSpawnedFromPool fires after the instance is activated, positioned, and parented, so transform values are final. OnReturnedToPool fires before deactivation, so a handler can still walk the scene tree to release references.

One canonical layout for generated assets

ZoaAssetPaths splits generated content into two buckets with different rules. Assets/Resources/ZOA/ holds anything that must be reachable through Resources.Load and is addressed at runtime by the matching ResourcesLoadPrefix. Assets/ZOA/Generated/ holds everything referenced directly through a serialised field: prefabs, scenes, materials, definitions. A third root, Assets/ZOA/Bundled/, is user-owned art that generators read but never write.

Routing every generator through those constants keeps the layout predictable enough to migrate later. ZoaAssetLayoutMigrator moves content out of legacy folders into the canonical roots using AssetDatabase.MoveAsset, so GUID bindings survive and existing serialised references keep resolving. ZoaGeneratedFolderGuard runs on load and repairs the "Generated 1" sibling folders that appear when AssetDatabase.CreateFolder is called for a directory that exists on disk but is not yet imported.

Setup

Workflow

  1. 01

    Derive your definitions from DefinitionBase

    Any ScriptableObject that needs to survive a save round trip should extend DefinitionBase rather than ScriptableObject. It brings the stable id, the display-name fallback, and the tag list the registry indexes. Override OnValidate if you need your own validation, and call base.OnValidate() first so the id stays populated.

  2. 02

    Build the GameDatabase

    Run Tools > ZOA > Advanced > Define > Foundation > Data > Rebuild GameDatabase (Default Path). The builder scans the project for DefinitionBase assets and writes the index to Assets/Resources/ZOA/GameDatabase.asset. Re-run it after authoring new definitions; content-pack installers call GameDatabaseBuilder.BuildOrUpdate directly so the rebuild happens without dialogs or selection changes.

  3. 03

    Build a registry at your composition root

    Call DefinitionRegistry.BuildFromResources() once during bootstrap and hold the result, or Build(database) if you loaded the asset yourself. Register it wherever your project resolves services. Consumers should take IDefinitionRegistry rather than reaching for GameDatabase.Instance, so a test can substitute a fixture database.

  4. 04

    Install the pool service in scenes that spawn

    Add a PrefabPoolSceneInstaller to your scene runtime root, or call ZOAFoundationSceneInstaller.InstallOrUpdateInActiveScene from a generator. The installer runs at -9700 so the service is registered before consumers Awake. Warm up the pools you know the budget for at install time, because GetOrCreate is first-call-wins on capacity.

  5. 05

    Route logging through ZOALog

    Replace diagnostic Debug.Log calls with ZOALog.Diag and a category, keep Debug.LogError semantics on ZOALog.Error, and guard any message that costs real work with ZOALog.IsEnabled or the Func<string> overload. A build with no production manager then ships silent by construction.

  6. 06

    Write generated assets through ZoaAssetPaths

    Generators should compose output paths from ZoaAssetPaths constants and create folders with ZoaAssetPathUtil.EnsureFolder rather than AssetDatabase.CreateFolder, which avoids the numbered-sibling folder problem. Projects upgrading from an older layout can run Tools > ZOA > Advanced > Maintain > Migration > Migrate Legacy Asset Layout, which moves assets and preserves GUID bindings.

Reference

Key types

DefinitionId

struct

Stable identifier for an authoring-time definition asset. Persisted in save files and used as the registry key, so it must outlive asset renames and re-imports.

  • readonly string Value
  • static DefinitionId Empty
  • static DefinitionId New()
  • static bool TryParse(string value, out DefinitionId id)
  • bool IsEmpty

InstanceId

struct

Stable identifier for a runtime instance: an item, a placed stack, a weapon instance, an attachment node, a world container. Same normalisation rules as DefinitionId, and a distinct type so the two cannot be swapped at a call site.

  • readonly string Value
  • static InstanceId Empty
  • static InstanceId New()
  • static bool TryParse(string value, out InstanceId id)

IDefinitionRegistry

interface

Runtime resolution of definition assets by id, type, or tag. A plain C# object with no singleton accessor, so a composition root owns one and a test can build another.

  • bool Contains(DefinitionId id)
  • bool TryGet<T>(DefinitionId id, out T definition) where T : DefinitionBase
  • T Get<T>(DefinitionId id) where T : DefinitionBase
  • IReadOnlyList<T> GetAll<T>() where T : DefinitionBase
  • IReadOnlyList<DefinitionBase> GetByTag(string tag)

DefinitionRegistry

class

The shipped registry implementation. Builds id, exact-type, and tag indexes in a single pass over the database and reports duplicate ids according to the configured policy.

  • static DefinitionRegistry Build(GameDatabase database, DefinitionRegistryBuildOptions? options = null)
  • static DefinitionRegistry BuildFromResources(string resourcesPath = GameDatabasePaths.DefaultResourcesPath, DefinitionRegistryBuildOptions? options = null, bool throwIfMissing = true)

DuplicateDefinitionIdPolicy

enum

Throw, KeepFirst, or KeepLast. Strict build options throw, which is the right default during development because a duplicate id means two assets are competing for the same save-file reference.

GridCoord

struct

Row/column coordinate in a row-major grid. Carries an explicit Invalid sentinel of (-1, -1) so a failed placement search returns a coordinate rather than a nullable.

  • readonly int Row
  • readonly int Col
  • static GridCoord Invalid
  • bool IsValid

GridSize

struct

Width/height footprint in cells. Pair it with GridSizeExtensions.Oriented to get the effective footprint under a given orientation.

  • readonly int Width
  • readonly int Height
  • bool IsValid
  • GridSize Oriented(this GridSize size, Orientation orientation)

Orientation

enum

Normal or Rotated, backed by a byte. Two states because a rectangle rotated twice is the same rectangle, so a four-way enum would encode a difference no fit test can observe.

InventoryCapabilities

enum

Flags enum declaring what an inventory supports: Stacking, Rotation, Weight, Reservations, EquipmentSlots, SizeChange. It lives in Foundation rather than in Inventory because shape definitions, equipment slots, and UI all need to read it.

DefinitionBase

class

Abstract ScriptableObject base for every authored definition in the product. Owns the stable id, an optional display-name override that falls back to the asset name, and the tag list the registry indexes.

  • DefinitionId Id
  • string IdString
  • string DisplayName
  • IReadOnlyList<string> Tags
  • void EnsureId()

ZOALog

class

The gated logging facade. Diagnostic levels consult ZOADiagnosticsGate before emitting; errors and exceptions always emit. Use IsEnabled or the Func<string> overload to keep hot paths free of wasted string building.

  • static void Diag(ZOADebugCategory category, string message, Object context = null)
  • static void Diag(ZOADebugCategory category, Func<string> message, Object context = null)
  • static void Info(string message, Object context = null)
  • static void Warn(ZOADebugCategory category, string message, Object context = null)
  • static void Error(string message, Object context = null)
  • static bool IsEnabled(ZOADebugCategory category)

ZOADiagnosticsGate

class

Process-wide diagnostics authority. Holds the current mode and category mask, raises Changed when either moves, and resets to Production/None at SubsystemRegistration so a stale editor session cannot leak into the next play.

  • static ZOAProductionMode Mode
  • static ZOADebugCategory EnabledCategories
  • static bool IsEnabled(ZOADebugCategory category)
  • static void Apply(ZOAProductionMode mode, ZOADebugCategory enabledCategories)
  • static event Action Changed

ZOADebugCategory

enum

Flags mask of independently toggleable diagnostic channels: General, NetworkSync, PlayerRig, Weapon, Ai, Inventory, Overlay, Performance. Gates both log call sites and debug HUDs, so one toggle silences a subsystem end to end.

IZOADebugComponent

interface

Implemented by every debug HUD so the production manager can discover and gate it without referencing each concrete type. Implementers must also self-check the gate in OnEnable, because a HUD can spawn after the gate was applied.

  • ZOADebugCategory DebugCategory
  • string DebugLabel
  • void SetDiagnosticsActive(bool active)

IPrefabPoolService

service

Project-wide pool registry keyed by prefab reference. Registered into FoundryServiceRegistry by PrefabPoolSceneInstaller and resolved from anywhere that spawns at high frequency.

  • IPrefabPool GetOrCreate(GameObject prefab, Transform poolParent = null, int initialCapacity = 8, int maxSize = 256)
  • GameObject Spawn(GameObject prefab, Vector3 position, Quaternion rotation, Transform parent = null)
  • void Despawn(GameObject instance)
  • void Warmup(GameObject prefab, int count)
  • bool HasPool(GameObject prefab)

IPrefabPool

interface

A pool for one prefab. Spawn activates, positions, parents, and fires the pooled lifecycle hook; Despawn is idempotent on unknown, null, or already-returned instances.

  • GameObject Prefab
  • int InstanceCount
  • int AvailableCount
  • GameObject Spawn(Vector3 position, Quaternion rotation, Transform parent = null)
  • void Despawn(GameObject instance)
  • void Warmup(int count)

IPooledLifecycle

interface

Opt-in reuse hooks for components on pooled prefabs. Implement it when Awake-once semantics are wrong and the object needs a genuine per-spawn reset.

  • void OnSpawnedFromPool()
  • void OnReturnedToPool()

PrefabPoolSceneInstaller

component

Constructs a PrefabPoolService on Awake at execution order -9700 and registers it, deferring to any service that registered first. Pool containers parent under this component, so scene unload disposes every pooled instance with no cleanup pass.

  • IPrefabPoolService Service

ZOAInputSuppression

class

Reference-counted gate for gameplay input while modal UI is open. Owners are tracked in a set so two overlapping panels cannot restore input while the other is still showing; ForceReleaseAll exists for scene transitions where the owning UI was destroyed mid-suppression.

  • static bool IsSuppressed
  • static void SetSuppressed(object owner, bool suppressed)
  • static void ForceReleaseAll()
  • static event Action<bool> SuppressionStateChanged

ZOACursorCapture

class

Reference-counted cursor unlock for UI over a first-person camera. Saving of the pre-capture cursor state is deferred to the first LateUpdate tick, so a controller that locks the cursor in Start does not get the UI's unlocked state recorded as the value to restore.

  • static bool IsCaptured
  • static void Acquire(UnityEngine.Object owner)
  • static void Release(UnityEngine.Object owner)
  • static event Action<bool> CaptureChanged

ZOAInputActionRegistry

class

Runtime roster of notable InputActions with a category, label, and description. Components register on enable and unregister on disable, so the key-bindings viewer renders a live list instead of a hand-maintained one.

  • static void Register(UnityEngine.Object owner, InputAction action, string category, string displayName, string description = null)
  • static void UnregisterOwner(UnityEngine.Object owner)
  • static IReadOnlyList<Entry> Entries
  • static event Action RegistryChanged

ZOAInputHelper

class

Resolves an InputActionProperty that may hold a reference, an inline action, or nothing, creating a default only in the last case. Components use it so they work in a bare scene without an authored InputActionAsset.

  • static InputAction ResolveAction(ref InputActionProperty property, string actionName, string defaultBinding)
  • static InputAction ResolveAction(ref InputActionProperty property, Func<InputAction> createDefault)
  • static bool WasTriggered(InputAction action)

ZOALayers

class

Physics-layer names resolved by name and cached, never by hardcoded index. Separates the locomotion capsule from per-bone hitboxes and ragdoll colliders so a hitscan lands on the mesh rather than on the fat movement capsule.

  • const string Player / Hitbox / Ragdoll / FirstPersonWeapon / FirstPersonArms / UI
  • static int PlayerLayer, HitboxLayer, RagdollLayer
  • static int WeaponHitscanMask
  • static int ExcludeLocomotion(int mask)
  • static void SetLayerRecursively(GameObject root, int layer)

ZoaAssetPaths

class

The canonical on-disk layout as compile-time constants. Resources-addressable content under Assets/Resources/ZOA, direct-referenced generated content under Assets/ZOA/Generated, and user-owned source art under Assets/ZOA/Bundled.

  • const string ResourcesRoot / ResourcesLoadPrefix
  • const string GeneratedRoot / BundledRoot
  • static class Resources { GameDatabaseAsset, GameDatabaseLoad, ... }
  • static class Generated { Definitions, Prefabs, Scenes, Materials, Players, ... }

FoundrySectionAttribute

class

Marks the start of a collapsible inspector section. The attributed field and every field after it in declaration order render in one group until the next marker; collapsed state persists across domain reloads. A pure metadata marker, rendered by the Workbench inspector.

  • FoundrySectionAttribute(string title, bool expandedByDefault = true, int order = 0, string icon = null, string tooltip = null)

IGenreBundleRegistry

interface

Resolves genre bundles by DefinitionId or by the human bundle-id string. Foundation declares and implements it but never publishes it; the workbench or runtime host owns registration.

  • bool TryGet(DefinitionId id, out GenreBundleDefinition bundle)
  • bool TryGetByBundleId(string bundleId, out GenreBundleDefinition bundle)
  • IReadOnlyList<GenreBundleDefinition> GetAll()
  • GenreBundleDefinition Default

GenreBundleValidation

class

Pure-function validator over a bundle plus a resolver callback that answers whether an id currently resolves. It distinguishes Pending from broken, so an id authored ahead of the asset it points at reports as Pending instead of failing validation.

  • static IReadOnlyList<GenreBundleIssue> Validate(GenreBundleDefinition bundle, Func<string, bool> idResolver)

ZOADiagnosticHudLayout

class

Position and size persistence for developer HUD windows, written to a dedicated JSON file under persistentDataPath. It lives here rather than in Persistence because Persistence already depends on Equipment and Inventory to capture their state, and hosting HUD layout there would close the cycle.

  • static bool TryGetRect(string id, out Rect rect)
  • static bool TryGetPosition(string id, out Vector2 pos)
  • static void SetRect(string id, Rect rect)
  • static void SetPosition(string id, Vector2 pos)

Reference

Authoring assets

GameDatabase

asset

The built index of every DefinitionBase asset the runtime needs. One asset, one Resources.Load, deterministic ordering. Create via Tools > ZOA > Foundation > Game Database, or let the rebuild tool author it at the default path.

  • static GameDatabase Instance
  • IReadOnlyList<DefinitionBase> Definitions
  • int Count
  • List<DefinitionBase> GetNonNullDefinitions()

GenreBundleDefinition

asset

Groups the content that makes a project feel like a genre: weapon pack, UI theme, input binding set, starter loadout, starter loot table, scene look, HUD style. Every reference is a plain string id rather than an object reference, so a bundle stays serialisable without forcing the referenced packs to be imported first.

  • string BundleId
  • string WeaponPackId, UiThemeId, InputBindingSetId
  • string StarterItemPaletteId, StarterLoadoutId, StarterLootTableId, StarterObjectiveId
  • GenreHudStyle HudStyleDefault
  • Color SceneAmbientColor, bool SceneFogEnabled

ContentPackInstallMarker

asset

A project-local receipt written when a content pack is installed, recording pack id, version, tags, and a UTC ISO-8601 install timestamp. It extends ScriptableObject, not DefinitionBase, so it never enters the GameDatabase or competes for a DefinitionId.

  • string PackId
  • string PackName
  • string Version
  • IReadOnlyList<string> Tags
  • string InstalledAtUtcIso8601

Usage

Examples

Resolving definitions by id and by tagcsharp
using ZOA.Foundation.Core.Ids;
using ZOA.Foundation.Unity.Database;

// Once, during bootstrap. Throws if no GameDatabase has been built,
// which is the right time to find out.
IDefinitionRegistry registry = DefinitionRegistry.BuildFromResources();

// By id: the form a save file stores.
if (DefinitionId.TryParse(savedId, out var id) &&
    registry.TryGet<MyItemDefinition>(id, out var def))
{
    Spawn(def);
}

// By tag: case-insensitive, order matches database iteration order.
foreach (var quest in registry.GetByTag("quest-item"))
    Register(quest);
GetAll<T> includes derived types, so a query for a base definition type returns its subclasses too.
Testing a rotated footprintcsharp
using ZOA.Foundation.Core.Grid;

// A 2x4 rifle in a 5x3 grid does not fit upright, but does fit rotated.
var footprint = new GridSize(2, 4);
var rotated = footprint.Oriented(Orientation.Rotated);   // 4x2

bool FitsAt(GridCoord origin, GridSize size, int columns, int rows)
    => origin.IsValid
       && origin.Col + size.Width  <= columns
       && origin.Row + size.Height <= rows;

FitsAt(new GridCoord(0, 0), footprint, columns: 5, rows: 3); // false
FitsAt(new GridCoord(0, 0), rotated,   columns: 5, rows: 3); // true
Orientation never mutates the definition's authored footprint. It is applied at the point of the fit test and stored alongside the placement.
Gated logging in a hot pathcsharp
using ZOA.Foundation.Unity.Diagnostics;

// Emits nothing, and builds no string, unless the NetworkSync
// category is enabled on the gate.
ZOALog.Diag(ZOADebugCategory.NetworkSync,
    () => $"tick {tick} applied {deltas.Count} deltas for {ownerId}");

// Real faults are never gated, even in a production build.
if (payload == null)
    ZOALog.Error("[NetSync] Null payload on an applied delta.", this);

// Turning categories on from a bootstrapper or a debug console:
ZOADiagnosticsGate.Apply(
    ZOAProductionMode.Development,
    ZOADebugCategory.Weapon | ZOADebugCategory.Inventory);
Spawning through the pool servicecsharp
using UnityEngine;
using ZOA.Foundation.Unity.Pooling;
using ZOA.Messaging;

public sealed class ImpactDecals : MonoBehaviour
{
    [SerializeField] private GameObject decalPrefab;

    private IPrefabPoolService _pools;

    private void Start()
    {
        // The scene installer runs at -9700, so the service exists by Start.
        FoundryServiceRegistry.TryResolve(out _pools);

        // Pre-register with an explicit budget: GetOrCreate is
        // first-call-wins, so a later caller cannot widen the pool.
        _pools?.GetOrCreate(decalPrefab, initialCapacity: 32, maxSize: 128);
    }

    public void Place(Vector3 point, Quaternion facing)
    {
        var decal = _pools?.Spawn(decalPrefab, point, facing);
        if (decal != null) StartCoroutine(ReturnAfter(decal, 8f));
    }
}
A component on the decal prefab can implement IPooledLifecycle to reset its fade timer on every spawn, which OnEnable alone cannot express.
A definition with collapsible inspector sectionscsharp
using UnityEngine;
using ZOA.Foundation.Unity.Attributes;
using ZOA.Foundation.Unity.Definitions;

[CreateAssetMenu(menuName = "Tools/MyGame/Consumable")]
public sealed class ConsumableDefinition : DefinitionBase
{
    [FoundrySection("Effect", order: 10, icon: "physics",
        tooltip: "What the item does when used.")]
    [Tooltip("Health restored on use, in hit points.")]
    [SerializeField] private int healthRestored = 25;

    [Tooltip("Seconds before the effect can be triggered again.")]
    [SerializeField] private float cooldownSeconds = 1f;

    [FoundrySection("Presentation", order: 20, icon: "ui")]
    [SerializeField] private AudioClip useSound;

    public int HealthRestored => healthRestored;
}
Fields declared before the first FoundrySection land in an implicit General group. Grouping runs to the next marker, so section boundaries are declaration order, not nesting.

Integration

Behavior to account for

See also