using System; using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine.Localization.Metadata; namespace UnityEngine.Localization.Tables { /// /// Player version of a table entry that can contain additional data that is not serialized. /// public class TableEntry : IMetadataCollection { SharedTableData.SharedTableEntry m_SharedTableEntry; /// /// The table that this entry is part of. /// public LocalizationTable Table { get; internal set; } /// /// The serialized data /// internal TableEntryData Data { get; set; } /// /// The shared table entry contains information for all locales, this is taken from . /// public SharedTableData.SharedTableEntry SharedEntry { get { if (m_SharedTableEntry == null) { Assertions.Assert.IsNotNull(Table); m_SharedTableEntry = Table.SharedData.GetEntry(KeyId); } return m_SharedTableEntry; } } /// /// The Key or Name of this table entry that is stored in . /// public string Key { get => SharedEntry?.Key; set => Table.SharedData.RenameKey(KeyId, value); } /// /// Key Id for this table entry. /// public long KeyId => Data.Id; /// /// Raw localized value. /// public string LocalizedValue => Data.Localized; /// /// The Metadata for this table entry. /// public IList MetadataEntries => Data.Metadata.MetadataEntries; /// /// Returns the first Metadata item from of type TObject. /// /// /// public TObject GetMetadata() where TObject : IMetadata { return Data.Metadata.GetMetadata(); } /// /// Populates the list with all Metadata from that is of type TObject. /// /// /// public void GetMetadatas(IList foundItems) where TObject : IMetadata { Data.Metadata.GetMetadatas(foundItems); } /// /// Returns all Metadata from that is of type TObject. /// /// /// public IList GetMetadatas() where TObject : IMetadata { return Data.Metadata.GetMetadatas(); } /// /// Returns true if any tag metadata of type TShared contains this entry. /// /// /// public bool HasTagMetadata() where TShared : SharedTableEntryMetadata { var tag = Table.GetMetadata(); return tag?.IsRegistered(this) == true; } /// /// Tags are Metadata that can be shared across multiple table entries, /// they are often used to indicate an entry has a particular attribute or feature, e.g SmartFormat. /// Generally Tags do not contains data, for sharing data across multiple table entries see . /// A Tag reference will be stored in and . /// /// public void AddTagMetadata() where TShared : SharedTableEntryMetadata, new() { TShared tag = null; foreach (var md in Table.MetadataEntries) { if (md is TShared shared) { tag = shared; // If we already have the tag then there is nothing we need to do. (LOC-779) if (tag.IsRegistered(this)) return; break; } } if (tag == null) { tag = new TShared(); Table.AddMetadata(tag); } tag.Register(this); AddMetadata(tag); } /// /// SharedTableEntryMetadata is Metadata that can be shared across multiple entries in a single table. /// The instance reference will be stored in and . /// /// public void AddSharedMetadata(SharedTableEntryMetadata md) { if (!Table.Contains(md)) { Table.AddMetadata(md); } // If we already have the tag then there is nothing we need to do. if (md.IsRegistered(this)) return; md.Register(this); AddMetadata(md); } /// /// SharedTableCollectionMetadata is Metadata that can be applied to multiple table entries in a table collection. /// The Metadata is stored in the . /// /// public void AddSharedMetadata(SharedTableCollectionMetadata md) { if (!Table.SharedData.Metadata.Contains(md)) { Table.SharedData.Metadata.AddMetadata(md); } md.AddEntry(Data.Id, Table.LocaleIdentifier.Code); } /// /// Add an entry to . /// /// public void AddMetadata(IMetadata md) { Data.Metadata.AddMetadata(md); } /// /// Removes the Metadata tag from this entry and the table if it is no longer used by any other table entries. /// /// public void RemoveTagMetadata() where TShared : SharedTableEntryMetadata { var tableMetada = Table.MetadataEntries; var entryMetadata = Data.Metadata.MetadataEntries; // We check both the entry and table metadata as we had some bugs in the past that caused them to go out of sync. (LOC-779) // Check entry for (int i = entryMetadata.Count - 1; i >= 0; --i) { if (entryMetadata[i] is TShared tag) { tag.Unregister(this); entryMetadata.RemoveAt(i); } } // Check table for (int i = tableMetada.Count - 1; i >= 0; --i) { if (tableMetada[i] is TShared tag) { tag.Unregister(this); // Remove the shared data if it is no longer used if (tag.Count == 0) { tableMetada.RemoveAt(i); } } } } /// /// Removes the entry from the shared Metadata in the table and removes the /// shared Metadata if no other entries are using it. /// /// public void RemoveSharedMetadata(SharedTableEntryMetadata md) { md.Unregister(this); RemoveMetadata(md); // Remove the shared data if it is no longer used if (md.Count == 0 && Table.Contains(md)) { Table.RemoveMetadata(md); } } /// /// Removes the entry from the Shared Metadata and removes it from the /// if no other entries are using it. /// /// public void RemoveSharedMetadata(SharedTableCollectionMetadata md) { md.RemoveEntry(Data.Id, Table.LocaleIdentifier.Code); if (md.IsEmpty) { Table.SharedData.Metadata.RemoveMetadata(md); } } /// /// Remove an entry from . /// /// /// public bool RemoveMetadata(IMetadata md) { return Data.Metadata.RemoveMetadata(md); } /// /// Checks if the Metadata is contained within . /// /// /// public bool Contains(IMetadata md) { return Data.Metadata.Contains(md); } public override string ToString() => $"{KeyId} - {LocalizedValue}"; }; /// /// Options for how to handle missing entries when using . /// public enum MissingEntryAction { /// /// Do nothing. /// Nothing, /// /// Add the missing entries to the . /// AddEntriesToSharedData, /// /// Remove the missing entries from the table. /// RemoveEntriesFromTable } /// /// Provides common functionality for both string and asset tables. /// /// public abstract class DetailedLocalizationTable : LocalizationTable, IDictionary, ISerializationCallbackReceiver where TEntry : TableEntry { Dictionary m_TableEntries = new Dictionary(); ICollection IDictionary.Keys => m_TableEntries.Keys; /// /// All values in this table. /// public ICollection Values => m_TableEntries.Values; /// /// The number of entries in this Table. /// public int Count => m_TableEntries.Count; /// /// Will always be false. Implemented because it is required by the System.Collections.IList interface. /// public bool IsReadOnly => false; /// /// Get/Set a value using the specified key. /// /// /// public TEntry this[long key] { get => m_TableEntries[key]; set { if (key == SharedTableData.EmptyId) throw new ArgumentException("Key Id value 0, is not valid. All Key Id's must be non-zero."); if (value.Table != this) throw new ArgumentException("Table entry does not belong to this table. Table entries can not be shared across tables."); // Move the entry RemoveEntry(value.Data.Id); value.Data.Id = key; m_TableEntries[key] = value; } } /// /// Get/Set a value using the specified key name. /// /// /// public TEntry this[string keyName] { get => GetEntry(keyName); set { if (value.Table != this) throw new ArgumentException("Table entry does not belong to this table. Table entries can not be shared across tables."); var key = FindKeyId(keyName, true); this[key] = value; } } /// /// Returns a new instance of TEntry. /// /// public abstract TEntry CreateTableEntry(); internal TEntry CreateTableEntry(TableEntryData data) { var entry = CreateTableEntry(); entry.Data = data; return entry; } /// public override void CreateEmpty(TableEntryReference entryReference) { AddEntryFromReference(entryReference, string.Empty); } /// /// Add or update an entry in the table. /// /// The name of the key. /// The localized item, a string for or asset guid for . /// public TEntry AddEntry(string key, string localized) { var keyId = FindKeyId(key, true); return keyId == 0 ? null : AddEntry(keyId, localized); } /// /// Add or update an entry in the table. /// /// The unique key id. /// The localized item, a string for or asset guid for . /// public virtual TEntry AddEntry(long keyId, string localized) { if (keyId == SharedTableData.EmptyId) throw new ArgumentException($"Key Id value {nameof(SharedTableData.EmptyId)}({SharedTableData.EmptyId}), is not valid. All Key Id's must be non-zero.", nameof(keyId)); if (!m_TableEntries.TryGetValue(keyId, out var tableEntry)) { tableEntry = CreateTableEntry(); tableEntry.Data = new TableEntryData(keyId); m_TableEntries[keyId] = tableEntry; } tableEntry.Data.Localized = localized; return tableEntry; } /// /// Add or update an entry in the table. /// /// The containing a valid Key or Key Id. /// The localized item, a string for or asset guid for /// public TEntry AddEntryFromReference(TableEntryReference entryReference, string localized) { if (entryReference.ReferenceType == TableEntryReference.Type.Id) return AddEntry(entryReference.KeyId, localized); if (entryReference.ReferenceType == TableEntryReference.Type.Name) return AddEntry(entryReference.Key, localized); throw new ArgumentException($"{nameof(TableEntryReference)} should not be Empty", nameof(entryReference)); } /// /// Remove an entry from the table if it exists. /// /// The name of the key. /// True if the entry was found and removed. public bool RemoveEntry(string key) { var keyId = FindKeyId(key, false); return keyId != 0 && RemoveEntry(keyId); } /// /// Remove an entry from the table if it exists. /// /// The key id to remove. /// True if the entry was found and removed. public virtual bool RemoveEntry(long keyId) { if (m_TableEntries.TryGetValue(keyId, out var item)) { // We also need to remove any references to this entry in shared metadata. for (int i = 0; i < MetadataEntries.Count; ++i) { var metadataEntry = MetadataEntries[i]; if (metadataEntry is SharedTableEntryMetadata sharedMetadata) { sharedMetadata.Unregister(item); // Remove the shared data if it is no longer used if (sharedMetadata.Count == 0) { MetadataEntries.RemoveAt(i); i--; } } } for (int i = 0; i < SharedData?.Metadata.MetadataEntries.Count; i++) { var metadata = SharedData.Metadata.MetadataEntries[i]; if (metadata is SharedTableCollectionMetadata sharedMetadata) { sharedMetadata.RemoveEntry(keyId, LocaleIdentifier.Code); // Remove the shared data if it is no longer used if (sharedMetadata.IsEmpty) { SharedData.Metadata.MetadataEntries.RemoveAt(i); i--; } } } item.Data.Id = SharedTableData.EmptyId; item.Table = null; return m_TableEntries.Remove(keyId); } return false; } /// /// Returns the entry reference or null if one does not exist. /// /// /// public TEntry GetEntryFromReference(TableEntryReference entryReference) { if (entryReference.ReferenceType == TableEntryReference.Type.Id) return GetEntry(entryReference.KeyId); else if (entryReference.ReferenceType == TableEntryReference.Type.Name) return GetEntry(entryReference.Key); return null; } /// /// Returns the entry for the key or null if one does not exist. /// /// /// public TEntry GetEntry(string key) { var keyId = FindKeyId(key, false); return keyId == 0 ? null : GetEntry(keyId); } /// /// Returns the entry for the key id or null if one does not exist. /// /// /// public virtual TEntry GetEntry(long keyId) { m_TableEntries.TryGetValue(keyId, out var tableEntry); return tableEntry; } /// /// Adds the entry with the specified keyId. /// /// /// public void Add(long keyId, TEntry value) { this[keyId] = value; } /// /// Adds the item value with the specified keyId. /// /// public void Add(KeyValuePair item) { this[item.Key] = item.Value; } /// /// Returns true if the table contains an entry with the keyId. /// /// /// public bool ContainsKey(long keyId) => m_TableEntries.ContainsKey(keyId); /// /// Returns true if the table contains an entry with the same value. /// /// The value to check for in all table entries. /// True if a match was found else false. public bool ContainsValue(string localized) { foreach (var entry in m_TableEntries.Values) { if (entry.Data.Localized == localized) return true; } return false; } /// /// Returns true if the table contains the item. /// /// /// public bool Contains(KeyValuePair item) => m_TableEntries.Contains(item); /// /// Remove the entry with the keyId. /// /// /// public bool Remove(long keyId) => RemoveEntry(keyId); /// /// Remove the item from the table if it exists. /// /// /// public bool Remove(KeyValuePair item) { if (Contains(item)) { RemoveEntry(item.Key); return true; } return false; } /// /// Tables do not store the full information for an entry, instead they store just the Id of that entry which can then be referenced in . /// It is possible that something may have caused an entry to be in the Table but missing from . /// This will cause issues and often result in the entry being ignored. This will check for any entries that exist in the table but do not have an entry in . /// /// The action to take on the found missing entries. /// The identified missing entries. public IList CheckForMissingSharedTableDataEntries(MissingEntryAction action = MissingEntryAction.Nothing) { // Find all entries that are missing from the Shared Table Data. var results = m_TableEntries.Where(e => !SharedData.Contains(e.Key)).Select(e => e.Value).ToArray(); if (results.Length == 0) return results; if (action == MissingEntryAction.AddEntriesToSharedData) { for (int i = 0; i < results.Length; ++i) { // Add a default key, then remap the id var sharedEntry = SharedData.AddKey(); SharedData.RemapId(sharedEntry.Id, results[i].KeyId); } } else if (action == MissingEntryAction.RemoveEntriesFromTable) { for (int i = 0; i < results.Length; ++i) { RemoveEntry(results[i].KeyId); } } return results; } /// /// Find the entry, if it exists in the table. /// /// /// /// True if the entry was found. public bool TryGetValue(long keyId, out TEntry value) => m_TableEntries.TryGetValue(keyId, out value); /// /// Clear all entries in this table. /// public void Clear() { TableData.Clear(); m_TableEntries.Clear(); } /// /// Copies the contents of the table into an array starting at the arrayIndex. /// /// /// public void CopyTo(KeyValuePair[] array, int arrayIndex) { foreach (var entry in m_TableEntries) { array[arrayIndex++] = entry; } } /// /// Return an enumerator for the entries in this table. /// /// public IEnumerator> GetEnumerator() => m_TableEntries.GetEnumerator(); /// /// Return an enumerator for the entries in this table. /// /// IEnumerator IEnumerable.GetEnumerator() => m_TableEntries.GetEnumerator(); /// /// Creates a string representation of the table as "{TableCollectionName}({LocaleIdentifier})". /// /// public override string ToString() => $"{TableCollectionName}({LocaleIdentifier})"; /// /// Does nothing but required for . /// public void OnBeforeSerialize() { TableData.Clear(); foreach (var entry in this) { // Sync the id entry.Value.Data.Id = entry.Key; TableData.Add(entry.Value.Data); } } /// /// Converts the serialized data into . /// public void OnAfterDeserialize() { try { m_TableEntries = TableData.ToDictionary(o => o.Id, CreateTableEntry); } catch (Exception e) { var error = $"Error Deserializing Table Data \"{TableCollectionName}({LocaleIdentifier})\".\n{e.Message}\n{e.InnerException}"; Debug.LogError(error, this); } } } }