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

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

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

UnityEngine.Localization.Tables.LocalizationTable.VerifySharedTableDataIsNotNull ()
UnityEngine.Localization.Tables.LocalizationTable.get_TableCollectionName ()
UnityEngine.Localization.Operations.PreloadLocaleOperation`2[TTable,TEntry].LoadTableContents (UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle`1[TObject] operation)
DelegateList`1[T].Invoke (T res)
UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationBase`1[TObject].InvokeCompletionEvent ()
UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationBase`1[TObject].Complete (TObject result, System.Boolean success, System.Exception exception, System.Boolean releaseDependenciesOnFailure)
UnityEngine.AsyncOperation.InvokeCompletionEvent ()
UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider+InternalOp.WaitForCompletionHandler ()
UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationBase`1[TObject].WaitForCompletion ()
UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion ()
UnityEngine.Localization.Operations.LocalizationGroupOperation.InvokeWaitForCompletion ()
UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationBase`1[TObject].WaitForCompletion ()
UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion ()
UnityEngine.Localization.Operations.WaitForCurrentOperationAsyncOperationBase`1[TObject].InvokeWaitForCompletion ()
UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationBase`1[TObject].WaitForCompletion ()
UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion ()
UnityEngine.Localization.Operations.WaitForCurrentOperationAsyncOperationBase`1[TObject].InvokeWaitForCompletion ()
UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationBase`1[TObject].WaitForCompletion ()
UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion ()
UnityEngine.Localization.Operations.LocalizationGroupOperation.InvokeWaitForCompletion ()
UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationBase`1[TObject].WaitForCompletion ()
UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion ()
UnityEngine.Localization.Operations.WaitForCurrentOperationAsyncOperationBase`1[TObject].InvokeWaitForCompletion ()
UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationBase`1[TObject].WaitForCompletion ()
UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion ()
UnityEngine.Localization.Operations.WaitForCurrentOperationAsyncOperationBase`1[TObject].InvokeWaitForCompletion ()
UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationBase`1[TObject].WaitForCompletion ()
UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion ()
UnityEngine.Localization.Operations.WaitForCurrentOperationAsyncOperationBase`1[TObject].InvokeWaitForCompletion ()
UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationBase`1[TObject].WaitForCompletion ()
UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion ()
UnityEngine.Localization.Operations.WaitForCurrentOperationAsyncOperationBase`1[TObject].InvokeWaitForCompletion ()
UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationBase`1[TObject].WaitForCompletion ()
UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle`1[TObject].WaitForCompletion ()
UnityEngine.Localization.LocalizedString.GetLocalizedString ()
MyComponent.Start ()

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.

1 Like

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:

/*-----------------------------------------------*/
var m_LocalizedStringTable = new LocalizedStringTable { TableReference = "UITable" };
m_LocalizedStringTable.TableChanged += OnStringTableChanged;
/*-----------------------------------------------*/
private async void OnStringTableChanged(StringTable stringTable)
{
            await LocalizationSettings.InitializationOperation.Task;
            var table = m_LocalizedStringTable.GetTableAsync();
            await table.Task;
            Debug.Log(table.Result.GetEntry("Settings").LocalizedValue); // <----- does not have a SharedTableData - error
}

my addressable group settings:

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.

That strange. Do you know what was being stripped to cause this issue?

Hmm… I disabled code stripping, but it didn’t help me.


Unity 2022.3.9f
Addressables 1.12.15
Localization 1.4.5

Are you able to share your project or an example project that reproduces the issue?

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 :eyes:

Okay I’ll try to share an example project soon

1 Like

What led me to the stripping theory is the following error I found via logcat:

Unknown managed type referenced: MaxLengthMetadata
Should not occur! Internal logic error: please report bug.
Unknown managed type referenced: MaxLengthMetadata
NullReferenceException: The Table "xyz_en" does not have a SharedTableData.

I then found this code in our project:

[Serializable]
[Metadata(AllowedTypes = MetadataType.SharedStringTableEntry, AllowMultiple = false, MenuItem = "Max Length")]
public class MaxLengthMetadata : UnityEngine.Localization.Metadata.Comment
{
    public int MaxLength;
}

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.

1 Like

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:

using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEngine;
using UnityEngine.Localization;
using UnityEngine.Localization.Settings;
using UnityEngine.Localization.Tables;
using UnityEngine.UIElements;

[RequireComponent(typeof(UIDocument))]
public class UI : MonoBehaviour
{
    private const string c_Key = "ex";
    private const string c_Table = "UITable";
 
    private VisualElement m_PrevButton;
    private VisualElement m_NextButton;
    private Label m_LocalizedText;

    private int m_CurrentLocaleIndex;
    private IList<Locale> m_Locales;

    private StringTable m_StringTable;
    private LocalizedStringTable m_LocalizedStringTable = new LocalizedStringTable { TableReference = c_Table };

    private void Awake()
    {
        var doc = GetComponent<UIDocument>();
        var uxml = doc.rootVisualElement;
    
        m_PrevButton = uxml.Q<VisualElement>(name: "PrevButton");
        m_NextButton = uxml.Q<VisualElement>(name: "NextButton");
        m_LocalizedText = uxml.Q<Label>(name: "LocalizedText");
    
        m_PrevButton.RegisterCallback<ClickEvent>(_ => PreviousLocale());
        m_NextButton.RegisterCallback<ClickEvent>(_ => NextLocale());
    }

    private IEnumerator Start()
    {
        yield return LocalizationSettings.InitializationOperation;
        Debug.Log("LocalizationSettings.InitializationOperation: " + LocalizationSettings.InitializationOperation.Status);
    
        m_Locales = LocalizationSettings.Instance.GetAvailableLocales().Locales;
        SetLocale(1);
    
        LocalizationSettings.SelectedLocaleChanged += DebugInfo;

        var op = m_LocalizedStringTable.GetTableAsync();
        yield return op.Task;
    
        m_LocalizedStringTable.TableChanged += OnStringTableChanged;
        DebugInfo();
    }

    private async void OnStringTableChanged(StringTable table)
    {
        await LocalizationSettings.InitializationOperation.Task;
        await m_LocalizedStringTable.GetTableAsync().Task;

        var entry = table.GetEntry(c_Key);
        Text = entry.GetLocalizedString();
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private void NextLocale()
    {
        SetLocale(m_CurrentLocaleIndex < m_Locales.Count - 1 ?  m_CurrentLocaleIndex + 1 : 0);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private void PreviousLocale()
    {
        SetLocale(m_CurrentLocaleIndex > 0 ? m_CurrentLocaleIndex - 1 : m_Locales.Count - 1);
    }
 
    private string Text
    {
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        set => m_LocalizedText.text = value;
    }
 
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private void SetLocale(int index)
    {
        m_CurrentLocaleIndex = index;
        LocalizationSettings.SelectedLocale = m_Locales[index];
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private void DebugInfo()
    {
        DebugInfo(LocalizationSettings.SelectedLocale);
        Debug.Log("Available locales: " + string.Join(", ", m_Locales));
    }
 
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private void DebugInfo(Locale locale)
    {
        Debug.Log("Current Locale: " + locale.Identifier);
    }
}

Errors in Browser Console:

:slight_smile: 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: Online Csharp Compare Tool

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.

private static SharedTableData m_SharedTableData = null;
// Caching SharedTableData once
if (m_SharedTableData == null)
     m_SharedTableData = table.SharedData;

And then use that value if the SharedData variable is null for the table instance.

if (table.SharedData == null)
    table.SharedData = m_SharedTableData;

I also changed the ‘RegisterTableNameOperation’ method:

        void RegisterTableNameOperation(AsyncOperationHandle<TTable> tableOperation)
        {
            if (!tableOperation.IsDone)
            {
                tableOperation.Completed += m_RegisterTableNameOperationAction;
                return;
            }

            var table = tableOperation.Result;
            var localeIdentifier = table.LocaleIdentifier;

            var key = (localeIdentifier, table.name);
            if (TableOperations.ContainsKey(key))
                return;

            TableOperations[key] = tableOperation;

            if (TablePostprocessor != null)
            {
                // Patch the table contents
                if (tableOperation.IsDone)
                    PatchTableContents(tableOperation);
                else
                    tableOperation.Completed += m_PatchTableContentsAction;
            }
        }

Modified file ‘LocalizedDatabase.cs’:

using System;
using System.Collections.Generic;
using UnityEngine.Localization.Operations;
using UnityEngine.Localization.Tables;
using UnityEngine.Pool;
using UnityEngine.ResourceManagement.AsyncOperations;

namespace UnityEngine.Localization.Settings
{
    /// <summary>
    /// Options for the different fallback behaviours that are available.
    /// </summary>
    public enum FallbackBehavior
    {
        /// <summary>
        /// Uses the <see cref="LocalizedDatabase{TTable, TEntry}.UseFallback"/> value in <see cref="LocalizationSettings.StringDatabase"/>
        /// when localizing a string or <see cref="LocalizationSettings.AssetDatabase"/> when localizing an asset.
        /// </summary>
        UseProjectSettings,

        /// <summary>
        /// Do not fallback.
        /// </summary>
        DontUseFallback,

        /// <summary>
        /// Attempts to use a fallback when a localized value is not found.
        /// </summary>
        UseFallback
    }

    /// <summary>
    /// Options for how to handle a missing translation.
    /// </summary>
    [Flags]
    public enum MissingTranslationBehavior
    {
        /// <summary>
        /// Includes the missing translation message in the translated string.
        /// </summary>
        ShowMissingTranslationMessage = 1,

        /// <summary>
        /// Prints the missing translation message using [Debug.LogWarning](https://docs.unity3d.com/ScriptReference/Debug.LogWarning.html).
        /// </summary>
        PrintWarning = 2
    }

    /// <summary>
    /// 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.
    /// </summary>
    /// <example>
    /// This example demonstrates how to use the <see cref="ITableProvider"/> to provide a custom String Table without using the Addressables system.
    /// This approach is particularly useful when you want to allow users to add third-party content, such as modding.
    /// The localization data could be loaded from an external file and then converted into a table at runtime.
    /// <code source="../../../DocCodeSamples.Tests/TableProviderSamples.cs" region="custom-table-provider"/>
    /// <code source="../../../DocCodeSamples.Tests/TableProviderSamples.cs" region="set-provider-editor"/>
    /// </example>
    public interface ITableProvider
    {
        /// <summary>
        /// Provides a way to return a custom table when when attempting to load from <see cref="LocalizedDatabase{TTable, TEntry}.GetTableAsync(TableReference, Locale)"/>.
        /// </summary>
        /// <param name="tableCollectionName"></param>
        /// <param name="locale"></param>
        /// <typeparam name="TTable"></typeparam>
        /// <returns>A valid table or <see langword="default"/>, which will trigger the default table loading.</returns>
        AsyncOperationHandle<TTable> ProvideTableAsync<TTable>(string tableCollectionName, Locale locale) where TTable : LocalizationTable;
    }

    /// <summary>
    /// Gets a notification when a <see cref="StringTable"/> or <see cref="AssetTable"/> completes loading.
    /// </summary>
    /// <example>
    /// 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.
    /// 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.
    /// <code source="../../../DocCodeSamples.Tests/TablePatcherSamples.cs" region="custom-table-patcher"/>
    /// <code source="../../../DocCodeSamples.Tests/TablePatcherSamples.cs" region="set-patcher-editor"/>
    /// </example>
    public interface ITablePostprocessor
    {
        /// <summary>
        /// This could be used to patch a table with updated values.
        /// </summary>
        /// <param name="table">The loaded <see cref="StringTable"/> or <see cref="AssetTable"/>.</param>
        void PostprocessTable(LocalizationTable table);
    }

    /// <summary>
    /// Provides common functionality for both string and asset table fetching.
    /// </summary>
    /// <typeparam name="TTable"></typeparam>
    /// <typeparam name="TEntry"></typeparam>
    [Serializable]
    public abstract class LocalizedDatabase<TTable, TEntry> : IPreloadRequired, IReset, IDisposable
        where TTable : DetailedLocalizationTable<TEntry>
        where TEntry : TableEntry
    {
        /// <summary>
        /// Contains the results of a request. The found entry and the table the entry was found in,
        /// this may be different if a fall back occurred.
        /// </summary>
        public struct TableEntryResult
        {
            /// <summary>
            /// The entry that was resolved or <see langword="null"/> if one could not be found.
            /// </summary>
            public TEntry Entry { get; }

            /// <summary>
            /// The table the entry was extracted from. When <see cref="Entry"/> is <see langword="null"/>, this contains the last table that was tried.
            /// </summary>
            public TTable Table { get; }

            internal TableEntryResult(TEntry entry, TTable table)
            {
                Entry = entry;
                Table = table;
            }
        }

        /// <summary>
        /// Preload operation.
        /// Loads all tables and their contents(when applicable) marked with the preload label for the selected locale.
        /// </summary>
        public AsyncOperationHandle PreloadOperation
        {
            get
            {
                #if UNITY_EDITOR
                // Don't preload in Editor preview
                if (!LocalizationSettings.Instance.IsPlayingOrWillChangePlaymode)
                    return AddressablesInterface.ResourceManager.CreateCompletedOperation(this, null);
                #endif

                if (!m_PreloadOperationHandle.IsValid())
                {
                    var operation = GenericPool<PreloadDatabaseOperation<TTable, TEntry>>.Get();
                    operation.Init(this);
                    m_PreloadOperationHandle = AddressablesInterface.ResourceManager.StartOperation(operation, default);
                }
                return m_PreloadOperationHandle;
            }
        }

        [SerializeField] TableReference m_DefaultTableReference;
        [SerializeReference] ITableProvider m_CustomTableProvider;
        [SerializeReference] ITablePostprocessor m_CustomTablePostprocessor;
        [SerializeField] bool m_UseFallback;

        internal AsyncOperationHandle m_PreloadOperationHandle;
        Action<AsyncOperationHandle> m_ReleaseNextFrame;

        readonly Action<AsyncOperationHandle<TTable>> m_PatchTableContentsAction;
        readonly Action<AsyncOperationHandle<TTable>> m_RegisterSharedTableAndGuidOperationAction;
        readonly Action<AsyncOperationHandle<TTable>> m_RegisterCompletedTableOperationAction;
        readonly Action<AsyncOperationHandle<TTable>> m_RegisterTableNameOperationAction; // <============================================

        internal Action<AsyncOperationHandle> ReleaseNextFrame => m_ReleaseNextFrame;

        // Used in place of the actual selected locale when it is still being loaded.
        internal static readonly LocaleIdentifier k_SelectedLocaleId = new LocaleIdentifier("selected locale placeholder");

        internal Dictionary<(LocaleIdentifier localeIdentifier, string tableNameOrGuid), AsyncOperationHandle<TTable>> TableOperations
        {
            get;
        } = new Dictionary<(LocaleIdentifier localeIdentifier, string tableNameOrGuid), AsyncOperationHandle<TTable>>();

        internal Dictionary<Guid, AsyncOperationHandle<SharedTableData>> SharedTableDataOperations
        {
            get;
        } = new Dictionary<Guid, AsyncOperationHandle<SharedTableData>>();

        /// <summary>
        /// The default table to use when no table collection name is provided.
        /// </summary>
        public virtual TableReference DefaultTable
        {
            get => m_DefaultTableReference;
            set => m_DefaultTableReference = value;
        }

        /// <summary>
        /// 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.
        /// </summary>
        /// <example>
        /// This example demonstrates how to use the <see cref="ITableProvider"/> to provide a custom String Table without using the Addressables system.
        /// This approach is particularly useful when you want to allow users to add third-party content, such as modding.
        /// The localization data could be loaded from an external file and then converted into a table at runtime.
        /// <code source="../../../DocCodeSamples.Tests/TableProviderSamples.cs" region="custom-table-provider"/>
        /// <code source="../../../DocCodeSamples.Tests/TableProviderSamples.cs" region="set-provider-editor"/>
        /// </example>
        public ITableProvider TableProvider
        {
            get => m_CustomTableProvider;
            set => m_CustomTableProvider = value;
        }

        /// <summary>
        /// Gets a notification when a table completes loading.
        /// This can be used to apply changes to a table at runtime, such as updating or creating new entries.
        /// </summary>
        /// <example>
        /// 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.
        /// 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.
        /// <code source="../../../DocCodeSamples.Tests/TablePatcherSamples.cs" region="custom-table-patcher"/>
        /// <code source="../../../DocCodeSamples.Tests/TablePatcherSamples.cs" region="set-patcher-editor"/>
        /// </example>
        public ITablePostprocessor TablePostprocessor
        {
            get => m_CustomTablePostprocessor;
            set => m_CustomTablePostprocessor = value;
        }

        /// <summary>
        /// Should the fallback Locale be used when a translation could not be found?.
        /// </summary>
        public bool UseFallback
        {
            get => m_UseFallback;
            set => m_UseFallback = value;
        }

        /// <summary>
        /// Creates a new instance of the database.
        /// </summary>
        public LocalizedDatabase()
        {
            m_PatchTableContentsAction = PatchTableContents;
            m_RegisterSharedTableAndGuidOperationAction = RegisterSharedTableAndGuidOperation;
            m_RegisterCompletedTableOperationAction = RegisterCompletedTableOperation;
            m_RegisterTableNameOperationAction = RegisterTableNameOperation; // <============================================
            m_ReleaseNextFrame = LocalizationBehaviour.ReleaseNextFrame;
        }

        internal TableReference GetDefaultTable()
        {
            if (m_DefaultTableReference.ReferenceType == TableReference.Type.Empty)
                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.");

            return m_DefaultTableReference;
        }

        internal void RegisterCompletedTableOperation(AsyncOperationHandle<TTable> tableOperation)
        {
            if (!tableOperation.IsDone)
            {
                tableOperation.Completed += m_RegisterCompletedTableOperationAction;
                return;
            }

            RegisterTableNameOperation(tableOperation);

            // If the table is already present then RegisterTableNameOperation will release the operation which may cause it to become invalid.
            if (tableOperation.IsValid())
                RegisterSharedTableAndGuidOperation(tableOperation);
        }

        void RegisterTableNameOperation(AsyncOperationHandle<TTable> tableOperation)
        {
            if (!tableOperation.IsDone)
            {
                tableOperation.Completed += m_RegisterTableNameOperationAction;
                return;
            }

            var table = tableOperation.Result;
            var localeIdentifier = table.LocaleIdentifier;

            var key = (localeIdentifier, table.name);
            if (TableOperations.ContainsKey(key))
                return;

            TableOperations[key] = tableOperation;

            if (TablePostprocessor != null)
            {
                // Patch the table contents
                if (tableOperation.IsDone)
                    PatchTableContents(tableOperation);
                else
                    tableOperation.Completed += m_PatchTableContentsAction;
            }
        }
    
        private static SharedTableData m_SharedTableData = null;
        void RegisterSharedTableAndGuidOperation(AsyncOperationHandle<TTable> tableOperation)
        {
            if (!tableOperation.IsDone)
            {
                tableOperation.Completed += m_RegisterSharedTableAndGuidOperationAction;
                return;
            }

            var table = tableOperation.Result;
            if (table == null)
                return;

            // Register the shared table data Guid.
            if (m_SharedTableData == null)
                m_SharedTableData = table.SharedData;

            if (table.SharedData == null)
                table.SharedData = m_SharedTableData;

            var tableNameGuid = table.SharedData.TableCollectionNameGuid;
            if (!SharedTableDataOperations.ContainsKey(tableNameGuid))
                SharedTableDataOperations[tableNameGuid] = AddressablesInterface.ResourceManager.CreateCompletedOperation(table.SharedData, null);

            // Register the table via the locale identifier and guid.
            var localeAndGuid = (table.LocaleIdentifier, TableReference.StringFromGuid(tableNameGuid));
            if (!TableOperations.ContainsKey(localeAndGuid))
            {
                // We acquire when using the guid.
                AddressablesInterface.Acquire(tableOperation);
                TableOperations[localeAndGuid] = tableOperation;
            }
        }

        /// <summary>
        /// Returns the Default table.
        /// This method is asynchronous and may not have an immediate result.
        /// Check [IsDone](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.IsDone) to see if the data is available,
        /// 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,
        /// yield on the operation or call [WaitForCompletion](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion)
        /// to force the operation to complete.
        /// </summary>
        /// <returns></returns>
        public AsyncOperationHandle<TTable> GetDefaultTableAsync()
        {
            return GetTableAsync(GetDefaultTable());
        }

        /// <summary>
        /// Returns the named table.
        /// This method is asynchronous and may not have an immediate result.
        /// Check [IsDone](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.IsDone) to see if the data is available,
        /// 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,
        /// yield on the operation or call [WaitForCompletion](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion)
        /// to force the operation to complete.
        /// </summary>
        /// <remarks>
        /// Internally the following is performed when a table is requested:
        /// ![](../manual/images/scripting/GetTable.dot.svg)
        /// </remarks>
        /// <param name="tableReference">The table identifier. Can be either the name of the table or the table collection name Guid.</param>
        /// <param name="locale">The <see cref="Locale"/> to load the table from, use null to default to <see cref="LocalizationSettings.SelectedLocale"/>.</param>
        /// <returns></returns>
        public virtual AsyncOperationHandle<TTable> GetTableAsync(TableReference tableReference, Locale locale = null)
        {
            // Extract the Locale Id or use a placeholder if we are using the selected locale and it is not ready yet.
            bool localeAvailable = locale != null || LocalizationSettings.SelectedLocaleAsync.IsDone;
            bool useSelectedLocalePlaceholder = true;
            if (localeAvailable)
            {
                if (locale == null)
                {
                    if (LocalizationSettings.SelectedLocaleAsync.Result == null)
                        return AddressablesInterface.ResourceManager.CreateCompletedOperation<TTable>(null, "SelectedLocale is null. Database could not get table.");
                    locale = LocalizationSettings.SelectedLocaleAsync.Result;
                }
                useSelectedLocalePlaceholder = false;
            }

            // Do we have a cached operation already running?
            tableReference.Validate();
            var tableIdString = tableReference.ReferenceType == TableReference.Type.Guid ? TableReference.StringFromGuid(tableReference.TableCollectionNameGuid) : tableReference.TableCollectionName;
            var localeId = useSelectedLocalePlaceholder ? k_SelectedLocaleId : locale.Identifier;
            if (TableOperations.TryGetValue((localeId, tableIdString), out var operationHandle))
                return operationHandle;

            // Start a new operation
            var operation = CreateLoadTableOperation();
            operation.Init(this, tableReference, locale);
            operation.Dependency = LocalizationSettings.InitializationOperation;
            var handle = AddressablesInterface.ResourceManager.StartOperation(operation, LocalizationSettings.InitializationOperation);

            if (useSelectedLocalePlaceholder || tableReference.ReferenceType == TableReference.Type.Guid)
            {
                // When using a Guid we increment the reference count.
                // We do not increment for placeholders as we only ever have 1 reference for them, we dont share it between
                // table name and guid, because the register operation will use the actual selected locale and not the placeholder.
                // We treat the table name as default and do not increment for that one.
                if (!useSelectedLocalePlaceholder)
                    AddressablesInterface.Acquire(handle);
                TableOperations[(localeId, tableIdString)] = handle;
            }
            else
            {
                // Register the table name and Guid
                RegisterTableNameOperation(handle);
            }

            // Register the table operation later. This will fully register everything including shared table data, table name and guid.
            RegisterCompletedTableOperation(handle);

            return handle;
        }

        /// <summary>
        /// Returns the named table.
        /// Uses [WaitForCompletion](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion) to force the loading to complete synchronously.
        /// Please note that [WaitForCompletion](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion) is not supported on
        /// [WebGL](https://docs.unity3d.com/Packages/com.unity.addressables@latest/index.html?subfolder=/manual/SynchronousAddressables.html#webgl).
        /// </summary>
        /// <param name="tableReference">The table identifier. Can be either the name of the table or the table collection name Guid.</param>
        /// <param name="locale">The <see cref="Locale"/> to load the table from, use null to default to cref="LocalizationSettings.SelectedLocale"/>.</param>
        /// <returns></returns>
        public virtual TTable GetTable(TableReference tableReference, Locale locale = null) => GetTableAsync(tableReference, locale).WaitForCompletion();

        /// <summary>
        /// Preloads the selected table. If the table is an <see cref="AssetTable"/> its assets will also be loaded.
        /// Check [IsDone](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.IsDone) to see if the data is available,
        /// 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,
        /// yield on the operation or call [WaitForCompletion](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion)
        /// to force the operation to complete.
        /// </summary>
        /// <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>
        /// <param name="locale">The <see cref="Locale"/> to use instead of the default <see cref="LocalizationSettings.SelectedLocale"/></param>
        /// <returns></returns>
        public AsyncOperationHandle PreloadTables(TableReference tableReference, Locale locale = null)
        {
            // Start a new operation
            var operation = CreatePreloadTablesOperation();
            operation.Init(this, new[] { tableReference }, locale);
            operation.Dependency = LocalizationSettings.InitializationOperation;
            var handle = AddressablesInterface.ResourceManager.StartOperation(operation, LocalizationSettings.InitializationOperation);

            if (LocalizationSettings.Instance.IsPlaying)
                handle.CompletedTypeless += ReleaseNextFrame;

            return handle;
        }

        /// <summary>
        /// Preloads the matching tables for the selected Locale. If the tables are <see cref="AssetTable"/> then their assets will also be loaded.
        /// Check [IsDone](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.IsDone) to see if the data is available,
        /// 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,
        /// yield on the operation or call [WaitForCompletion](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion)
        /// to force the operation to complete.
        /// </summary>
        /// <param name="tableReferences">An IList of tableReferences to check for the string.</param>
        /// <param name="locale">The <see cref="Locale"/> to use instead of the default <see cref="LocalizationSettings.SelectedLocale"/></param>
        /// <returns></returns>
        /// <example>
        /// This shows how to manually preload tables instead of marking them as Preload in the editor.
        /// <code source="../../../DocCodeSamples.Tests/LocalizedStringDatabaseSamples.cs" region="preload-example"/>
        /// </example>
        public AsyncOperationHandle PreloadTables(IList<TableReference> tableReferences, Locale locale = null)
        {
            // Start a new operation
            var operation = CreatePreloadTablesOperation();
            operation.Init(this, tableReferences, locale);
            operation.Dependency = LocalizationSettings.InitializationOperation;
            var handle = AddressablesInterface.ResourceManager.StartOperation(operation, LocalizationSettings.InitializationOperation);

            if (LocalizationSettings.Instance.IsPlaying)
                handle.CompletedTypeless += ReleaseNextFrame;

            return handle;
        }

        /// <summary>
        /// Releases all tables that are currently loaded in the database.
        /// 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.
        /// </summary>
        /// <param name="locale">The <see cref="Locale"/> to release tables for, when <see langword="null"/> all locales will be released.</param>
        public void ReleaseAllTables(Locale locale = null)
        {
            using (HashSetPool<TTable>.Get(out var releasedTables))
            {
                foreach (var to in TableOperations.Values)
                {
                    if (!to.IsValid())
                        continue;

                    if (locale != null && to.Result.LocaleIdentifier != locale.Identifier)
                        continue;

                    // We may have multiple references to the table so we keep track in order to only call release once.
                    if (to.Result != null && !releasedTables.Contains(to.Result))
                    {
                        ReleaseTableContents(to.Result);
                        releasedTables.Add(to.Result);
                    }
                    AddressablesInterface.Release(to);
                }
            }

            foreach (var shared in SharedTableDataOperations)
            {
                AddressablesInterface.SafeRelease(shared.Value);
            }
            SharedTableDataOperations.Clear();

            if (m_PreloadOperationHandle.IsValid())
            {
                //Debug.Assert(m_PreloadOperationHandle.IsDone, "Disposing an incomplete preload operation");

                if (m_PreloadOperationHandle.IsDone)
                    AddressablesInterface.Release(m_PreloadOperationHandle);
                m_PreloadOperationHandle = default;
            }

            TableOperations.Clear();
        }

        /// <summary>
        /// Releases all references to the table that matches the <paramref name="tableReference"/> and <paramref name="locale"/>.
        /// 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.
        /// A table is released by calling <see cref="AddressableAssets.Addressables.Release"/> on it which decrements the ref-count.
        /// When a given Asset's ref-count is zero, that Asset is ready to be unloaded.
        /// 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).
        /// </summary>
        /// <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>
        /// <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>
        /// <example>
        /// This shows how to release a table but prevent it from being unloaded.
        /// <code source="../../../DocCodeSamples.Tests/LocalizedStringDatabaseSamples.cs" region="release-example"/>
        /// </example>
        public void ReleaseTable(TableReference tableReference, Locale locale = null)
        {
            tableReference.Validate();
            var usingSelectedLocale = locale == LocalizationSettings.SelectedLocaleAsync.Result;
            if (locale == null)
            {
                locale = LocalizationSettings.SelectedLocaleAsync.Result;
                usingSelectedLocale = true;
                if (locale == null)
                    return;
            }

            // Get the shared table data
            SharedTableData sharedTableData;
            if (tableReference.ReferenceType == TableReference.Type.Guid)
            {
                if (!SharedTableDataOperations.TryGetValue(tableReference.TableCollectionNameGuid, out var sharedTableDataOperationHandle) || sharedTableDataOperationHandle.Result == null)
                    return;
                sharedTableData = sharedTableDataOperationHandle.Result;
            }
            else
            {
                var nameAndLocale = (locale.Identifier, tableReference.TableCollectionName);
                if (!TableOperations.TryGetValue(nameAndLocale, out var operationHandleName) || operationHandleName.Result == null)
                    return;
                sharedTableData = operationHandleName.Result.SharedData;
            }

            if (sharedTableData == null)
                return;

            // We may have multiple references to the table(Guid, Table name, placeholders etc) so we will iterate through and remove them all.
            // We also need to see if the Shared table data is still being used or if we can also release that.
            int sharedTableDataUsers = 0;
            bool removedContents = false;
            using (ListPool<(LocaleIdentifier localeIdentifier, string tableNameOrGuid)>.Get(out var itemsToRemove))
            {
                foreach (var tableOperation in TableOperations)
                {
                    if (!tableOperation.Value.IsValid() || tableOperation.Value.Result == null || tableOperation.Value.Result.SharedData != sharedTableData)
                        continue;

                    // Check locale and placeholder
                    if (tableOperation.Key.localeIdentifier == locale.Identifier || usingSelectedLocale && tableOperation.Key.localeIdentifier == k_SelectedLocaleId)
                    {
                        // We only want to do this once.
                        if (!removedContents)
                        {
                            ReleaseTableContents(tableOperation.Value.Result);
                            removedContents = true;
                        }

                        AddressablesInterface.SafeRelease(tableOperation.Value);

                        itemsToRemove.Add(tableOperation.Key);
                    }
                    else
                    {
                        sharedTableDataUsers++;
                    }
                }

                // Remove the items from the dictionary
                foreach (var tableKey in itemsToRemove)
                {
                    TableOperations.Remove(tableKey);
                }

                // If there's no other references to the shared table data then we can also remove that.
                if (sharedTableDataUsers == 0 && SharedTableDataOperations.TryGetValue(sharedTableData.TableCollectionNameGuid, out var sharedTableDataOperationHandle))
                {
                    AddressablesInterface.SafeRelease(sharedTableDataOperationHandle);
                    SharedTableDataOperations.Remove(sharedTableData.TableCollectionNameGuid);
                }
            }
        }

        /// <summary>
        /// Returns all the tables available.
        /// This method is asynchronous and may not have an immediate result.
        /// Check [IsDone](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.IsDone) to see if the tables are available.
        /// 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,
        /// yield on the operation or call [WaitForCompletion](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion)
        /// to force the operation to complete.```
        /// </summary>
        /// <param name="locale">The <see cref="Locale"/> to load the table from, use null to default to cref="LocalizationSettings.SelectedLocale"/>.</param>
        /// <returns></returns>
        public virtual AsyncOperationHandle<IList<TTable>> GetAllTables(Locale locale = null)
        {
            var operation = GenericPool<LoadAllTablesOperation<TTable, TEntry>>.Get();
            operation.Init(this, locale);
            operation.Dependency = LocalizationSettings.InitializationOperation;
            var handle = AddressablesInterface.ResourceManager.StartOperation(operation, LocalizationSettings.InitializationOperation);

            if (LocalizationSettings.Instance.IsPlaying)
                handle.CompletedTypeless += ReleaseNextFrame;

            return handle;
        }

        /// <summary>
        /// Checks if the table is currently loaded or not.
        /// </summary>
        /// <param name="tableReference">The table identifier. Can be either the name of the table or the table collection name Guid.</param>
        /// <param name="locale">The <see cref="Locale"/> to load the table from, use null to default to cref="LocalizationSettings.SelectedLocale"/>.</param>
        /// <returns></returns>
        public virtual bool IsTableLoaded(TableReference tableReference, Locale locale = null)
        {
            var tableIdString = tableReference.ReferenceType == TableReference.Type.Guid ? TableReference.StringFromGuid(tableReference.TableCollectionNameGuid) : tableReference.TableCollectionName;
            var localeAndName = locale != null ? (locale.Identifier, tableIdString) : (LocalizationSettings.SelectedLocaleAsync.Result.Identifier, tableIdString);
            if (TableOperations.TryGetValue(localeAndName, out var TableOperationHandle))
                return TableOperationHandle.Status == AsyncOperationStatus.Succeeded;
            else
                return false;
        }

        internal virtual LoadTableOperation<TTable, TEntry> CreateLoadTableOperation() => GenericPool<LoadTableOperation<TTable, TEntry>>.Get();
        internal virtual PreloadTablesOperation<TTable, TEntry> CreatePreloadTablesOperation() => GenericPool<PreloadTablesOperation<TTable, TEntry>>.Get();

        /// <summary>
        /// Returns the entry from the requested table. A table entry will contain the localized item and metadata.
        /// This method is asynchronous and may not have an immediate result.
        /// Check [IsDone](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.IsDone) to see if the data is available,
        /// 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,
        /// yield on the operation or call [WaitForCompletion](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion)
        /// to force the operation to complete.
        /// 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.
        /// If you do plan to keep hold of the handle after completion then you should call [Acquire](xref::UnityEngine.ResourceManagement.AsyncOperationHandle.Acquire)
        /// to prevent the operation being reused and <see cref="AddressableAssets.Addressables.Release(AsyncOperationHandle)"/> to finally return the operation back to the pool.
        /// </summary>
        /// <remarks>
        /// Internally the following is performed when an Entry is requested.
        /// First the table will be requested using <see cref="GetTableAsync(TableReference, Locale)"/>.
        /// Once the table is loaded the entry will be extracted like so:
        /// ![](../manual/images/GetEntry.dot.svg)
        /// </remarks>
        /// <param name="tableReference">The table identifier. Can be either the name of the table or the table collection name Guid.</param>
        /// <param name="tableEntryReference">A reference to the entry in the table.</param>
        /// <param name="locale">The <see cref="Locale"/> to load the table from. Null will use <see cref="LocalizationSettings.SelectedLocale"/>.</param>
        /// <param name="fallbackBehavior">A Enum which determines if a Fallback should be used when no value could be found for the Locale.</param>
        /// <returns></returns>
        public virtual AsyncOperationHandle<TableEntryResult> GetTableEntryAsync(TableReference tableReference, TableEntryReference tableEntryReference, Locale locale = null, FallbackBehavior fallbackBehavior = FallbackBehavior.UseProjectSettings)
        {
            var loadTableOperation = GetTableAsync(tableReference, locale);
            var getTableEntryOperation = GenericPool<GetTableEntryOperation<TTable, TEntry>>.Get();
            var useFallback = fallbackBehavior != FallbackBehavior.UseProjectSettings ? fallbackBehavior == FallbackBehavior.UseFallback : UseFallback;

            getTableEntryOperation.Init(this, loadTableOperation, tableReference, tableEntryReference, locale, useFallback, true);
            getTableEntryOperation.Dependency = loadTableOperation;
            var handle = AddressablesInterface.ResourceManager.StartOperation(getTableEntryOperation, loadTableOperation);

            return handle;
        }

        /// <summary>
        /// Returns the entry from the requested table. A table entry will contain the localized item and metadata.
        /// Uses [WaitForCompletion](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion) to force the loading to complete synchronously.
        /// Please note that [WaitForCompletion](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion) is not supported on
        /// [WebGL](https://docs.unity3d.com/Packages/com.unity.addressables@latest/index.html?subfolder=/manual/SynchronousAddressables.html#webgl).
        /// </summary>
        /// <param name="tableReference">The table identifier. Can be either the name of the table or the table collection name Guid.</param>
        /// <param name="tableEntryReference">A reference to the entry in the table.</param>
        /// <param name="locale">The <see cref="Locale"/> to load the table from. Null will use <see cref="LocalizationSettings.SelectedLocale"/>.</param>
        /// <param name="fallbackBehavior">A Enum which determines if a Fallback should be used when no value could be found for the Locale.</param>
        /// <returns>The table entry result which contains the table </returns>
        public virtual TableEntryResult GetTableEntry(TableReference tableReference, TableEntryReference tableEntryReference, Locale locale = null, FallbackBehavior fallbackBehavior = FallbackBehavior.UseProjectSettings)
        {
            return GetTableEntryAsync(tableReference, tableEntryReference, locale, fallbackBehavior).WaitForCompletion();
        }

        internal AsyncOperationHandle<SharedTableData> GetSharedTableData(Guid tableNameGuid)
        {
            if (SharedTableDataOperations.TryGetValue(tableNameGuid, out var sharedTableDataOp))
                return sharedTableDataOp;

            sharedTableDataOp = AddressablesInterface.LoadAssetFromGUID<SharedTableData>(TableReference.StringFromGuid(tableNameGuid));
            SharedTableDataOperations[tableNameGuid] = sharedTableDataOp;
            return sharedTableDataOp;
        }

        internal virtual void ReleaseTableContents(TTable table) {}

        /// <summary>
        /// Called before the LocaleChanged event is sent out in order to give the database a chance to prepare.
        /// </summary>
        /// <param name="locale"></param>
        public virtual void OnLocaleChanged(Locale locale)
        {
            ReleaseAllTables();
        }

        void PatchTableContents(AsyncOperationHandle<TTable> tableOperation)
        {
            // This should only be called once, after the table has loaded. It gives users the opurtunity to patch a Localized table.
            // For example you may want to read in some extra data from a csv file after the game has been built.
            if (TablePostprocessor != null && tableOperation.Result != null)
                TablePostprocessor.PostprocessTable(tableOperation.Result);
        }

        /// <summary>
        /// Resets the state of the provider by removing all the cached tables and clearing the preload operation.
        /// </summary>
        public void ResetState()
        {
            ReleaseAllTables();
        }

        /// <summary>
        /// Calls <see cref="ReleaseAllTables(Locale)"/>..
        /// </summary>
        void IDisposable.Dispose()
        {
            ReleaseAllTables();
        }
    }
}
1 Like

Thanks. Your example project is reproducing the issue for me so that will give me something to debug against. I dont think your fix will be suitable for us though, it looks like it could assign the shared table data to the wrong table. I created a bug Unity Issue Tracker - NullReferenceException: The Table &quot;xyz_en&quot; does not have a SharedTableData

1 Like

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:

private void OnStringTableChanged(StringTable table)
{
    var operation = Addressables.LoadAssetAsync<SharedTableData>(table.SharedData.TableCollectionNameGuid.ToString("N"));
    Addressables.ResourceManager.Acquire(operation);
}

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 Unity Issue Tracker - [Addressables/WebGL] Unloading and reloading 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)

9467816–1330499–com.unity.localization-14-49-23.patch.zip (1.38 KB)

2 Likes

You my hero!
After half year problem still alive.

1 Like