# Jovian Tag System A hierarchical, strongly-typed tag system for Unity. Define tags as dot-delimited hierarchies (`Damage.Fire.AOE`), auto-generate type-safe C# constants, and query relationships like ancestor/descendant/sibling at runtime with zero allocations. ## Table of Contents - [Installation](#installation) - [Quick Start](#quick-start) - [**Important: Tags Are Not Inherited**](#important-tags-are-not-inherited) - [Hierarchical Tags](#hierarchical-tags) - [Tag Editor Window](#tag-editor-window) - [Using Tags in Code](#using-tags-in-code) - [Inspector Integration](#inspector-integration) - [Tag Picker Popup](#tag-picker-popup) - [Code Generation](#code-generation) - [Tag Containers](#tag-containers) - [Tag Containers with Data](#tag-containers-with-data) - [Runtime API Reference](#runtime-api-reference) - [Performance](#performance) ## Quick Start ### 1. Create a Tag Settings asset Right-click in the Project window > `Create` > `Jovian` > `Tag System` > `Tag Settings` ### 2. Add tags Open the Tag Editor via `Jovian` > `Tag System` > `Tag Editor...` Add tags like: ``` Character Character.Player Character.Enemy Character.Enemy.Boss Damage Damage.Fire Damage.Ice Status Status.Burning Status.Frozen ``` ### 3. Click Save & Generate This creates a `GameTags.cs` file with strongly-typed constants: ```csharp // Auto-generated public static partial class GameTags { public static class Character { public static JovianTag Character_Tag; public static class Enemy { public static JovianTag Enemy_Tag; public static JovianTag Boss_Tag; } public static JovianTag Player_Tag; } public static class Damage { public static JovianTag Damage_Tag; public static JovianTag Fire_Tag; public static JovianTag Ice_Tag; } } ``` ### 4. Use in code ```csharp if (projectile.IsDescendantOf(GameTags.Damage.Damage_Tag)) { // This is any damage type (Fire, Ice, etc.) } ``` --- ## Important: Tags Are Not Inherited > **TL;DR — Tagging an object with `Enemy.Name.Butcher.Size.Large` does NOT automatically assign `Enemy.Name.Butcher` or `Enemy` to that object.** Each dot-delimited path is a **distinct, independent tag**. The hierarchy is **structural** — it enables tree queries — but it does **not** imply membership. ### Concrete Example Register this tag in the Tag Editor: ``` Enemy.Name.Butcher.Size.Large ``` The system creates **5 separate tag definitions** (one per segment), parented in the tree: - `Enemy` - `Enemy.Name` - `Enemy.Name.Butcher` - `Enemy.Name.Butcher.Size` - `Enemy.Name.Butcher.Size.Large` Now tag a game object with `Enemy.Name.Butcher.Size.Large`. On that object, **only that one tag is set.** The ancestor tags exist as definitions, but they are not active on the object. ### What This Means in Practice | You want to match... | Do this | |----------------------|---------| | Only Large Butcher | Tag object with `Enemy.Name.Butcher.Size.Large`, query with `Contains` | | Any Butcher (any size) via separate tag | Add a second tag `Enemy.Name.Butcher` to the object | | Any Butcher (any size) via hierarchy | Tag with `Enemy.Name.Butcher.Size.Large`, query with `ContainsDescendantOf(GameTags.Enemy.Name.Butcher_Tag)` | ### Exact vs Hierarchy Queries | Query | Behavior | |-------|----------| | `Contains(tag)` | **Exact match only** — matches the literal tag you assigned | | `ContainsDescendantOf(tag)` | Matches any descendant of `tag`, including `tag` itself | | `ContainsAncestorOf(tag)` | Matches any ancestor of `tag` | **The tag itself does not decide matching behavior — the query does.** Choose the query type based on your intent: `Contains` for "this exact thing," `ContainsDescendantOf` for "anything in this category." --- ## Hierarchical Tags Tags are organized in a tree using dot-delimited names. Each segment creates a level in the hierarchy. ``` Damage ← root tag (depth 0) ├── Damage.Fire ← child of Damage (depth 1) │ └── Damage.Fire.AOE ← child of Fire (depth 2) ├── Damage.Ice ← child of Damage (depth 1) └── Damage.Lightning ← child of Damage (depth 1) ``` ### Hierarchy Queries Every tag knows its parent. This enables three types of queries: **IsDescendantOf** — "Is this tag a child/grandchild/etc. of another?" ```csharp var fire = GameTags.Damage.Fire_Tag; var damage = GameTags.Damage.Damage_Tag; var aoe = GameTags.Damage.Fire.AOE_Tag; fire.IsDescendantOf(damage); // true — Fire is under Damage aoe.IsDescendantOf(damage); // true — AOE is under Damage (via Fire) aoe.IsDescendantOf(fire); // true — AOE is directly under Fire damage.IsDescendantOf(fire); // false — Damage is above Fire fire.IsDescendantOf(fire); // true — a tag is a descendant of itself ``` **IsAncestorOf** — "Is this tag a parent/grandparent/etc. of another?" ```csharp damage.IsAncestorOf(fire); // true damage.IsAncestorOf(aoe); // true fire.IsAncestorOf(damage); // false ``` **IsSiblingTo** — "Do these tags share the same parent?" ```csharp var fire = GameTags.Damage.Fire_Tag; var ice = GameTags.Damage.Ice_Tag; var player = GameTags.Character.Player_Tag; fire.IsSiblingTo(ice); // true — both under Damage fire.IsSiblingTo(player); // false — different parents ``` ### Practical Use Cases **Category matching** — check if something belongs to a broad category: ```csharp // Does this entity have ANY damage tag? if (entity.tags.ContainsDescendantOf(GameTags.Damage.Damage_Tag)) { ApplyDamageEffect(); } ``` **Specific matching** — check for an exact tag: ```csharp if (entity.tags.Contains(GameTags.Damage.Fire_Tag)) { ApplyBurnEffect(); } ``` **Resistance system** — use hierarchy for type matching: ```csharp // Entity is immune to all fire damage (Fire, Fire.AOE, etc.) if (incomingDamage.IsDescendantOf(GameTags.Damage.Fire_Tag) && entity.HasResistance(GameTags.Damage.Fire_Tag)) { return 0; } ``` ## Tag Editor Window Open via `Jovian` > `Tag System` > `Tag Editor...` ### Features - **Hierarchical tree view** — tags displayed as an expandable/collapsible tree - **Add root tags** — type a name in the "New Tag" field at the bottom and click "+ Add" - **Add child tags** — click the green **+** button on any tag to add a child under it - **Delete tags** — click the red **✕** button. If the tag has children, you'll be asked to confirm deletion of the entire branch - **Search** — filter tags by name - **Save & Generate** — saves all changes and regenerates the C# constants file - **Vertical indent guides** — visual lines showing parent-child relationships ### Adding hierarchical tags You can type full paths in the "New Tag" field: ``` Damage.Fire.AOE ``` This automatically creates `Damage` and `Damage.Fire` as parents if they don't exist. Or use the **+** button on an existing tag to add a child — a popup asks for just the child name. ### Validation Tags must: - Start with a letter - Contain only letters, digits, or underscores - Not be a C# reserved keyword (`class`, `int`, `static`, etc.) Invalid names are rejected with an error message. Duplicates are detected and shown as warnings. ## Using Tags in Code ### Generated Constants After clicking **Save & Generate**, use the generated constants: ```csharp using Jovian.TagSystem; public class Projectile : MonoBehaviour { public void OnHit(JovianTag targetTag) { if (targetTag.IsDescendantOf(GameTags.Character.Enemy.Enemy_Tag)) { DealDamage(); } } } ``` ### JovianTag Struct The core type — 8 bytes, zero heap allocation: ```csharp JovianTag tag = GameTags.Damage.Fire_Tag; tag.IsValid(); // true (not the empty/None tag) tag.IsNone(); // false tag.Id; // unique int identifier tag.ParentId; // parent's int identifier (0 = root) tag.ToString(); // "Damage.Fire" (when manager is initialized) tag.IsDescendantOf(otherTag); // hierarchy query tag.IsAncestorOf(otherTag); // hierarchy query tag.IsSiblingTo(otherTag); // same parent check tag == otherTag; // equality by ID ``` ## Inspector Integration ### JovianTagsGroup Use `JovianTagsGroup` on any MonoBehaviour or ScriptableObject to select tags in the inspector: ```csharp public class Enemy : MonoBehaviour { public JovianTagsGroup tags; private void Start() { // Check tags at runtime if (tags.ContainsDescendantOf(GameTags.Character.Enemy.Enemy_Tag)) { Debug.Log("This is an enemy!"); } // Convert to a runtime container for efficient repeated queries var container = tags.ToContainer(); } } ``` ### Inspector Display - **Collapsible** — shows `Tags (3)` when collapsed, full list when expanded - **Tag list** — each tag shown with a red ✕ remove button - **+ Add Tags** button — opens the tag picker popup - **Edit Tags** button — opens the Tag Editor window ## Tag Picker Popup The picker popup opens when you click **+ Add Tags** in the inspector. Features: - **Hierarchical tree with checkboxes** — check/uncheck any tag at any depth - **Search** — filter by name - **+all / -all buttons** — bulk select/deselect all children of a parent tag - **Already-selected tags** are pre-checked when the popup opens - **Confirm / Cancel** — apply or discard the selection - **Edit Tags** button — jump to the Tag Editor window ## Code Generation ### How It Works The Tag Editor generates a C# file with: 1. **Nested static classes** mirroring the tag hierarchy 2. **Static `JovianTag` fields** for each tag (postfixed with `_Tag`) 3. **`[RuntimeInitializeOnLoadMethod]`** that registers all tags and resolves the fields at startup 4. **`[InitializeOnLoadMethod]`** (editor-only) for editor play mode ### Manual Edits You can add fields by hand to the generated file. They will be preserved on regeneration as long as the file compiles. ### Regenerate Without the Window Use `Jovian` > `Tag System` > `Regenerate Tag Constants` to regenerate from the menu without opening the Tag Editor. ### Settings Inspector The `JovianTagsSettings` ScriptableObject inspector has: - Default array editor for direct tag editing - **Save & Generate Tags** button - **Open Tag Editor** button - Validation errors shown inline (invalid names, duplicates) ## Tag Containers ### JovianTagsContainer (tags only) A runtime collection for holding and querying multiple tags: ```csharp var container = new JovianTagsContainer(4); container.Add(GameTags.Damage.Fire_Tag); container.Add(GameTags.Status.Burning_Tag); container.Contains(GameTags.Damage.Fire_Tag); // true container.ContainsDescendantOf(GameTags.Damage.Damage_Tag); // true container.ContainsAncestorOf(GameTags.Damage.Fire.AOE_Tag); // true container.ContainsSibling(GameTags.Damage.Ice_Tag); // true (Fire and Ice are siblings) container.Count; // 2 container.Remove(GameTags.Damage.Fire_Tag); container.Clear(); ``` ### From JovianTagsGroup Convert a serialized selection to a runtime container: ```csharp public JovianTagsGroup selectedTags; void Start() { var container = selectedTags.ToContainer(); // Use container for efficient queries } ``` ## Tag Containers with Data ### JovianTagsContainer\ (tags + values) Pair tags with typed data — like a dictionary with hierarchy-aware queries: ```csharp // Damage resistances var resistances = new JovianTagsContainer(4); resistances.Add(GameTags.Damage.Fire_Tag, 0.5f); // 50% fire resistance resistances.Add(GameTags.Damage.Ice_Tag, 0.75f); // 75% ice resistance // Exact lookup if (resistances.TryGetValue(GameTags.Damage.Fire_Tag, out float fireRes)) { Debug.Log($"Fire resistance: {fireRes}"); // 0.5 } // Hierarchy query — do I resist ANY damage type? resistances.ContainsDescendantOf(GameTags.Damage.Damage_Tag); // true // Get all resistances under a parent var results = new List>(); resistances.GetByAncestor(GameTags.Damage.Damage_Tag, results); // results contains Fire(0.5) and Ice(0.75) ``` ### TagEntry\ Each entry in a generic container: ```csharp TagEntry entry = ...; entry.Tag; // the JovianTag entry.Value; // the float value entry.IsDescendantOf(someAncestor); // hierarchy query on the tag entry.Is(someTag); // exact match ``` ## Runtime API Reference ### JovianTagsHandler (static) The central tag registry. Initialized automatically by the generated code. | Method | Description | |--------|-------------| | `Initialize()` | Reset and initialize the registry | | `RegisterTag(string)` | Register a dot-delimited tag and all parents | | `GetTag(string)` | Get tag by full name | | `GetTag(int)` | Get tag by ID | | `TryGetGameTag(string, out JovianTag)` | Safe lookup by name | | `TagToString(JovianTag)` | Get the full name of a tag | | `DisplayName(string)` | Get the last segment (`"Damage.Fire"` → `"Fire"`) | | `IsInitialized` | Whether the registry is ready | ### JovianTag (struct, 8 bytes) | Member | Description | |--------|-------------| | `Id` | Unique integer identifier | | `ParentId` | Parent's ID (0 = root) | | `IsValid()` | Not the empty tag | | `IsNone()` | Is the empty tag | | `IsDescendantOf(tag)` | Hierarchy: child/grandchild check | | `IsAncestorOf(tag)` | Hierarchy: parent/grandparent check | | `IsSiblingTo(tag)` | Same parent check | | `==`, `!=`, `Equals()` | Equality by ID | ### JovianTagsGroup (serializable struct) | Member | Description | |--------|-------------| | `tags` | `string[]` of tag names (serialized) | | `ToContainer()` | Resolve to `JovianTagsContainer` | | `Contains(tag)` | Check if any tag matches | | `ContainsDescendantOf(tag)` | Hierarchy query | | `ContainsAncestorOf(tag)` | Hierarchy query | | `HasAny()` | True if any tags selected | ## Performance - **JovianTag** — 8 bytes (`int id` + `int parentId`), no heap allocation - **Hierarchy queries** — O(depth) parent chain walk via `JovianTagsHandler.GetTag(int)` dictionary lookup. Typical depth is 1-4 hops. - **Equality** — O(1) integer comparison - **Container queries** — O(n) linear scan, optimal for typical small tag counts (<10 per entity) - **ToString** — uses cached static `StringBuilder` to avoid per-call allocation - **Registration** — uses `ReadOnlySpan` for segment parsing to minimize string allocations