Search Unity

  1. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Question NullReferenceException: The Table "xyz_en" does not have a SharedTableData

Discussion in 'Localization Tools' started by Peter77, Nov 2, 2023.

  1. Peter77

    Peter77

    QA Jesus

    Joined:
    Jun 12, 2013
    Posts:
    6,574
    We encounter the following error that occurs randomly when the game attempts to get a LocalizedString for the first time. The code is executed in a Start() method in the very first scene that loads.

    Do you have any suggestions what we could do to workaround this issue? We use Unity 2022.3.11f1, Localization 1.4.5 and Addressables 1.21.18. @karl_jones

    Code (CSharp):
    1. NullReferenceException: The Table "xyz_en" does not have a SharedTableData.
    2.  
    3. UnityEngine.Localization.Tables.LocalizationTable.VerifySharedTableDataIsNotNull ()
    4. UnityEngine.Localization.Tables.LocalizationTable.get_TableCollectionName ()
    5. UnityEngine.Localization.Operations.PreloadLocaleOperation`2[TTable,TEntry].LoadTableContents (UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle`1[TObject] operation)
    6. DelegateList`1[T].Invoke (T res)
    7. UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationBase`1[TObject].InvokeCompletionEvent ()
    8. UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationBase`1[TObject].Complete (TObject result, System.Boolean success, System.Exception exception, System.Boolean releaseDependenciesOnFailure)
    9. UnityEngine.AsyncOperation.InvokeCompletionEvent ()
    10. UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider+InternalOp.WaitForCompletionHandler ()
    11. UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationBase`1[TObject].WaitForCompletion ()
    12. UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion ()
    13. UnityEngine.Localization.Operations.LocalizationGroupOperation.InvokeWaitForCompletion ()
    14. UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationBase`1[TObject].WaitForCompletion ()
    15. UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion ()
    16. UnityEngine.Localization.Operations.WaitForCurrentOperationAsyncOperationBase`1[TObject].InvokeWaitForCompletion ()
    17. UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationBase`1[TObject].WaitForCompletion ()
    18. UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion ()
    19. UnityEngine.Localization.Operations.WaitForCurrentOperationAsyncOperationBase`1[TObject].InvokeWaitForCompletion ()
    20. UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationBase`1[TObject].WaitForCompletion ()
    21. UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion ()
    22. UnityEngine.Localization.Operations.LocalizationGroupOperation.InvokeWaitForCompletion ()
    23. UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationBase`1[TObject].WaitForCompletion ()
    24. UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion ()
    25. UnityEngine.Localization.Operations.WaitForCurrentOperationAsyncOperationBase`1[TObject].InvokeWaitForCompletion ()
    26. UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationBase`1[TObject].WaitForCompletion ()
    27. UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion ()
    28. UnityEngine.Localization.Operations.WaitForCurrentOperationAsyncOperationBase`1[TObject].InvokeWaitForCompletion ()
    29. UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationBase`1[TObject].WaitForCompletion ()
    30. UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion ()
    31. UnityEngine.Localization.Operations.WaitForCurrentOperationAsyncOperationBase`1[TObject].InvokeWaitForCompletion ()
    32. UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationBase`1[TObject].WaitForCompletion ()
    33. UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion ()
    34. UnityEngine.Localization.Operations.WaitForCurrentOperationAsyncOperationBase`1[TObject].InvokeWaitForCompletion ()
    35. UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationBase`1[TObject].WaitForCompletion ()
    36. UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle`1[TObject].WaitForCompletion ()
    37. UnityEngine.Localization.LocalizedString.GetLocalizedString ()
    38. MyComponent.Start ()
     
  2. karl_jones

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    8,149
    We don't have any active bugs with these symptoms.
    It looks like something is going wrong during preloading. Is this happening in the editor or player? We dont touch the SharedTableData in the player so if its going null then its likely an addressables issue, maybe the shared table data asset bundle had issues being loaded?
    Can you share the log file?
    You could also try using the addressables event viewer to see whats being loaded, check if the bundle with the shared table data in is loaded.
     
    Peter77 likes this.
  3. imaxs

    imaxs

    Joined:
    Oct 31, 2020
    Posts:
    14
    Yesterday I faced exactly the same error. It appears in WebGL build after change locale through Localization Settings.SelectedLocale

    And yes it occurs randomly when the game attempts to get a localized value.



    I use something like this code:
    Code (CSharp):
    1.  
    2. /*-----------------------------------------------*/
    3. var m_LocalizedStringTable = new LocalizedStringTable { TableReference = "UITable" };
    4. m_LocalizedStringTable.TableChanged += OnStringTableChanged;
    5. /*-----------------------------------------------*/
    6. private async void OnStringTableChanged(StringTable stringTable)
    7. {
    8.             await LocalizationSettings.InitializationOperation.Task;
    9.             var table = m_LocalizedStringTable.GetTableAsync();
    10.             await table.Task;
    11.             Debug.Log(table.Result.GetEntry("Settings").LocalizedValue); // <----- does not have a SharedTableData - error
    12. }
    13.  
    my addressable group settings:

     
  4. Peter77

    Peter77

    QA Jesus

    Joined:
    Jun 12, 2013
    Posts:
    6,574
    Thank you for the reply. It turned out the issue was most likely caused by stripping.

    One desperate attempt to fix it lead to another and when I added every assembly to
    linker.xml
    with the
    preserve
    tag, the issue disappeared.
     
  5. karl_jones

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    8,149
    That strange. Do you know what was being stripped to cause this issue?
     
  6. imaxs

    imaxs

    Joined:
    Oct 31, 2020
    Posts:
    14
    Hmm... I disabled code stripping, but it didn't help me.




    Unity 2022.3.9f
    Addressables 1.12.15
    Localization 1.4.5
     
  7. karl_jones

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    8,149
    Are you able to share your project or an example project that reproduces the issue?
     
  8. imaxs

    imaxs

    Joined:
    Oct 31, 2020
    Posts:
    14
    It's amazing I made a developer build with the profiler enabled in order to track down the error. But nothing happened... the error has disappeared.



    If I disable the profiler the error appears again o_O


    Okay I'll try to share an example project soon
     
    karl_jones likes this.
  9. Peter77

    Peter77

    QA Jesus

    Joined:
    Jun 12, 2013
    Posts:
    6,574
    What led me to the stripping theory is the following error I found via logcat:
    Code (CSharp):
    1. Unknown managed type referenced: MaxLengthMetadata
    2. Should not occur! Internal logic error: please report bug.
    3. Unknown managed type referenced: MaxLengthMetadata
    4. NullReferenceException: The Table "xyz_en" does not have a SharedTableData.
    I then found this code in our project:
    Code (CSharp):
    1. [Serializable]
    2. [Metadata(AllowedTypes = MetadataType.SharedStringTableEntry, AllowMultiple = false, MenuItem = "Max Length")]
    3. public class MaxLengthMetadata : UnityEngine.Localization.Metadata.Comment
    4. {
    5.     public int MaxLength;
    6. }
    I presume MaxLengthMetadata is added to one or multiple loca entries and was stripped, therefore the error and ultimately the exception in the localization code.
     
    karl_jones likes this.
  10. imaxs

    imaxs

    Joined:
    Oct 31, 2020
    Posts:
    14
    So, I created a simple example to demonstrate the error.
    Link: https://drive.google.com/drive/folders/1w9M4Y4QfFCFdleUKMVpBu85GJKA7APYs?usp=drive_link

    There is a build and source code (re-import packages before using). I also shared two videos. The first video demonstrates how it works in the editor, the second video shows errors in the browser.

    Code:
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using System.Runtime.CompilerServices;
    4. using UnityEngine;
    5. using UnityEngine.Localization;
    6. using UnityEngine.Localization.Settings;
    7. using UnityEngine.Localization.Tables;
    8. using UnityEngine.UIElements;
    9.  
    10. [RequireComponent(typeof(UIDocument))]
    11. public class UI : MonoBehaviour
    12. {
    13.     private const string c_Key = "ex";
    14.     private const string c_Table = "UITable";
    15.  
    16.     private VisualElement m_PrevButton;
    17.     private VisualElement m_NextButton;
    18.     private Label m_LocalizedText;
    19.  
    20.     private int m_CurrentLocaleIndex;
    21.     private IList<Locale> m_Locales;
    22.  
    23.     private StringTable m_StringTable;
    24.     private LocalizedStringTable m_LocalizedStringTable = new LocalizedStringTable { TableReference = c_Table };
    25.  
    26.     private void Awake()
    27.     {
    28.         var doc = GetComponent<UIDocument>();
    29.         var uxml = doc.rootVisualElement;
    30.    
    31.         m_PrevButton = uxml.Q<VisualElement>(name: "PrevButton");
    32.         m_NextButton = uxml.Q<VisualElement>(name: "NextButton");
    33.         m_LocalizedText = uxml.Q<Label>(name: "LocalizedText");
    34.    
    35.         m_PrevButton.RegisterCallback<ClickEvent>(_ => PreviousLocale());
    36.         m_NextButton.RegisterCallback<ClickEvent>(_ => NextLocale());
    37.     }
    38.  
    39.     private IEnumerator Start()
    40.     {
    41.         yield return LocalizationSettings.InitializationOperation;
    42.         Debug.Log("LocalizationSettings.InitializationOperation: " + LocalizationSettings.InitializationOperation.Status);
    43.    
    44.         m_Locales = LocalizationSettings.Instance.GetAvailableLocales().Locales;
    45.         SetLocale(1);
    46.    
    47.         LocalizationSettings.SelectedLocaleChanged += DebugInfo;
    48.  
    49.         var op = m_LocalizedStringTable.GetTableAsync();
    50.         yield return op.Task;
    51.    
    52.         m_LocalizedStringTable.TableChanged += OnStringTableChanged;
    53.         DebugInfo();
    54.     }
    55.  
    56.     private async void OnStringTableChanged(StringTable table)
    57.     {
    58.         await LocalizationSettings.InitializationOperation.Task;
    59.         await m_LocalizedStringTable.GetTableAsync().Task;
    60.  
    61.         var entry = table.GetEntry(c_Key);
    62.         Text = entry.GetLocalizedString();
    63.     }
    64.  
    65.     [MethodImpl(MethodImplOptions.AggressiveInlining)]
    66.     private void NextLocale()
    67.     {
    68.         SetLocale(m_CurrentLocaleIndex < m_Locales.Count - 1 ?  m_CurrentLocaleIndex + 1 : 0);
    69.     }
    70.  
    71.     [MethodImpl(MethodImplOptions.AggressiveInlining)]
    72.     private void PreviousLocale()
    73.     {
    74.         SetLocale(m_CurrentLocaleIndex > 0 ? m_CurrentLocaleIndex - 1 : m_Locales.Count - 1);
    75.     }
    76.  
    77.     private string Text
    78.     {
    79.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    80.         set => m_LocalizedText.text = value;
    81.     }
    82.  
    83.     [MethodImpl(MethodImplOptions.AggressiveInlining)]
    84.     private void SetLocale(int index)
    85.     {
    86.         m_CurrentLocaleIndex = index;
    87.         LocalizationSettings.SelectedLocale = m_Locales[index];
    88.     }
    89.  
    90.     [MethodImpl(MethodImplOptions.AggressiveInlining)]
    91.     private void DebugInfo()
    92.     {
    93.         DebugInfo(LocalizationSettings.SelectedLocale);
    94.         Debug.Log("Available locales: " + string.Join(", ", m_Locales));
    95.     }
    96.  
    97.     [MethodImpl(MethodImplOptions.AggressiveInlining)]
    98.     private void DebugInfo(Locale locale)
    99.     {
    100.         Debug.Log("Current Locale: " + locale.Identifier);
    101.     }
    102. }
    Errors in Browser Console:
     
    Last edited: Nov 9, 2023
  11. imaxs

    imaxs

    Joined:
    Oct 31, 2020
    Posts:
    14
    :) I solved the issue. The solution looks a bit clumsy, but it works for me.

    Localization 1.4.5

    all changes in the 'LocalizedDatabase.cs' file: https://www.textcompare.org/csharp/?id=654d836e3f5d6192242a24c8



    In the RegisterSharedTableAndGuidOperation method after the comment '// Register the shared table data Guid.' the SharedData variable is accessed without checking for null. That is why the error occurs.
    In my solution, I just cache the value into a static variable once.
    Code (CSharp):
    1.  
    2. private static SharedTableData m_SharedTableData = null;
    3.  
    Code (CSharp):
    1. // Caching SharedTableData once
    2. if (m_SharedTableData == null)
    3.      m_SharedTableData = table.SharedData;
    And then use that value if the SharedData variable is null for the table instance.
    Code (CSharp):
    1. if (table.SharedData == null)
    2.     table.SharedData = m_SharedTableData;
    I also changed the 'RegisterTableNameOperation' method:
    Code (CSharp):
    1.         void RegisterTableNameOperation(AsyncOperationHandle<TTable> tableOperation)
    2.         {
    3.             if (!tableOperation.IsDone)
    4.             {
    5.                 tableOperation.Completed += m_RegisterTableNameOperationAction;
    6.                 return;
    7.             }
    8.  
    9.             var table = tableOperation.Result;
    10.             var localeIdentifier = table.LocaleIdentifier;
    11.  
    12.             var key = (localeIdentifier, table.name);
    13.             if (TableOperations.ContainsKey(key))
    14.                 return;
    15.  
    16.             TableOperations[key] = tableOperation;
    17.  
    18.             if (TablePostprocessor != null)
    19.             {
    20.                 // Patch the table contents
    21.                 if (tableOperation.IsDone)
    22.                     PatchTableContents(tableOperation);
    23.                 else
    24.                     tableOperation.Completed += m_PatchTableContentsAction;
    25.             }
    26.         }
    Modified file 'LocalizedDatabase.cs':
    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using UnityEngine.Localization.Operations;
    4. using UnityEngine.Localization.Tables;
    5. using UnityEngine.Pool;
    6. using UnityEngine.ResourceManagement.AsyncOperations;
    7.  
    8. namespace UnityEngine.Localization.Settings
    9. {
    10.     /// <summary>
    11.     /// Options for the different fallback behaviours that are available.
    12.     /// </summary>
    13.     public enum FallbackBehavior
    14.     {
    15.         /// <summary>
    16.         /// Uses the <see cref="LocalizedDatabase{TTable, TEntry}.UseFallback"/> value in <see cref="LocalizationSettings.StringDatabase"/>
    17.         /// when localizing a string or <see cref="LocalizationSettings.AssetDatabase"/> when localizing an asset.
    18.         /// </summary>
    19.         UseProjectSettings,
    20.  
    21.         /// <summary>
    22.         /// Do not fallback.
    23.         /// </summary>
    24.         DontUseFallback,
    25.  
    26.         /// <summary>
    27.         /// Attempts to use a fallback when a localized value is not found.
    28.         /// </summary>
    29.         UseFallback
    30.     }
    31.  
    32.     /// <summary>
    33.     /// Options for how to handle a missing translation.
    34.     /// </summary>
    35.     [Flags]
    36.     public enum MissingTranslationBehavior
    37.     {
    38.         /// <summary>
    39.         /// Includes the missing translation message in the translated string.
    40.         /// </summary>
    41.         ShowMissingTranslationMessage = 1,
    42.  
    43.         /// <summary>
    44.         /// Prints the missing translation message using [Debug.LogWarning](https://docs.unity3d.com/ScriptReference/Debug.LogWarning.html).
    45.         /// </summary>
    46.         PrintWarning = 2
    47.     }
    48.  
    49.     /// <summary>
    50.     /// Can be assigned to <see cref="LocalizedDatabase{TTable, TEntry}.TableProvider"/> to override the default table loading through Addressables in order to provide a custom table.
    51.     /// </summary>
    52.     /// <example>
    53.     /// This example demonstrates how to use the <see cref="ITableProvider"/> to provide a custom String Table without using the Addressables system.
    54.     /// This approach is particularly useful when you want to allow users to add third-party content, such as modding.
    55.     /// The localization data could be loaded from an external file and then converted into a table at runtime.
    56.     /// <code source="../../../DocCodeSamples.Tests/TableProviderSamples.cs" region="custom-table-provider"/>
    57.     /// <code source="../../../DocCodeSamples.Tests/TableProviderSamples.cs" region="set-provider-editor"/>
    58.     /// </example>
    59.     public interface ITableProvider
    60.     {
    61.         /// <summary>
    62.         /// Provides a way to return a custom table when when attempting to load from <see cref="LocalizedDatabase{TTable, TEntry}.GetTableAsync(TableReference, Locale)"/>.
    63.         /// </summary>
    64.         /// <param name="tableCollectionName"></param>
    65.         /// <param name="locale"></param>
    66.         /// <typeparam name="TTable"></typeparam>
    67.         /// <returns>A valid table or <see langword="default"/>, which will trigger the default table loading.</returns>
    68.         AsyncOperationHandle<TTable> ProvideTableAsync<TTable>(string tableCollectionName, Locale locale) where TTable : LocalizationTable;
    69.     }
    70.  
    71.     /// <summary>
    72.     /// Gets a notification when a <see cref="StringTable"/> or <see cref="AssetTable"/> completes loading.
    73.     /// </summary>
    74.     /// <example>
    75.     /// This example demonstrates how to use the <see cref="ITablePostprocessor"/> to apply changes to a table after it has loaded but before it has been used.
    76.     /// This can be beneficial when you wish to modify or add entries to a table, such as when supporting third-party content, for example modding.
    77.     /// <code source="../../../DocCodeSamples.Tests/TablePatcherSamples.cs" region="custom-table-patcher"/>
    78.     /// <code source="../../../DocCodeSamples.Tests/TablePatcherSamples.cs" region="set-patcher-editor"/>
    79.     /// </example>
    80.     public interface ITablePostprocessor
    81.     {
    82.         /// <summary>
    83.         /// This could be used to patch a table with updated values.
    84.         /// </summary>
    85.         /// <param name="table">The loaded <see cref="StringTable"/> or <see cref="AssetTable"/>.</param>
    86.         void PostprocessTable(LocalizationTable table);
    87.     }
    88.  
    89.     /// <summary>
    90.     /// Provides common functionality for both string and asset table fetching.
    91.     /// </summary>
    92.     /// <typeparam name="TTable"></typeparam>
    93.     /// <typeparam name="TEntry"></typeparam>
    94.     [Serializable]
    95.     public abstract class LocalizedDatabase<TTable, TEntry> : IPreloadRequired, IReset, IDisposable
    96.         where TTable : DetailedLocalizationTable<TEntry>
    97.         where TEntry : TableEntry
    98.     {
    99.         /// <summary>
    100.         /// Contains the results of a request. The found entry and the table the entry was found in,
    101.         /// this may be different if a fall back occurred.
    102.         /// </summary>
    103.         public struct TableEntryResult
    104.         {
    105.             /// <summary>
    106.             /// The entry that was resolved or <see langword="null"/> if one could not be found.
    107.             /// </summary>
    108.             public TEntry Entry { get; }
    109.  
    110.             /// <summary>
    111.             /// The table the entry was extracted from. When <see cref="Entry"/> is <see langword="null"/>, this contains the last table that was tried.
    112.             /// </summary>
    113.             public TTable Table { get; }
    114.  
    115.             internal TableEntryResult(TEntry entry, TTable table)
    116.             {
    117.                 Entry = entry;
    118.                 Table = table;
    119.             }
    120.         }
    121.  
    122.         /// <summary>
    123.         /// Preload operation.
    124.         /// Loads all tables and their contents(when applicable) marked with the preload label for the selected locale.
    125.         /// </summary>
    126.         public AsyncOperationHandle PreloadOperation
    127.         {
    128.             get
    129.             {
    130.                 #if UNITY_EDITOR
    131.                 // Don't preload in Editor preview
    132.                 if (!LocalizationSettings.Instance.IsPlayingOrWillChangePlaymode)
    133.                     return AddressablesInterface.ResourceManager.CreateCompletedOperation(this, null);
    134.                 #endif
    135.  
    136.                 if (!m_PreloadOperationHandle.IsValid())
    137.                 {
    138.                     var operation = GenericPool<PreloadDatabaseOperation<TTable, TEntry>>.Get();
    139.                     operation.Init(this);
    140.                     m_PreloadOperationHandle = AddressablesInterface.ResourceManager.StartOperation(operation, default);
    141.                 }
    142.                 return m_PreloadOperationHandle;
    143.             }
    144.         }
    145.  
    146.         [SerializeField] TableReference m_DefaultTableReference;
    147.         [SerializeReference] ITableProvider m_CustomTableProvider;
    148.         [SerializeReference] ITablePostprocessor m_CustomTablePostprocessor;
    149.         [SerializeField] bool m_UseFallback;
    150.  
    151.         internal AsyncOperationHandle m_PreloadOperationHandle;
    152.         Action<AsyncOperationHandle> m_ReleaseNextFrame;
    153.  
    154.         readonly Action<AsyncOperationHandle<TTable>> m_PatchTableContentsAction;
    155.         readonly Action<AsyncOperationHandle<TTable>> m_RegisterSharedTableAndGuidOperationAction;
    156.         readonly Action<AsyncOperationHandle<TTable>> m_RegisterCompletedTableOperationAction;
    157.         readonly Action<AsyncOperationHandle<TTable>> m_RegisterTableNameOperationAction; // <============================================
    158.  
    159.         internal Action<AsyncOperationHandle> ReleaseNextFrame => m_ReleaseNextFrame;
    160.  
    161.         // Used in place of the actual selected locale when it is still being loaded.
    162.         internal static readonly LocaleIdentifier k_SelectedLocaleId = new LocaleIdentifier("selected locale placeholder");
    163.  
    164.         internal Dictionary<(LocaleIdentifier localeIdentifier, string tableNameOrGuid), AsyncOperationHandle<TTable>> TableOperations
    165.         {
    166.             get;
    167.         } = new Dictionary<(LocaleIdentifier localeIdentifier, string tableNameOrGuid), AsyncOperationHandle<TTable>>();
    168.  
    169.         internal Dictionary<Guid, AsyncOperationHandle<SharedTableData>> SharedTableDataOperations
    170.         {
    171.             get;
    172.         } = new Dictionary<Guid, AsyncOperationHandle<SharedTableData>>();
    173.  
    174.         /// <summary>
    175.         /// The default table to use when no table collection name is provided.
    176.         /// </summary>
    177.         public virtual TableReference DefaultTable
    178.         {
    179.             get => m_DefaultTableReference;
    180.             set => m_DefaultTableReference = value;
    181.         }
    182.  
    183.         /// <summary>
    184.         /// Called when attempting to load a table, can be used to override the default table loading through Addressables in order to provide a custom table.
    185.         /// </summary>
    186.         /// <example>
    187.         /// This example demonstrates how to use the <see cref="ITableProvider"/> to provide a custom String Table without using the Addressables system.
    188.         /// This approach is particularly useful when you want to allow users to add third-party content, such as modding.
    189.         /// The localization data could be loaded from an external file and then converted into a table at runtime.
    190.         /// <code source="../../../DocCodeSamples.Tests/TableProviderSamples.cs" region="custom-table-provider"/>
    191.         /// <code source="../../../DocCodeSamples.Tests/TableProviderSamples.cs" region="set-provider-editor"/>
    192.         /// </example>
    193.         public ITableProvider TableProvider
    194.         {
    195.             get => m_CustomTableProvider;
    196.             set => m_CustomTableProvider = value;
    197.         }
    198.  
    199.         /// <summary>
    200.         /// Gets a notification when a table completes loading.
    201.         /// This can be used to apply changes to a table at runtime, such as updating or creating new entries.
    202.         /// </summary>
    203.         /// <example>
    204.         /// This example demonstrates how to use the <see cref="ITablePostprocessor"/> to apply changes to a table after it has loaded but before it has been used.
    205.         /// This can be beneficial when you wish to modify or add entries to a table, such as when supporting third-party content, for example modding.
    206.         /// <code source="../../../DocCodeSamples.Tests/TablePatcherSamples.cs" region="custom-table-patcher"/>
    207.         /// <code source="../../../DocCodeSamples.Tests/TablePatcherSamples.cs" region="set-patcher-editor"/>
    208.         /// </example>
    209.         public ITablePostprocessor TablePostprocessor
    210.         {
    211.             get => m_CustomTablePostprocessor;
    212.             set => m_CustomTablePostprocessor = value;
    213.         }
    214.  
    215.         /// <summary>
    216.         /// Should the fallback Locale be used when a translation could not be found?.
    217.         /// </summary>
    218.         public bool UseFallback
    219.         {
    220.             get => m_UseFallback;
    221.             set => m_UseFallback = value;
    222.         }
    223.  
    224.         /// <summary>
    225.         /// Creates a new instance of the database.
    226.         /// </summary>
    227.         public LocalizedDatabase()
    228.         {
    229.             m_PatchTableContentsAction = PatchTableContents;
    230.             m_RegisterSharedTableAndGuidOperationAction = RegisterSharedTableAndGuidOperation;
    231.             m_RegisterCompletedTableOperationAction = RegisterCompletedTableOperation;
    232.             m_RegisterTableNameOperationAction = RegisterTableNameOperation; // <============================================
    233.             m_ReleaseNextFrame = LocalizationBehaviour.ReleaseNextFrame;
    234.         }
    235.  
    236.         internal TableReference GetDefaultTable()
    237.         {
    238.             if (m_DefaultTableReference.ReferenceType == TableReference.Type.Empty)
    239.                 throw new Exception($"Trying to get the DefaultTable however the {GetType().Name} DefaultTable value has not been set. This can be configured in the Localization Settings.");
    240.  
    241.             return m_DefaultTableReference;
    242.         }
    243.  
    244.         internal void RegisterCompletedTableOperation(AsyncOperationHandle<TTable> tableOperation)
    245.         {
    246.             if (!tableOperation.IsDone)
    247.             {
    248.                 tableOperation.Completed += m_RegisterCompletedTableOperationAction;
    249.                 return;
    250.             }
    251.  
    252.             RegisterTableNameOperation(tableOperation);
    253.  
    254.             // If the table is already present then RegisterTableNameOperation will release the operation which may cause it to become invalid.
    255.             if (tableOperation.IsValid())
    256.                 RegisterSharedTableAndGuidOperation(tableOperation);
    257.         }
    258.  
    259.         void RegisterTableNameOperation(AsyncOperationHandle<TTable> tableOperation)
    260.         {
    261.             if (!tableOperation.IsDone)
    262.             {
    263.                 tableOperation.Completed += m_RegisterTableNameOperationAction;
    264.                 return;
    265.             }
    266.  
    267.             var table = tableOperation.Result;
    268.             var localeIdentifier = table.LocaleIdentifier;
    269.  
    270.             var key = (localeIdentifier, table.name);
    271.             if (TableOperations.ContainsKey(key))
    272.                 return;
    273.  
    274.             TableOperations[key] = tableOperation;
    275.  
    276.             if (TablePostprocessor != null)
    277.             {
    278.                 // Patch the table contents
    279.                 if (tableOperation.IsDone)
    280.                     PatchTableContents(tableOperation);
    281.                 else
    282.                     tableOperation.Completed += m_PatchTableContentsAction;
    283.             }
    284.         }
    285.    
    286.         private static SharedTableData m_SharedTableData = null;
    287.         void RegisterSharedTableAndGuidOperation(AsyncOperationHandle<TTable> tableOperation)
    288.         {
    289.             if (!tableOperation.IsDone)
    290.             {
    291.                 tableOperation.Completed += m_RegisterSharedTableAndGuidOperationAction;
    292.                 return;
    293.             }
    294.  
    295.             var table = tableOperation.Result;
    296.             if (table == null)
    297.                 return;
    298.  
    299.             // Register the shared table data Guid.
    300.             if (m_SharedTableData == null)
    301.                 m_SharedTableData = table.SharedData;
    302.  
    303.             if (table.SharedData == null)
    304.                 table.SharedData = m_SharedTableData;
    305.  
    306.             var tableNameGuid = table.SharedData.TableCollectionNameGuid;
    307.             if (!SharedTableDataOperations.ContainsKey(tableNameGuid))
    308.                 SharedTableDataOperations[tableNameGuid] = AddressablesInterface.ResourceManager.CreateCompletedOperation(table.SharedData, null);
    309.  
    310.             // Register the table via the locale identifier and guid.
    311.             var localeAndGuid = (table.LocaleIdentifier, TableReference.StringFromGuid(tableNameGuid));
    312.             if (!TableOperations.ContainsKey(localeAndGuid))
    313.             {
    314.                 // We acquire when using the guid.
    315.                 AddressablesInterface.Acquire(tableOperation);
    316.                 TableOperations[localeAndGuid] = tableOperation;
    317.             }
    318.         }
    319.  
    320.         /// <summary>
    321.         /// Returns the Default table.
    322.         /// This method is asynchronous and may not have an immediate result.
    323.         /// Check [IsDone](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.IsDone) to see if the data is available,
    324.         /// if it is false then you can use the [Completed](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.Completed) event to get a callback when it is finished,
    325.         /// yield on the operation or call [WaitForCompletion](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion)
    326.         /// to force the operation to complete.
    327.         /// </summary>
    328.         /// <returns></returns>
    329.         public AsyncOperationHandle<TTable> GetDefaultTableAsync()
    330.         {
    331.             return GetTableAsync(GetDefaultTable());
    332.         }
    333.  
    334.         /// <summary>
    335.         /// Returns the named table.
    336.         /// This method is asynchronous and may not have an immediate result.
    337.         /// Check [IsDone](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.IsDone) to see if the data is available,
    338.         /// if it is false then you can use the [Completed](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.Completed) event to get a callback when it is finished,
    339.         /// yield on the operation or call [WaitForCompletion](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion)
    340.         /// to force the operation to complete.
    341.         /// </summary>
    342.         /// <remarks>
    343.         /// Internally the following is performed when a table is requested:
    344.         /// ![](../manual/images/scripting/GetTable.dot.svg)
    345.         /// </remarks>
    346.         /// <param name="tableReference">The table identifier. Can be either the name of the table or the table collection name Guid.</param>
    347.         /// <param name="locale">The <see cref="Locale"/> to load the table from, use null to default to <see cref="LocalizationSettings.SelectedLocale"/>.</param>
    348.         /// <returns></returns>
    349.         public virtual AsyncOperationHandle<TTable> GetTableAsync(TableReference tableReference, Locale locale = null)
    350.         {
    351.             // Extract the Locale Id or use a placeholder if we are using the selected locale and it is not ready yet.
    352.             bool localeAvailable = locale != null || LocalizationSettings.SelectedLocaleAsync.IsDone;
    353.             bool useSelectedLocalePlaceholder = true;
    354.             if (localeAvailable)
    355.             {
    356.                 if (locale == null)
    357.                 {
    358.                     if (LocalizationSettings.SelectedLocaleAsync.Result == null)
    359.                         return AddressablesInterface.ResourceManager.CreateCompletedOperation<TTable>(null, "SelectedLocale is null. Database could not get table.");
    360.                     locale = LocalizationSettings.SelectedLocaleAsync.Result;
    361.                 }
    362.                 useSelectedLocalePlaceholder = false;
    363.             }
    364.  
    365.             // Do we have a cached operation already running?
    366.             tableReference.Validate();
    367.             var tableIdString = tableReference.ReferenceType == TableReference.Type.Guid ? TableReference.StringFromGuid(tableReference.TableCollectionNameGuid) : tableReference.TableCollectionName;
    368.             var localeId = useSelectedLocalePlaceholder ? k_SelectedLocaleId : locale.Identifier;
    369.             if (TableOperations.TryGetValue((localeId, tableIdString), out var operationHandle))
    370.                 return operationHandle;
    371.  
    372.             // Start a new operation
    373.             var operation = CreateLoadTableOperation();
    374.             operation.Init(this, tableReference, locale);
    375.             operation.Dependency = LocalizationSettings.InitializationOperation;
    376.             var handle = AddressablesInterface.ResourceManager.StartOperation(operation, LocalizationSettings.InitializationOperation);
    377.  
    378.             if (useSelectedLocalePlaceholder || tableReference.ReferenceType == TableReference.Type.Guid)
    379.             {
    380.                 // When using a Guid we increment the reference count.
    381.                 // We do not increment for placeholders as we only ever have 1 reference for them, we dont share it between
    382.                 // table name and guid, because the register operation will use the actual selected locale and not the placeholder.
    383.                 // We treat the table name as default and do not increment for that one.
    384.                 if (!useSelectedLocalePlaceholder)
    385.                     AddressablesInterface.Acquire(handle);
    386.                 TableOperations[(localeId, tableIdString)] = handle;
    387.             }
    388.             else
    389.             {
    390.                 // Register the table name and Guid
    391.                 RegisterTableNameOperation(handle);
    392.             }
    393.  
    394.             // Register the table operation later. This will fully register everything including shared table data, table name and guid.
    395.             RegisterCompletedTableOperation(handle);
    396.  
    397.             return handle;
    398.         }
    399.  
    400.         /// <summary>
    401.         /// Returns the named table.
    402.         /// Uses [WaitForCompletion](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion) to force the loading to complete synchronously.
    403.         /// Please note that [WaitForCompletion](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion) is not supported on
    404.         /// [WebGL](https://docs.unity3d.com/Packages/com.unity.addressables@latest/index.html?subfolder=/manual/SynchronousAddressables.html#webgl).
    405.         /// </summary>
    406.         /// <param name="tableReference">The table identifier. Can be either the name of the table or the table collection name Guid.</param>
    407.         /// <param name="locale">The <see cref="Locale"/> to load the table from, use null to default to cref="LocalizationSettings.SelectedLocale"/>.</param>
    408.         /// <returns></returns>
    409.         public virtual TTable GetTable(TableReference tableReference, Locale locale = null) => GetTableAsync(tableReference, locale).WaitForCompletion();
    410.  
    411.         /// <summary>
    412.         /// Preloads the selected table. If the table is an <see cref="AssetTable"/> its assets will also be loaded.
    413.         /// Check [IsDone](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.IsDone) to see if the data is available,
    414.         /// if it is false then you can use the [Completed](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.Completed) event to get a callback when it is finished,
    415.         /// yield on the operation or call [WaitForCompletion](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion)
    416.         /// to force the operation to complete.
    417.         /// </summary>
    418.         /// <param name="tableReference">A reference to the table. A table reference can be either the name of the table or the table collection name Guid.</param>
    419.         /// <param name="locale">The <see cref="Locale"/> to use instead of the default <see cref="LocalizationSettings.SelectedLocale"/></param>
    420.         /// <returns></returns>
    421.         public AsyncOperationHandle PreloadTables(TableReference tableReference, Locale locale = null)
    422.         {
    423.             // Start a new operation
    424.             var operation = CreatePreloadTablesOperation();
    425.             operation.Init(this, new[] { tableReference }, locale);
    426.             operation.Dependency = LocalizationSettings.InitializationOperation;
    427.             var handle = AddressablesInterface.ResourceManager.StartOperation(operation, LocalizationSettings.InitializationOperation);
    428.  
    429.             if (LocalizationSettings.Instance.IsPlaying)
    430.                 handle.CompletedTypeless += ReleaseNextFrame;
    431.  
    432.             return handle;
    433.         }
    434.  
    435.         /// <summary>
    436.         /// Preloads the matching tables for the selected Locale. If the tables are <see cref="AssetTable"/> then their assets will also be loaded.
    437.         /// Check [IsDone](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.IsDone) to see if the data is available,
    438.         /// if it is false then you can use the [Completed](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.Completed) event to get a callback when it is finished,
    439.         /// yield on the operation or call [WaitForCompletion](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion)
    440.         /// to force the operation to complete.
    441.         /// </summary>
    442.         /// <param name="tableReferences">An IList of tableReferences to check for the string.</param>
    443.         /// <param name="locale">The <see cref="Locale"/> to use instead of the default <see cref="LocalizationSettings.SelectedLocale"/></param>
    444.         /// <returns></returns>
    445.         /// <example>
    446.         /// This shows how to manually preload tables instead of marking them as Preload in the editor.
    447.         /// <code source="../../../DocCodeSamples.Tests/LocalizedStringDatabaseSamples.cs" region="preload-example"/>
    448.         /// </example>
    449.         public AsyncOperationHandle PreloadTables(IList<TableReference> tableReferences, Locale locale = null)
    450.         {
    451.             // Start a new operation
    452.             var operation = CreatePreloadTablesOperation();
    453.             operation.Init(this, tableReferences, locale);
    454.             operation.Dependency = LocalizationSettings.InitializationOperation;
    455.             var handle = AddressablesInterface.ResourceManager.StartOperation(operation, LocalizationSettings.InitializationOperation);
    456.  
    457.             if (LocalizationSettings.Instance.IsPlaying)
    458.                 handle.CompletedTypeless += ReleaseNextFrame;
    459.  
    460.             return handle;
    461.         }
    462.  
    463.         /// <summary>
    464.         /// Releases all tables that are currently loaded in the database.
    465.         /// This will also release any references to the <see cref="SharedTableData"/> providing there are no other references to it, such as different Locale versions of the table that have been loaded.
    466.         /// </summary>
    467.         /// <param name="locale">The <see cref="Locale"/> to release tables for, when <see langword="null"/> all locales will be released.</param>
    468.         public void ReleaseAllTables(Locale locale = null)
    469.         {
    470.             using (HashSetPool<TTable>.Get(out var releasedTables))
    471.             {
    472.                 foreach (var to in TableOperations.Values)
    473.                 {
    474.                     if (!to.IsValid())
    475.                         continue;
    476.  
    477.                     if (locale != null && to.Result.LocaleIdentifier != locale.Identifier)
    478.                         continue;
    479.  
    480.                     // We may have multiple references to the table so we keep track in order to only call release once.
    481.                     if (to.Result != null && !releasedTables.Contains(to.Result))
    482.                     {
    483.                         ReleaseTableContents(to.Result);
    484.                         releasedTables.Add(to.Result);
    485.                     }
    486.                     AddressablesInterface.Release(to);
    487.                 }
    488.             }
    489.  
    490.             foreach (var shared in SharedTableDataOperations)
    491.             {
    492.                 AddressablesInterface.SafeRelease(shared.Value);
    493.             }
    494.             SharedTableDataOperations.Clear();
    495.  
    496.             if (m_PreloadOperationHandle.IsValid())
    497.             {
    498.                 //Debug.Assert(m_PreloadOperationHandle.IsDone, "Disposing an incomplete preload operation");
    499.  
    500.                 if (m_PreloadOperationHandle.IsDone)
    501.                     AddressablesInterface.Release(m_PreloadOperationHandle);
    502.                 m_PreloadOperationHandle = default;
    503.             }
    504.  
    505.             TableOperations.Clear();
    506.         }
    507.  
    508.         /// <summary>
    509.         /// Releases all references to the table that matches the <paramref name="tableReference"/> and <paramref name="locale"/>.
    510.         /// This will also release any references to the <see cref="SharedTableData"/> providing there are no other references to it, such as different Locale versions of the table that have been loaded.
    511.         /// A table is released by calling <see cref="AddressableAssets.Addressables.Release"/> on it which decrements the ref-count.
    512.         /// When a given Asset's ref-count is zero, that Asset is ready to be unloaded.
    513.         /// For more information, read the Addressables section [on when memory is cleared](https://docs.unity3d.com/Packages/com.unity.addressables@latest/index.html?subfolder=/manual/MemoryManagement.html).
    514.         /// </summary>
    515.         /// <param name="tableReference">A reference to the table. A table reference can be either the name of the table or the table collection name Guid.</param>
    516.         /// <param name="locale">The Locale version of the table that should be unloaded. When <see langword="null"/> the <see cref="LocalizationSettings.SelectedLocale"/> will be used.</param>
    517.         /// <example>
    518.         /// This shows how to release a table but prevent it from being unloaded.
    519.         /// <code source="../../../DocCodeSamples.Tests/LocalizedStringDatabaseSamples.cs" region="release-example"/>
    520.         /// </example>
    521.         public void ReleaseTable(TableReference tableReference, Locale locale = null)
    522.         {
    523.             tableReference.Validate();
    524.             var usingSelectedLocale = locale == LocalizationSettings.SelectedLocaleAsync.Result;
    525.             if (locale == null)
    526.             {
    527.                 locale = LocalizationSettings.SelectedLocaleAsync.Result;
    528.                 usingSelectedLocale = true;
    529.                 if (locale == null)
    530.                     return;
    531.             }
    532.  
    533.             // Get the shared table data
    534.             SharedTableData sharedTableData;
    535.             if (tableReference.ReferenceType == TableReference.Type.Guid)
    536.             {
    537.                 if (!SharedTableDataOperations.TryGetValue(tableReference.TableCollectionNameGuid, out var sharedTableDataOperationHandle) || sharedTableDataOperationHandle.Result == null)
    538.                     return;
    539.                 sharedTableData = sharedTableDataOperationHandle.Result;
    540.             }
    541.             else
    542.             {
    543.                 var nameAndLocale = (locale.Identifier, tableReference.TableCollectionName);
    544.                 if (!TableOperations.TryGetValue(nameAndLocale, out var operationHandleName) || operationHandleName.Result == null)
    545.                     return;
    546.                 sharedTableData = operationHandleName.Result.SharedData;
    547.             }
    548.  
    549.             if (sharedTableData == null)
    550.                 return;
    551.  
    552.             // We may have multiple references to the table(Guid, Table name, placeholders etc) so we will iterate through and remove them all.
    553.             // We also need to see if the Shared table data is still being used or if we can also release that.
    554.             int sharedTableDataUsers = 0;
    555.             bool removedContents = false;
    556.             using (ListPool<(LocaleIdentifier localeIdentifier, string tableNameOrGuid)>.Get(out var itemsToRemove))
    557.             {
    558.                 foreach (var tableOperation in TableOperations)
    559.                 {
    560.                     if (!tableOperation.Value.IsValid() || tableOperation.Value.Result == null || tableOperation.Value.Result.SharedData != sharedTableData)
    561.                         continue;
    562.  
    563.                     // Check locale and placeholder
    564.                     if (tableOperation.Key.localeIdentifier == locale.Identifier || usingSelectedLocale && tableOperation.Key.localeIdentifier == k_SelectedLocaleId)
    565.                     {
    566.                         // We only want to do this once.
    567.                         if (!removedContents)
    568.                         {
    569.                             ReleaseTableContents(tableOperation.Value.Result);
    570.                             removedContents = true;
    571.                         }
    572.  
    573.                         AddressablesInterface.SafeRelease(tableOperation.Value);
    574.  
    575.                         itemsToRemove.Add(tableOperation.Key);
    576.                     }
    577.                     else
    578.                     {
    579.                         sharedTableDataUsers++;
    580.                     }
    581.                 }
    582.  
    583.                 // Remove the items from the dictionary
    584.                 foreach (var tableKey in itemsToRemove)
    585.                 {
    586.                     TableOperations.Remove(tableKey);
    587.                 }
    588.  
    589.                 // If there's no other references to the shared table data then we can also remove that.
    590.                 if (sharedTableDataUsers == 0 && SharedTableDataOperations.TryGetValue(sharedTableData.TableCollectionNameGuid, out var sharedTableDataOperationHandle))
    591.                 {
    592.                     AddressablesInterface.SafeRelease(sharedTableDataOperationHandle);
    593.                     SharedTableDataOperations.Remove(sharedTableData.TableCollectionNameGuid);
    594.                 }
    595.             }
    596.         }
    597.  
    598.         /// <summary>
    599.         /// Returns all the tables available.
    600.         /// This method is asynchronous and may not have an immediate result.
    601.         /// Check [IsDone](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.IsDone) to see if the tables are available.
    602.         /// if it is false then you can use the [Completed](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.Completed) event to get a callback when it is finished,
    603.         /// yield on the operation or call [WaitForCompletion](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion)
    604.         /// to force the operation to complete.```
    605.         /// </summary>
    606.         /// <param name="locale">The <see cref="Locale"/> to load the table from, use null to default to cref="LocalizationSettings.SelectedLocale"/>.</param>
    607.         /// <returns></returns>
    608.         public virtual AsyncOperationHandle<IList<TTable>> GetAllTables(Locale locale = null)
    609.         {
    610.             var operation = GenericPool<LoadAllTablesOperation<TTable, TEntry>>.Get();
    611.             operation.Init(this, locale);
    612.             operation.Dependency = LocalizationSettings.InitializationOperation;
    613.             var handle = AddressablesInterface.ResourceManager.StartOperation(operation, LocalizationSettings.InitializationOperation);
    614.  
    615.             if (LocalizationSettings.Instance.IsPlaying)
    616.                 handle.CompletedTypeless += ReleaseNextFrame;
    617.  
    618.             return handle;
    619.         }
    620.  
    621.         /// <summary>
    622.         /// Checks if the table is currently loaded or not.
    623.         /// </summary>
    624.         /// <param name="tableReference">The table identifier. Can be either the name of the table or the table collection name Guid.</param>
    625.         /// <param name="locale">The <see cref="Locale"/> to load the table from, use null to default to cref="LocalizationSettings.SelectedLocale"/>.</param>
    626.         /// <returns></returns>
    627.         public virtual bool IsTableLoaded(TableReference tableReference, Locale locale = null)
    628.         {
    629.             var tableIdString = tableReference.ReferenceType == TableReference.Type.Guid ? TableReference.StringFromGuid(tableReference.TableCollectionNameGuid) : tableReference.TableCollectionName;
    630.             var localeAndName = locale != null ? (locale.Identifier, tableIdString) : (LocalizationSettings.SelectedLocaleAsync.Result.Identifier, tableIdString);
    631.             if (TableOperations.TryGetValue(localeAndName, out var TableOperationHandle))
    632.                 return TableOperationHandle.Status == AsyncOperationStatus.Succeeded;
    633.             else
    634.                 return false;
    635.         }
    636.  
    637.         internal virtual LoadTableOperation<TTable, TEntry> CreateLoadTableOperation() => GenericPool<LoadTableOperation<TTable, TEntry>>.Get();
    638.         internal virtual PreloadTablesOperation<TTable, TEntry> CreatePreloadTablesOperation() => GenericPool<PreloadTablesOperation<TTable, TEntry>>.Get();
    639.  
    640.         /// <summary>
    641.         /// Returns the entry from the requested table. A table entry will contain the localized item and metadata.
    642.         /// This method is asynchronous and may not have an immediate result.
    643.         /// Check [IsDone](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.IsDone) to see if the data is available,
    644.         /// if it is false then you can use the [Completed](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.Completed) event to get a callback when it is finished,
    645.         /// yield on the operation or call [WaitForCompletion](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion)
    646.         /// to force the operation to complete.
    647.         /// Once the Completed event has been called, during the next update, the internal operation will be returned to a pool so that it can be reused.
    648.         /// If you do plan to keep hold of the handle after completion then you should call [Acquire](xref::UnityEngine.ResourceManagement.AsyncOperationHandle.Acquire)
    649.         /// to prevent the operation being reused and <see cref="AddressableAssets.Addressables.Release(AsyncOperationHandle)"/> to finally return the operation back to the pool.
    650.         /// </summary>
    651.         /// <remarks>
    652.         /// Internally the following is performed when an Entry is requested.
    653.         /// First the table will be requested using <see cref="GetTableAsync(TableReference, Locale)"/>.
    654.         /// Once the table is loaded the entry will be extracted like so:
    655.         /// ![](../manual/images/GetEntry.dot.svg)
    656.         /// </remarks>
    657.         /// <param name="tableReference">The table identifier. Can be either the name of the table or the table collection name Guid.</param>
    658.         /// <param name="tableEntryReference">A reference to the entry in the table.</param>
    659.         /// <param name="locale">The <see cref="Locale"/> to load the table from. Null will use <see cref="LocalizationSettings.SelectedLocale"/>.</param>
    660.         /// <param name="fallbackBehavior">A Enum which determines if a Fallback should be used when no value could be found for the Locale.</param>
    661.         /// <returns></returns>
    662.         public virtual AsyncOperationHandle<TableEntryResult> GetTableEntryAsync(TableReference tableReference, TableEntryReference tableEntryReference, Locale locale = null, FallbackBehavior fallbackBehavior = FallbackBehavior.UseProjectSettings)
    663.         {
    664.             var loadTableOperation = GetTableAsync(tableReference, locale);
    665.             var getTableEntryOperation = GenericPool<GetTableEntryOperation<TTable, TEntry>>.Get();
    666.             var useFallback = fallbackBehavior != FallbackBehavior.UseProjectSettings ? fallbackBehavior == FallbackBehavior.UseFallback : UseFallback;
    667.  
    668.             getTableEntryOperation.Init(this, loadTableOperation, tableReference, tableEntryReference, locale, useFallback, true);
    669.             getTableEntryOperation.Dependency = loadTableOperation;
    670.             var handle = AddressablesInterface.ResourceManager.StartOperation(getTableEntryOperation, loadTableOperation);
    671.  
    672.             return handle;
    673.         }
    674.  
    675.         /// <summary>
    676.         /// Returns the entry from the requested table. A table entry will contain the localized item and metadata.
    677.         /// Uses [WaitForCompletion](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion) to force the loading to complete synchronously.
    678.         /// Please note that [WaitForCompletion](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion) is not supported on
    679.         /// [WebGL](https://docs.unity3d.com/Packages/com.unity.addressables@latest/index.html?subfolder=/manual/SynchronousAddressables.html#webgl).
    680.         /// </summary>
    681.         /// <param name="tableReference">The table identifier. Can be either the name of the table or the table collection name Guid.</param>
    682.         /// <param name="tableEntryReference">A reference to the entry in the table.</param>
    683.         /// <param name="locale">The <see cref="Locale"/> to load the table from. Null will use <see cref="LocalizationSettings.SelectedLocale"/>.</param>
    684.         /// <param name="fallbackBehavior">A Enum which determines if a Fallback should be used when no value could be found for the Locale.</param>
    685.         /// <returns>The table entry result which contains the table </returns>
    686.         public virtual TableEntryResult GetTableEntry(TableReference tableReference, TableEntryReference tableEntryReference, Locale locale = null, FallbackBehavior fallbackBehavior = FallbackBehavior.UseProjectSettings)
    687.         {
    688.             return GetTableEntryAsync(tableReference, tableEntryReference, locale, fallbackBehavior).WaitForCompletion();
    689.         }
    690.  
    691.         internal AsyncOperationHandle<SharedTableData> GetSharedTableData(Guid tableNameGuid)
    692.         {
    693.             if (SharedTableDataOperations.TryGetValue(tableNameGuid, out var sharedTableDataOp))
    694.                 return sharedTableDataOp;
    695.  
    696.             sharedTableDataOp = AddressablesInterface.LoadAssetFromGUID<SharedTableData>(TableReference.StringFromGuid(tableNameGuid));
    697.             SharedTableDataOperations[tableNameGuid] = sharedTableDataOp;
    698.             return sharedTableDataOp;
    699.         }
    700.  
    701.         internal virtual void ReleaseTableContents(TTable table) {}
    702.  
    703.         /// <summary>
    704.         /// Called before the LocaleChanged event is sent out in order to give the database a chance to prepare.
    705.         /// </summary>
    706.         /// <param name="locale"></param>
    707.         public virtual void OnLocaleChanged(Locale locale)
    708.         {
    709.             ReleaseAllTables();
    710.         }
    711.  
    712.         void PatchTableContents(AsyncOperationHandle<TTable> tableOperation)
    713.         {
    714.             // This should only be called once, after the table has loaded. It gives users the opurtunity to patch a Localized table.
    715.             // For example you may want to read in some extra data from a csv file after the game has been built.
    716.             if (TablePostprocessor != null && tableOperation.Result != null)
    717.                 TablePostprocessor.PostprocessTable(tableOperation.Result);
    718.         }
    719.  
    720.         /// <summary>
    721.         /// Resets the state of the provider by removing all the cached tables and clearing the preload operation.
    722.         /// </summary>
    723.         public void ResetState()
    724.         {
    725.             ReleaseAllTables();
    726.         }
    727.  
    728.         /// <summary>
    729.         /// Calls <see cref="ReleaseAllTables(Locale)"/>..
    730.         /// </summary>
    731.         void IDisposable.Dispose()
    732.         {
    733.             ReleaseAllTables();
    734.         }
    735.     }
    736. }
    737.  
     
    Last edited: Nov 10, 2023
  12. karl_jones

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    8,149
    imaxs likes this.
  13. karl_jones

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    8,149
    Hi,
    So I think we have figured out what the issue is.
    When we switch languages we release all the tables and related assets (including the shared table data), we then initialize which may request to load tables that use the same shared table data. In Unity 2021 Addressables uses asynchronous releasing of asset bundles which means they may not be released immediately.

    So something like this seems to happen:

    1) Unload the Table and subsequently Shared Table Data bundles asynchronously.
    2) Load a new Table and the same Shared Table Data bundle.
    3) Unload async completes and unloads the Shared table data asset.
    4) Null refernece!

    The String Tables are in different bundles per locale so they don't suffer from this issue but the shared table data does.
    This seems to be more prevalent on WebGL, likely due to the single threading limitations it has.

    A workaround for this now is to force a load of the shared table data after you get the table, then the shared table data will never be released.

    Something like this:

    Code (csharp):
    1. private void OnStringTableChanged(StringTable table)
    2. {
    3.     var operation = Addressables.LoadAssetAsync<SharedTableData>(table.SharedData.TableCollectionNameGuid.ToString("N"));
    4.     Addressables.ResourceManager.Acquire(operation);
    5. }
    Ill speak to the addressables team and see if they have any ideas for a better fix.
    One idea I had was to defer the unloading until we had switched locale however this would only really work when using preloading, theres still a chance we would miss some.

    Edit: Addressables bug https://issuetracker.unity3d.com/is...asset-in-the-same-frame-causes-null-reference

    I have attached a fix we have done in the package. We will try and get this into the next release (1.5.0)
     

    Attached Files:

    Last edited: Nov 13, 2023
    Karabin and imaxs like this.