2025-05-01 01:48:08 -07:00

549 lines
19 KiB
C#

using System;
using System.Collections.Generic;
using UnityEngine.Localization.Metadata;
using UnityEngine.Serialization;
namespace UnityEngine.Localization.Tables
{
/// <summary>
/// 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.
/// </summary>
public class SharedTableData : ScriptableObject, ISerializationCallbackReceiver
{
/// <summary>
/// A entry in the SharedTableData. Contains the unique id, the name of the key and optional Metadata.
/// </summary>
[Serializable]
public class SharedTableEntry
{
[SerializeField]
long m_Id;
[SerializeField]
string m_Key;
[SerializeField]
MetadataCollection m_Metadata = new MetadataCollection();
/// <summary>
/// Unique id(to this SharedTableData).
/// </summary>
public long Id
{
get => m_Id;
internal set => m_Id = value;
}
/// <summary>
/// The name of the key, must also be unique.
/// </summary>
public string Key
{
get => m_Key;
internal set => m_Key = value;
}
/// <summary>
/// Optional Metadata for this key that is also shared between all tables that use this <see cref="SharedTableData"/>.
/// </summary>
public MetadataCollection Metadata
{
get => m_Metadata;
set => m_Metadata = value;
}
public override string ToString() => $"{Id} - {Key}";
}
/// <summary>
/// Represents an empty or null Key Id.
/// </summary>
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<SharedTableEntry> m_Entries = new List<SharedTableEntry>();
[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<long, SharedTableEntry> m_IdDictionary = new Dictionary<long, SharedTableEntry>();
Dictionary<string, SharedTableEntry> m_KeyDictionary = new Dictionary<string, SharedTableEntry>();
/// <summary>
/// All entries.
/// </summary>
public List<SharedTableEntry> Entries
{
get => m_Entries;
set
{
m_Entries = value;
m_IdDictionary.Clear();
m_KeyDictionary.Clear();
}
}
/// <summary>
/// Clear all entries in this table.
/// </summary>
public void Clear()
{
m_Entries.Clear();
m_IdDictionary.Clear();
m_KeyDictionary.Clear();
}
/// <summary>
/// The name of this table collection.
/// All <see cref="LocalizationTable"/> that use this SharedTableData will have this name.
/// </summary>
public string TableCollectionName
{
get => m_TableCollectionName;
set => m_TableCollectionName = value;
}
/// <summary>
/// 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.
/// </summary>
public Guid TableCollectionNameGuid
{
get => m_TableCollectionNameGuid;
internal set => m_TableCollectionNameGuid = value;
}
/// <summary>
/// Metadata that is shared between all tables.
/// </summary>
public MetadataCollection Metadata
{
get => m_Metadata;
set => m_Metadata = value;
}
/// <summary>
/// The Key Generator to use when adding new entries.
/// By default this will use <see cref="DistributedUIDGenerator"/>.
/// </summary>
/// <example>
/// This example shows how the <see cref="KeyGenerator"/> could be configured to use a <see cref="SequentialIDGenerator"/>.
/// <code source="../../DocCodeSamples.Tests/ChangeKeyGenerator.cs"/>
/// </example>
public IKeyGenerator KeyGenerator
{
get => m_KeyGenerator;
set => m_KeyGenerator = value;
}
/// <summary>
/// Get the key associated with the id.
/// </summary>
/// <param name="id">Id the key belongs to.</param>
/// <returns>The found key or null if one can not be found.</returns>
public string GetKey(long id)
{
var foundPair = FindWithId(id);
return foundPair?.Key;
}
/// <summary>
/// Get the unique Id for the key name from the shared table data.
/// </summary>
/// <param name="key">The key whose id is being requested.</param>
/// <returns>The keys id value or <see cref="EmptyId"/> if one does not exist.</returns>
public long GetId(string key)
{
var foundPair = FindWithKey(key);
return foundPair?.Id ?? 0;
}
/// <summary>
/// Get the unique Id for the key name, if one does not exist then a new entry is added.
/// </summary>
/// <param name="key"></param>
/// <param name="addNewKey"></param>
/// <returns>Id of the Key that exists else the Id of the newly created Key if addNewKey is True or EmptyId.</returns>
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;
}
/// <summary>
/// Returns the Entry for the key id, this contains all data for the key.
/// </summary>
/// <param name="tableEntryReference">Reference to the entry.</param>
/// <returns>The found key entry or null if one can not be found.</returns>
public SharedTableEntry GetEntryFromReference(TableEntryReference tableEntryReference)
{
if (tableEntryReference.ReferenceType == TableEntryReference.Type.Name)
return GetEntry(tableEntryReference.Key);
return GetEntry(tableEntryReference.KeyId);
}
/// <summary>
/// Returns the Entry for the key id, this contains all data for the key.
/// </summary>
/// <param name="id">Id the key belongs to.</param>
/// <returns>The found key entry or null if one can not be found.</returns>
public SharedTableEntry GetEntry(long id)
{
return FindWithId(id);
}
/// <summary>
/// Returns the Entry for the key, this contains all data for the key.
/// </summary>
/// <param name="key">The name of the key.</param>
/// <returns>The found key entry or null if one can not be found.</returns>
public SharedTableEntry GetEntry(string key)
{
return FindWithKey(key);
}
/// <summary>
/// Is the Id value used by any entries in this SharedTableData?
/// </summary>
/// <param name="id">Id to check.</param>
/// <returns></returns>
public bool Contains(long id) => FindWithId(id) != null;
/// <summary>
/// Is the key value used by any entries in this SharedTableData?
/// </summary>
/// <param name="key">Key to check.</param>
/// <returns></returns>
public bool Contains(string key) => FindWithKey(key) != null;
/// <summary>
/// Adds a new key to this SharedTableData if one does not already exists with the same id.
/// </summary>
/// <param name="key">The unique key name to assign to the entry.</param>
/// <param name="id">The unique id to assign to the key.</param>
/// <returns>The new entry or null if an entry already exists with the same id.</returns>
public SharedTableEntry AddKey(string key, long id)
{
return !Contains(id) ? AddKeyInternal(key, id) : null;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="key">The name for the new Key.</param>
/// <returns></returns>
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;
}
/// <summary>
/// Attempts to remove the key with provided id.
/// </summary>
/// <param name="id"></param>
public void RemoveKey(long id)
{
var foundEntry = FindWithId(id);
if (foundEntry != null)
RemoveKeyInternal(foundEntry);
}
/// <summary>
/// Attempts to remove the key from this SharedTableData.
/// </summary>
/// <param name="key">The key to be removed.</param>
public void RemoveKey(string key)
{
var foundEntry = FindWithKey(key);
if (foundEntry != null)
RemoveKeyInternal(foundEntry);
}
/// <summary>
/// Rename the key value for the provided id if it exists.
/// </summary>
/// <param name="id"></param>
/// <param name="newValue"></param>
public void RenameKey(long id, string newValue)
{
var foundEntry = FindWithId(id);
if (foundEntry != null)
RenameKeyInternal(foundEntry, newValue);
}
/// <summary>
/// Rename the key value if it exists.
/// </summary>
/// <param name="oldValue"></param>
/// <param name="newValue"></param>
public void RenameKey(string oldValue, string newValue)
{
var foundEntry = FindWithKey(oldValue);
if (foundEntry != null)
RenameKeyInternal(foundEntry, newValue);
}
/// <summary>
/// Attempts to change the Id of an entry.
/// </summary>
/// <param name="currentId">The current Id that should be changed. Must exist.</param>
/// <param name="newId">The new Id to use. Must not already be in use.</param>
/// <returns>True is the Id was changed successfully.</returns>
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;
}
/// <summary>
/// Returns the <see cref="SharedTableEntry"/> that is the most similar to the text.
/// Uses the Levenshtein distance method.
/// </summary>
/// <param name="text">The text to match against.</param>
/// <param name="distance">The number of edits needed to turn <paramref name="text"/> into the returned <see cref="SharedTableEntry"/>, 0 being an exact match.</param>
/// <returns>The <see cref="SharedTableEntry"/> that is the most similar to the text or null if one could not be found.</returns>
[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
/// <summary>
/// Compute the distance between two strings.
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <returns>The number of edits needed to turn one string into another.</returns>
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)";
/// <summary>
/// Converts the Guid into a serializable string.
/// </summary>
public void OnBeforeSerialize()
{
m_TableCollectionNameGuidString = TableReference.StringFromGuid(m_TableCollectionNameGuid);
}
/// <summary>
/// Converts the serializable string into a Guid.
/// </summary>
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
}
}
}