ZOA Foundation
Stable ids, the definition database, grid geometry, pooling, and the diagnostics gate every other package compiles against.
Foundation is the second root of the graph. Messaging owns the bus and the service registry; Foundation owns the vocabulary. If two packages need to agree on what an id is, how a rectangular footprint rotates, where a generated asset gets written, or whether a diagnostic log is allowed to emit, the agreement is declared here and nowhere else. The package description says "do not add features here", and the rule holds: every type in Foundation is either a primitive, a registry, or a cross-cutting gate, and anything with gameplay behaviour belongs downstream.
The package splits into two assemblies with a hard line between them. `ZOA.Foundation.Core` is compiled with `noEngineReferences`, so it holds no `UnityEngine` types at all: ids, grid coordinates, and the inventory capability flags. Everything a save file or a headless test needs to reason about lives on that side of the line, so the inventory algorithms run under plain NUnit. `ZOA.Foundation.Unity` adds the ScriptableObject bases, the database, pooling, input helpers, and the diagnostics facade, and it is the assembly every other Foundry package references.
The recurring design move is to name a thing once and resolve it by that name forever. A `DefinitionId` is a normalised GUID, not an asset path or an array index, so a save file written today still resolves after the asset is moved, renamed, or re-imported. A physics layer is resolved by name through `ZOALayers`, never by a hardcoded index. A generated asset path is a constant on `ZoaAssetPaths`, never a string literal in a generator. Each of those choices removes a class of bug that only shows up months later, in someone else's project.
Depends on (0)
Depended on by (41)
How it works
Concepts
Ids are normalised GUIDs, and normalisation is the validation
`DefinitionId` and `InstanceId` are readonly structs wrapping a single string, and the constructor is the only way in. It runs the value through `Guid.TryParse` and re-emits it in `"N"` format, lowercase: 32 hex digits, no braces, no hyphens. Anything that fails to parse becomes the empty id rather than being stored as-is, so an id that survives construction is either valid or obviously empty. There is no third state where a half-legible string silently propagates into a save file.
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.
In the editor
Screens
Screenshot pending
/screenshots/foundation-game-database-inspector.png
The Project window showing Assets/Resources/ZOA/GameDatabase.asset selected, and the Inspector listing the indexed definitions with the count visible, so the one-asset-index model reads at a glance.
Screenshot pending
/screenshots/foundation-foundry-section-inspector.png
An Inspector for a component or definition using [FoundrySection], with two or three named collapsible groups, one expanded and one collapsed, and per-field tooltips visible in the expanded group.
Screenshot pending
/screenshots/foundation-definition-id-tools-menu.png
The Tools > ZOA > Advanced > Maintain > Foundation > Data submenu open, showing Regenerate Definition IDs (Selected) and Repair Duplicate Definition IDs (Project-Wide), with a definition asset selected in the Project window so the first item is enabled.
Setup
Workflow
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
Surface
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)
Surface
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
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);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); // trueusing 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);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));
}
}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;
}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.
Read this
Notes and caveats
See also