using System; using System.Collections.Generic; using UnityEngine.Localization.Metadata; using UnityEngine.Serialization; namespace UnityEngine.Localization.Tables { /// /// The SharedTableData holds data that is accessible across all tables. /// It is responsible for the localization keys and associating the keys to unique ids. /// Each collection of tables will reference a single SharedTableData asset. /// public class SharedTableData : ScriptableObject, ISerializationCallbackReceiver { /// /// A entry in the SharedTableData. Contains the unique id, the name of the key and optional Metadata. /// [Serializable] public class SharedTableEntry { [SerializeField] long m_Id; [SerializeField] string m_Key; [SerializeField] MetadataCollection m_Metadata = new MetadataCollection(); /// /// Unique id(to this SharedTableData). /// public long Id { get => m_Id; internal set => m_Id = value; } /// /// The name of the key, must also be unique. /// public string Key { get => m_Key; internal set => m_Key = value; } /// /// Optional Metadata for this key that is also shared between all tables that use this . /// public MetadataCollection Metadata { get => m_Metadata; set => m_Metadata = value; } public override string ToString() => $"{Id} - {Key}"; } /// /// Represents an empty or null Key Id. /// public const long EmptyId = 0; internal const string NewEntryKey = "New Entry"; [FormerlySerializedAs("m_TableName")] [SerializeField] string m_TableCollectionName; [FormerlySerializedAs("m_TableNameGuidString")] [SerializeField] string m_TableCollectionNameGuidString; [SerializeField] List m_Entries = new List(); [SerializeField] [MetadataType(MetadataType.SharedTableData)] MetadataCollection m_Metadata = new MetadataCollection(); [SerializeReference] IKeyGenerator m_KeyGenerator = new DistributedUIDGenerator(); Guid m_TableCollectionNameGuid; // Used for fast lookup. Only generated when required. Dictionary m_IdDictionary = new Dictionary(); Dictionary m_KeyDictionary = new Dictionary(); /// /// All entries. /// public List Entries { get => m_Entries; set { m_Entries = value; m_IdDictionary.Clear(); m_KeyDictionary.Clear(); } } /// /// Clear all entries in this table. /// public void Clear() { m_Entries.Clear(); m_IdDictionary.Clear(); m_KeyDictionary.Clear(); } /// /// The name of this table collection. /// All that use this SharedTableData will have this name. /// public string TableCollectionName { get => m_TableCollectionName; set => m_TableCollectionName = value; } /// /// A unique Id that will never change. Comes from the SharedTableData asset Guid. /// Provides a way to reference a table that will not be broken if the table collection name was to be changed. /// public Guid TableCollectionNameGuid { get => m_TableCollectionNameGuid; internal set => m_TableCollectionNameGuid = value; } /// /// Metadata that is shared between all tables. /// public MetadataCollection Metadata { get => m_Metadata; set => m_Metadata = value; } /// /// The Key Generator to use when adding new entries. /// By default this will use . /// /// /// This example shows how the could be configured to use a . /// /// public IKeyGenerator KeyGenerator { get => m_KeyGenerator; set => m_KeyGenerator = value; } /// /// Get the key associated with the id. /// /// Id the key belongs to. /// The found key or null if one can not be found. public string GetKey(long id) { var foundPair = FindWithId(id); return foundPair?.Key; } /// /// Get the unique Id for the key name from the shared table data. /// /// The key whose id is being requested. /// The keys id value or if one does not exist. public long GetId(string key) { var foundPair = FindWithKey(key); return foundPair?.Id ?? 0; } /// /// Get the unique Id for the key name, if one does not exist then a new entry is added. /// /// /// /// Id of the Key that exists else the Id of the newly created Key if addNewKey is True or EmptyId. public long GetId(string key, bool addNewKey) { var foundPair = FindWithKey(key); var foundId = EmptyId; if (foundPair != null) { foundId = foundPair.Id; } else if (addNewKey) { foundId = AddKeyInternal(key).Id; } return foundId; } /// /// Returns the Entry for the key id, this contains all data for the key. /// /// Reference to the entry. /// The found key entry or null if one can not be found. public SharedTableEntry GetEntryFromReference(TableEntryReference tableEntryReference) { if (tableEntryReference.ReferenceType == TableEntryReference.Type.Name) return GetEntry(tableEntryReference.Key); return GetEntry(tableEntryReference.KeyId); } /// /// Returns the Entry for the key id, this contains all data for the key. /// /// Id the key belongs to. /// The found key entry or null if one can not be found. public SharedTableEntry GetEntry(long id) { return FindWithId(id); } /// /// Returns the Entry for the key, this contains all data for the key. /// /// The name of the key. /// The found key entry or null if one can not be found. public SharedTableEntry GetEntry(string key) { return FindWithKey(key); } /// /// Is the Id value used by any entries in this SharedTableData? /// /// Id to check. /// public bool Contains(long id) => FindWithId(id) != null; /// /// Is the key value used by any entries in this SharedTableData? /// /// Key to check. /// public bool Contains(string key) => FindWithKey(key) != null; /// /// Adds a new key to this SharedTableData if one does not already exists with the same id. /// /// The unique key name to assign to the entry. /// The unique id to assign to the key. /// The new entry or null if an entry already exists with the same id. public SharedTableEntry AddKey(string key, long id) { return !Contains(id) ? AddKeyInternal(key, id) : null; } /// /// Adds a new key to this SharedTableData with a default name. If the name already exists then a unique version is generated based on the provided name. /// /// The name for the new Key. /// public SharedTableEntry AddKey(string key = null) { var newKeyName = string.IsNullOrEmpty(key) ? NewEntryKey : key; SharedTableEntry entry = null; int counter = 1; var keyToTry = newKeyName; while (entry == null) { if (Contains(keyToTry)) { keyToTry = $"{newKeyName} {counter++}"; } else { entry = AddKeyInternal(keyToTry); } } return entry; } /// /// Attempts to remove the key with provided id. /// /// public void RemoveKey(long id) { var foundEntry = FindWithId(id); if (foundEntry != null) RemoveKeyInternal(foundEntry); } /// /// Attempts to remove the key from this SharedTableData. /// /// The key to be removed. public void RemoveKey(string key) { var foundEntry = FindWithKey(key); if (foundEntry != null) RemoveKeyInternal(foundEntry); } /// /// Rename the key value for the provided id if it exists. /// /// /// public void RenameKey(long id, string newValue) { var foundEntry = FindWithId(id); if (foundEntry != null) RenameKeyInternal(foundEntry, newValue); } /// /// Rename the key value if it exists. /// /// /// public void RenameKey(string oldValue, string newValue) { var foundEntry = FindWithKey(oldValue); if (foundEntry != null) RenameKeyInternal(foundEntry, newValue); } /// /// Attempts to change the Id of an entry. /// /// The current Id that should be changed. Must exist. /// The new Id to use. Must not already be in use. /// True is the Id was changed successfully. public bool RemapId(long currentId, long newId) { // Is the new id in use? if (FindWithId(newId) != null) return false; var foundEntry = FindWithId(currentId); if (foundEntry == null) return false; foundEntry.Id = newId; m_IdDictionary.Remove(currentId); m_IdDictionary[newId] = foundEntry; return true; } /// /// Returns the that is the most similar to the text. /// Uses the Levenshtein distance method. /// /// The text to match against. /// The number of edits needed to turn into the returned , 0 being an exact match. /// The that is the most similar to the text or null if one could not be found. [Obsolete("FindSimilarKey will be removed in the future, please use Unity Search. See TableEntrySearchData class for further details.")] public SharedTableEntry FindSimilarKey(string text, out int distance) { SharedTableEntry foundEntry = null; distance = int.MaxValue; foreach (var entry in Entries) { var d = ComputeLevenshteinDistance(text.ToLower(), entry.Key.ToLower()); if (d < distance) { foundEntry = entry; distance = d; } } return foundEntry; } #pragma warning disable CA1814 // CA1814 Prefer jagged arrays over multidimensional /// /// Compute the distance between two strings. /// /// /// /// The number of edits needed to turn one string into another. static int ComputeLevenshteinDistance(string a, string b) { // Based on https://www.dotnetperls.com/levenshtein int n = a.Length; int m = b.Length; int[,] d = new int[n + 1, m + 1]; // Step 1 if (n == 0) return m; if (m == 0) return n; // Step 2 for (int i = 0; i <= n; d[i, 0] = i++) {} for (int j = 0; j <= m; d[0, j] = j++) {} // Step 3 for (int i = 1; i <= n; i++) { //Step 4 for (int j = 1; j <= m; j++) { // Step 5 int cost = (b[j - 1] == a[i - 1]) ? 0 : 1; // Step 6 d[i, j] = Mathf.Min(Mathf.Min(d[i - 1, j] + 1, d[i, j - 1] + 1), d[i - 1, j - 1] + cost); } } // Step 7 return d[n, m]; } #pragma warning restore CA1814 SharedTableEntry AddKeyInternal(string key) { var newEntry = new SharedTableEntry() { Id = m_KeyGenerator.GetNextKey(), Key = key }; // Its possible that the Id already exists, such as when a custom entry was added by a user. // We need to to make sure this value is unique, if it is not then we keep trying until we find a unique Id. while (FindWithId(newEntry.Id) != null) { newEntry.Id = m_KeyGenerator.GetNextKey(); } Entries.Add(newEntry); if (m_IdDictionary.Count > 0) m_IdDictionary[newEntry.Id] = newEntry; if (m_KeyDictionary.Count > 0) m_KeyDictionary[key] = newEntry; return newEntry; } SharedTableEntry AddKeyInternal(string key, long id) { var newEntry = new SharedTableEntry() { Id = id, Key = key }; Entries.Add(newEntry); if (m_IdDictionary.Count > 0) m_IdDictionary[newEntry.Id] = newEntry; if (m_KeyDictionary.Count > 0) m_KeyDictionary[key] = newEntry; return newEntry; } void RenameKeyInternal(SharedTableEntry entry, string newValue) { if (m_KeyDictionary.Count > 0) { m_KeyDictionary.Remove(entry.Key); m_KeyDictionary[newValue] = entry; } entry.Key = newValue; } void RemoveKeyInternal(SharedTableEntry entry) { if (m_KeyDictionary.Count > 0) m_KeyDictionary.Remove(entry.Key); if (m_IdDictionary.Count > 0) m_IdDictionary.Remove(entry.Id); Entries.Remove(entry); } SharedTableEntry FindWithId(long id) { if (id == EmptyId) return null; if (m_IdDictionary.Count == 0) { foreach (var keyAndIdPair in m_Entries) { m_IdDictionary[keyAndIdPair.Id] = keyAndIdPair; } } m_IdDictionary.TryGetValue(id, out var foundPair); return foundPair; } SharedTableEntry FindWithKey(string key) { if (m_KeyDictionary.Count == 0) { foreach (var keyAndIdPair in m_Entries) { m_KeyDictionary[keyAndIdPair.Key] = keyAndIdPair; } } m_KeyDictionary.TryGetValue(key, out var foundPair); return foundPair; } public override string ToString() => $"{TableCollectionName}(Shared Table Data)"; /// /// Converts the Guid into a serializable string. /// public void OnBeforeSerialize() { m_TableCollectionNameGuidString = TableReference.StringFromGuid(m_TableCollectionNameGuid); } /// /// Converts the serializable string into a Guid. /// public void OnAfterDeserialize() { m_IdDictionary.Clear(); m_KeyDictionary.Clear(); m_TableCollectionNameGuid = string.IsNullOrEmpty(m_TableCollectionNameGuidString) ? Guid.Empty : Guid.Parse(m_TableCollectionNameGuidString); #if UNITY_EDITOR // If in the editor we can try and repair missing GUID issues. if (m_TableCollectionNameGuid == Guid.Empty) { // We have to defer as we can not use the asset database whilst inside of OnAfterDeserialize. UnityEditor.EditorApplication.delayCall += () => { if (this != null && UnityEditor.AssetDatabase.TryGetGUIDAndLocalFileIdentifier(this, out string guid, out long _)) { m_TableCollectionNameGuid = Guid.Parse(guid); UnityEditor.EditorUtility.SetDirty(this); } }; } #endif } } }