first comit
This commit is contained in:
3
Runtime/AssemblyInfo.cs
Normal file
3
Runtime/AssemblyInfo.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Jovian.TagSystem.Editor"), InternalsVisibleTo("Jovian.TagSystem.Tests")]
|
||||
2
Runtime/AssemblyInfo.cs.meta
Normal file
2
Runtime/AssemblyInfo.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 925b98fc2eb4d8f49900afdee32991a1
|
||||
14
Runtime/Jovian.TagSystem.asmdef
Normal file
14
Runtime/Jovian.TagSystem.asmdef
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "Jovian.TagSystem",
|
||||
"rootNamespace": "Jovian.TagSystem",
|
||||
"references": [],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
7
Runtime/Jovian.TagSystem.asmdef.meta
Normal file
7
Runtime/Jovian.TagSystem.asmdef.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f71bff532e6b8e24a8fc3e2761205890
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
85
Runtime/JovianTag.cs
Normal file
85
Runtime/JovianTag.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.TagSystem {
|
||||
/// <summary>
|
||||
/// Lightweight tag identity. 8 bytes, no heap allocation.
|
||||
/// Hierarchy queries are resolved via GameTagManager static lookups.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public struct JovianTag : IEquatable<JovianTag>, IComparable<JovianTag> {
|
||||
[SerializeField] private int id;
|
||||
[SerializeField] private int parentId;
|
||||
|
||||
public int Id => id;
|
||||
public int ParentId => parentId;
|
||||
|
||||
public JovianTag(int id, int parentId) {
|
||||
this.id = id;
|
||||
this.parentId = parentId;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly bool IsNone() => id == 0;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly bool IsValid() => id > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if this tag is a descendant of the given ancestor.
|
||||
/// Walks the parent chain via GameTagManager.
|
||||
/// </summary>
|
||||
public readonly bool IsDescendantOf(JovianTag ancestor) {
|
||||
if(id == 0 || ancestor.id == 0) return false;
|
||||
if(id == ancestor.id) return true;
|
||||
|
||||
// Walk up from this tag's parent chain
|
||||
var current = this;
|
||||
while(current.parentId != 0) {
|
||||
if(current.parentId == ancestor.id) return true;
|
||||
current = JovianTagsHandler.GetTag(current.parentId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if this tag is an ancestor of the given descendant.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly bool IsAncestorOf(JovianTag descendant) {
|
||||
return descendant.IsDescendantOf(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if this tag shares the same parent as the given tag.
|
||||
/// </summary>
|
||||
public readonly bool IsSiblingTo(JovianTag sibling) {
|
||||
if(id == 0 || sibling.id == 0) return false;
|
||||
return parentId == sibling.parentId;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool operator ==(JovianTag x, JovianTag y) => x.id == y.id;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool operator !=(JovianTag x, JovianTag y) => x.id != y.id;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly bool Equals(JovianTag other) => id == other.id;
|
||||
|
||||
public readonly int CompareTo(JovianTag other) => id.CompareTo(other.id);
|
||||
|
||||
public override readonly bool Equals(object obj) => obj is JovianTag other && id == other.id;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override readonly int GetHashCode() => id;
|
||||
|
||||
public override readonly string ToString() {
|
||||
if(JovianTagsHandler.IsInitialized) {
|
||||
return JovianTagsHandler.TagToString(this);
|
||||
}
|
||||
return id == 0 ? "None" : $"Tag({id})";
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Runtime/JovianTag.cs.meta
Normal file
2
Runtime/JovianTag.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2f7edc6a4a42107499ed2b46654d63f3
|
||||
163
Runtime/JovianTagsContainer.cs
Normal file
163
Runtime/JovianTagsContainer.cs
Normal file
@@ -0,0 +1,163 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Jovian.TagSystem {
|
||||
|
||||
/// <summary>
|
||||
/// Entry in a JovianTagContainer — pairs a tag with an optional typed value.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public struct TagEntry<T> {
|
||||
public JovianTag Tag;
|
||||
public T Value;
|
||||
|
||||
public TagEntry(JovianTag tag, T value) {
|
||||
Tag = tag;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public TagEntry(JovianTag tag) {
|
||||
Tag = tag;
|
||||
Value = default;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly bool Is(JovianTag other) => Tag.Equals(other);
|
||||
public readonly bool IsDescendantOf(JovianTag ancestor) => Tag.IsDescendantOf(ancestor);
|
||||
public readonly bool IsAncestorOf(JovianTag descendant) => Tag.IsAncestorOf(descendant);
|
||||
public readonly bool IsSiblingTo(JovianTag sibling) => Tag.IsSiblingTo(sibling);
|
||||
public readonly bool IsValid() => Tag.IsValid();
|
||||
|
||||
public readonly override string ToString() => $"{Tag}: {Value}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generic tag container — holds tag+value pairs with hierarchy-aware queries.
|
||||
/// For tags-only, use non-generic JovianTagContainer.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class JovianTagsContainer<T> {
|
||||
public readonly List<TagEntry<T>> entries;
|
||||
|
||||
public JovianTagsContainer(int capacity) {
|
||||
entries = new List<TagEntry<T>>(capacity);
|
||||
}
|
||||
|
||||
public int Count => entries.Count;
|
||||
|
||||
public void Add(JovianTag tag, T value) {
|
||||
for(int i = 0, n = entries.Count; i < n; i++) {
|
||||
if(entries[i].Tag.Id == tag.Id) return; // no duplicates
|
||||
}
|
||||
entries.Add(new TagEntry<T>(tag, value));
|
||||
}
|
||||
|
||||
public bool Remove(JovianTag tag) {
|
||||
for(int i = entries.Count - 1; i >= 0; i--) {
|
||||
if(entries[i].Tag.Id == tag.Id) {
|
||||
entries.RemoveAt(i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Clear() => entries.Clear();
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool Contains(JovianTag tag) {
|
||||
for(int i = 0, n = entries.Count; i < n; i++) {
|
||||
if(entries[i].Tag.Id == tag.Id) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryGetValue(JovianTag tag, out T value) {
|
||||
for(int i = 0, n = entries.Count; i < n; i++) {
|
||||
if(entries[i].Tag.Id == tag.Id) {
|
||||
value = entries[i].Value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool ContainsDescendantOf(JovianTag ancestor) {
|
||||
for(int i = 0, n = entries.Count; i < n; i++) {
|
||||
if(entries[i].Tag.IsDescendantOf(ancestor)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool ContainsAncestorOf(JovianTag descendant) {
|
||||
for(int i = 0, n = entries.Count; i < n; i++) {
|
||||
if(entries[i].Tag.IsAncestorOf(descendant)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool ContainsSibling(JovianTag sibling) {
|
||||
for(int i = 0, n = entries.Count; i < n; i++) {
|
||||
if(entries[i].Tag.IsSiblingTo(sibling)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void GetByAncestor(JovianTag ancestor, List<TagEntry<T>> results) {
|
||||
for(int i = 0, n = entries.Count; i < n; i++) {
|
||||
if(entries[i].Tag.IsDescendantOf(ancestor)) results.Add(entries[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public void GetByDescendant(JovianTag descendant, List<TagEntry<T>> results) {
|
||||
for(int i = 0, n = entries.Count; i < n; i++) {
|
||||
if(entries[i].Tag.IsAncestorOf(descendant)) results.Add(entries[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public void GetBySibling(JovianTag sibling, List<TagEntry<T>> results) {
|
||||
for(int i = 0, n = entries.Count; i < n; i++) {
|
||||
if(entries[i].Tag.IsSiblingTo(sibling)) results.Add(entries[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly StringBuilder sb = new(256);
|
||||
|
||||
public override string ToString() {
|
||||
sb.Clear();
|
||||
for(int i = 0, n = entries.Count; i < n; i++) {
|
||||
if(i > 0) sb.Append(" | ");
|
||||
sb.Append(entries[i].ToString());
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sentinel type for tag-only containers (no payload).
|
||||
/// </summary>
|
||||
public struct NoValue { }
|
||||
|
||||
/// <summary>
|
||||
/// Non-generic tag container — tags only, no data payload.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class JovianTagsContainer : JovianTagsContainer<NoValue> {
|
||||
private static readonly JovianTagsContainer empty = new(0);
|
||||
public static JovianTagsContainer Empty => empty;
|
||||
|
||||
public JovianTagsContainer(int capacity) : base(capacity) { }
|
||||
|
||||
public void Add(JovianTag tag) => Add(tag, default);
|
||||
|
||||
/// <summary>
|
||||
/// Access tags directly for backwards compatibility.
|
||||
/// </summary>
|
||||
public JovianTag this[int index] => entries[index].Tag;
|
||||
|
||||
public JovianTag GetTag(int index) => entries[index].Tag;
|
||||
}
|
||||
}
|
||||
2
Runtime/JovianTagsContainer.cs.meta
Normal file
2
Runtime/JovianTagsContainer.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cb368346014a3644995688d0a325abcd
|
||||
102
Runtime/JovianTagsGroup.cs
Normal file
102
Runtime/JovianTagsGroup.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.TagSystem {
|
||||
/// <summary>
|
||||
/// Serializable tag selection for use in MonoBehaviours and ScriptableObjects.
|
||||
/// Always supports multiple tags. Use the property drawer to select tags in the inspector.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public struct JovianTagsGroup {
|
||||
[SerializeField] public string[] tags;
|
||||
|
||||
public JovianTagsGroup(params string[] tags) {
|
||||
this.tags = tags ?? Array.Empty<string>();
|
||||
}
|
||||
|
||||
public int Count => tags?.Length ?? 0;
|
||||
|
||||
/// <summary>
|
||||
/// Returns all selected tags as resolved GameTags.
|
||||
/// Stale/unregistered tag names are silently skipped.
|
||||
/// </summary>
|
||||
public JovianTagsContainer ToContainer() {
|
||||
if(tags == null || tags.Length == 0) {
|
||||
return JovianTagsContainer.Empty;
|
||||
}
|
||||
var container = new JovianTagsContainer(tags.Length);
|
||||
foreach(var tag in tags) {
|
||||
if(JovianTagsHandler.TryGetGameTagThatIsNotNone(tag, out var resolved)) {
|
||||
container.Add(resolved);
|
||||
}
|
||||
}
|
||||
return container;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if any selected tag matches the given tag exactly.
|
||||
/// Stale/unregistered tag names count as no match (no error log).
|
||||
/// </summary>
|
||||
public bool Contains(JovianTag jovianTag) {
|
||||
if(tags == null) return false;
|
||||
foreach(var tag in tags) {
|
||||
if(JovianTagsHandler.TryGetGameTag(tag, out var resolved)
|
||||
&& resolved.Equals(jovianTag)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if any selected tag is a descendant of the given ancestor.
|
||||
/// </summary>
|
||||
public bool ContainsDescendantOf(JovianTag ancestor) {
|
||||
if(tags == null) return false;
|
||||
foreach(var tag in tags) {
|
||||
if(JovianTagsHandler.TryGetGameTag(tag, out var resolved)
|
||||
&& resolved.IsDescendantOf(ancestor)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if any selected tag is an ancestor of the given descendant.
|
||||
/// </summary>
|
||||
public bool ContainsAncestorOf(JovianTag descendant) {
|
||||
if(tags == null) return false;
|
||||
foreach(var tag in tags) {
|
||||
if(JovianTagsHandler.TryGetGameTag(tag, out var resolved)
|
||||
&& resolved.IsAncestorOf(descendant)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if any selected tag is a sibling of the given tag.
|
||||
/// </summary>
|
||||
public bool ContainsSiblingOf(JovianTag sibling) {
|
||||
if(tags == null) return false;
|
||||
foreach(var tag in tags) {
|
||||
if(JovianTagsHandler.TryGetGameTag(tag, out var resolved)
|
||||
&& resolved.IsSiblingTo(sibling)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool HasAny() {
|
||||
return tags != null && tags.Length > 0;
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
if(tags == null || tags.Length == 0) return "None";
|
||||
return string.Join(", ", tags);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Runtime/JovianTagsGroup.cs.meta
Normal file
2
Runtime/JovianTagsGroup.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 140c6dc4e1c289e48b7441ac6ee5e20f
|
||||
151
Runtime/JovianTagsHandler.cs
Normal file
151
Runtime/JovianTagsHandler.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using JetBrains.Annotations;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.TagSystem {
|
||||
|
||||
[Serializable]
|
||||
public struct RegisteredTag : IEquatable<RegisteredTag> {
|
||||
public string tag;
|
||||
|
||||
public RegisteredTag(string tag) {
|
||||
this.tag = tag;
|
||||
}
|
||||
|
||||
public bool Equals(RegisteredTag other) => tag == other.tag;
|
||||
public override bool Equals([CanBeNull] object obj) => obj is RegisteredTag other && Equals(other);
|
||||
public override int GetHashCode() => tag != null ? tag.GetHashCode() : 0;
|
||||
}
|
||||
|
||||
public static class JovianTagsHandler {
|
||||
public const char tagDelimiter = '.';
|
||||
public const string emptyTagName = "None";
|
||||
public const int emptyTagId = 0;
|
||||
public static readonly JovianTag emptyTag = new(emptyTagId, 0);
|
||||
|
||||
// Primary lookups — no allocations on query
|
||||
private static Dictionary<string, JovianTag> tagsByName = new();
|
||||
private static Dictionary<int, JovianTag> tagsById = new();
|
||||
private static Dictionary<int, string> tagNames = new();
|
||||
private static int idCounter;
|
||||
private static bool initialized;
|
||||
|
||||
public static bool IsInitialized => initialized;
|
||||
|
||||
public static void Initialize() {
|
||||
tagsByName = new Dictionary<string, JovianTag>(64) { { emptyTagName, emptyTag } };
|
||||
tagsById = new Dictionary<int, JovianTag>(64) { { emptyTagId, emptyTag } };
|
||||
tagNames = new Dictionary<int, string>(64) { { emptyTagId, emptyTagName } };
|
||||
idCounter = 0;
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
public static void EnsureInitialized() {
|
||||
if(!initialized) Initialize();
|
||||
}
|
||||
|
||||
public static void Reset() {
|
||||
tagsByName = new Dictionary<string, JovianTag>();
|
||||
tagsById = new Dictionary<int, JovianTag>();
|
||||
tagNames = new Dictionary<int, string>();
|
||||
idCounter = 0;
|
||||
initialized = false;
|
||||
}
|
||||
|
||||
public static void RegisterTags(RegisteredTag[] serializedTags) {
|
||||
EnsureInitialized();
|
||||
for(int i = 0, n = serializedTags.Length; i < n; i++) {
|
||||
RegisterTag(serializedTags[i].tag);
|
||||
}
|
||||
}
|
||||
|
||||
public static void RegisterTags(string[] tagNames) {
|
||||
EnsureInitialized();
|
||||
for(int i = 0, n = tagNames.Length; i < n; i++) {
|
||||
RegisterTag(tagNames[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public static void RegisterTag(string tagToRegister) {
|
||||
EnsureInitialized();
|
||||
|
||||
// Walk segments without allocating a string[] — use ReadOnlySpan
|
||||
var span = tagToRegister.AsSpan();
|
||||
int parentId = 0;
|
||||
|
||||
for(int i = 0; i <= span.Length; i++) {
|
||||
if(i < span.Length && span[i] != tagDelimiter) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// span[segStart..i] is the current segment
|
||||
// Full tag name is tagToRegister[0..i]
|
||||
var fullName = tagToRegister.Substring(0, i);
|
||||
|
||||
if(!tagsByName.TryGetValue(fullName, out var tag)) {
|
||||
idCounter++;
|
||||
tag = new JovianTag(idCounter, parentId);
|
||||
tagsByName[fullName] = tag;
|
||||
tagsById[idCounter] = tag;
|
||||
tagNames[idCounter] = fullName;
|
||||
}
|
||||
|
||||
parentId = tag.Id;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static JovianTag GetTag(string tagName) {
|
||||
if(string.IsNullOrEmpty(tagName)) {
|
||||
return emptyTag;
|
||||
}
|
||||
if(tagsByName.TryGetValue(tagName, out var tag)) {
|
||||
return tag;
|
||||
}
|
||||
Debug.LogError($"[TagManager] Trying to get unregistered Tag: {tagName}");
|
||||
return emptyTag;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static JovianTag GetTag(int id) {
|
||||
return tagsById.GetValueOrDefault(id, emptyTag);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool TryGetGameTag(string tagName, out JovianTag tag) {
|
||||
tag = emptyTag;
|
||||
return !string.IsNullOrEmpty(tagName) && tagsByName.TryGetValue(tagName, out tag);
|
||||
}
|
||||
|
||||
public static bool TryGetGameTagThatIsNotNone(string tagName, out JovianTag tag) {
|
||||
if(!TryGetGameTag(tagName, out tag)) {
|
||||
return false;
|
||||
}
|
||||
return tag.Id != emptyTagId;
|
||||
}
|
||||
|
||||
public static void GetAllTags(List<JovianTag> results) {
|
||||
foreach(var kvp in tagsByName) {
|
||||
if(kvp.Value.Id != emptyTagId)
|
||||
results.Add(kvp.Value);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static string TagToString(JovianTag jovianTag) {
|
||||
return tagNames.TryGetValue(jovianTag.Id, out var text) ? text : string.Empty;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static string TagToString(int tagId) {
|
||||
return tagNames.TryGetValue(tagId, out var text) ? text : string.Empty;
|
||||
}
|
||||
|
||||
public static string DisplayName(string name) {
|
||||
var lastDot = name.LastIndexOf(tagDelimiter);
|
||||
return lastDot < 0 ? name : name.Substring(lastDot + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Runtime/JovianTagsHandler.cs.meta
Normal file
2
Runtime/JovianTagsHandler.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e700d92d5f8326b4aacff3563881ed6f
|
||||
90
Runtime/JovianTagsSettings.cs
Normal file
90
Runtime/JovianTagsSettings.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace Jovian.TagSystem {
|
||||
[CreateAssetMenu(menuName = "Jovian/Game Tag System/Game Tag Settings")]
|
||||
public class JovianTagsSettings : ScriptableObject {
|
||||
public RegisteredTag[] gameTags = Array.Empty<RegisteredTag>();
|
||||
|
||||
private static readonly System.Text.RegularExpressions.Regex ValidSegment =
|
||||
new(@"^[A-Za-z][A-Za-z0-9_]*$");
|
||||
|
||||
private static readonly HashSet<string> CSharpKeywords = new(StringComparer.Ordinal) {
|
||||
"abstract", "as", "base", "bool", "break", "byte", "case", "catch", "char",
|
||||
"checked", "class", "const", "continue", "decimal", "default", "delegate", "do",
|
||||
"double", "else", "enum", "event", "explicit", "extern", "false", "finally",
|
||||
"fixed", "float", "for", "foreach", "goto", "if", "implicit", "in", "int",
|
||||
"interface", "internal", "is", "lock", "long", "namespace", "new", "null",
|
||||
"object", "operator", "out", "override", "params", "private", "protected",
|
||||
"public", "readonly", "ref", "return", "sbyte", "sealed", "short", "sizeof",
|
||||
"stackalloc", "static", "string", "struct", "switch", "this", "throw", "true",
|
||||
"try", "typeof", "uint", "ulong", "unchecked", "unsafe", "ushort", "using",
|
||||
"virtual", "void", "volatile", "while"
|
||||
};
|
||||
|
||||
public void AddGameTag(string newTag) {
|
||||
List<string> tagHierarchy = newTag.Split(JovianTagsHandler.tagDelimiter).ToList();
|
||||
tagHierarchy.Remove("");
|
||||
newTag = string.Join(JovianTagsHandler.tagDelimiter, tagHierarchy);
|
||||
|
||||
// Validate each segment
|
||||
foreach(var segment in tagHierarchy) {
|
||||
if(!ValidSegment.IsMatch(segment)) {
|
||||
Debug.LogError($"Invalid tag segment '{segment}': must start with a letter and contain only letters, digits, or underscores.");
|
||||
return;
|
||||
}
|
||||
if(CSharpKeywords.Contains(segment)) {
|
||||
Debug.LogError($"Invalid tag segment '{segment}': is a C# reserved keyword.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(gameTags.Contains(new(newTag))) {
|
||||
Debug.LogError($"{newTag} is already added to the game");
|
||||
return;
|
||||
}
|
||||
|
||||
string[] tagsSplit = newTag.Split(JovianTagsHandler.tagDelimiter);
|
||||
string tagToAdd = "";
|
||||
for(int i = 0, n = tagsSplit.Length; i < n; i++) {
|
||||
tagToAdd += tagsSplit[i];
|
||||
if(gameTags.Any((a) => a.tag == tagToAdd)) {
|
||||
tagToAdd += ".";
|
||||
continue;
|
||||
}
|
||||
|
||||
var previous = gameTags;
|
||||
gameTags = new RegisteredTag[gameTags.Length + 1];
|
||||
previous.CopyTo(gameTags, 0);
|
||||
gameTags[^1] = new RegisteredTag(tagToAdd);
|
||||
tagToAdd += ".";
|
||||
}
|
||||
#if UNITY_EDITOR
|
||||
UnityEditor.EditorUtility.SetDirty(this);
|
||||
#endif
|
||||
}
|
||||
|
||||
private static bool IsValidSegment(string segment) {
|
||||
return !string.IsNullOrWhiteSpace(segment)
|
||||
&& ValidSegment.IsMatch(segment)
|
||||
&& !CSharpKeywords.Contains(segment);
|
||||
}
|
||||
|
||||
private static bool IsValidTag(string tag) {
|
||||
if(string.IsNullOrWhiteSpace(tag)) return false;
|
||||
return tag.Split(JovianTagsHandler.tagDelimiter).All(IsValidSegment);
|
||||
}
|
||||
|
||||
public void RemoveTag(string gameTagToRemove) {
|
||||
var tagSearch = gameTags.ToList();
|
||||
tagSearch.RemoveAll((a) => a.tag.StartsWith(gameTagToRemove));
|
||||
gameTags = tagSearch.ToArray();
|
||||
#if UNITY_EDITOR
|
||||
UnityEditor.EditorUtility.SetDirty(this);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Runtime/JovianTagsSettings.cs.meta
Normal file
2
Runtime/JovianTagsSettings.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 32e658688044a8f469e0c311f9c4facb
|
||||
Reference in New Issue
Block a user