diff --git a/Assets/PerfectWorld/Scripts/Addressable/AddressablesLabelDebug.cs b/Assets/PerfectWorld/Scripts/Addressable/AddressablesLabelDebug.cs
new file mode 100644
index 0000000000..4576e3e869
--- /dev/null
+++ b/Assets/PerfectWorld/Scripts/Addressable/AddressablesLabelDebug.cs
@@ -0,0 +1,215 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Cysharp.Threading.Tasks;
+using UnityEngine;
+using UnityEngine.AddressableAssets;
+using UnityEngine.AddressableAssets.ResourceLocators;
+using UnityEngine.ResourceManagement.AsyncOperations;
+using UnityEngine.ResourceManagement.ResourceLocations;
+using UnityEngine.ResourceManagement.ResourceProviders;
+
+namespace BrewMonster.Scripts
+{
+ ///
+ /// Runtime diagnostics for Addressables labels (e.g. RemoteContent) — log with [Cuong] prefix.
+ ///
+ public static class AddressablesLabelDebug
+ {
+ const string LogPrefix = "[Cuong] AddressablesLabelDebug:";
+
+ ///
+ /// After , logs locators, label lookup (exact + case variants),
+ /// and sample remote InternalIds. Labels are case-sensitive in Addressables.
+ ///
+ public static async UniTask LogLabelDiagnosticsAsync(string primaryLabel, int maxSampleLocations = 5)
+ {
+ if (string.IsNullOrWhiteSpace(primaryLabel))
+ {
+ Debug.LogWarning($"{LogPrefix} primaryLabel is empty — skip.");
+ return;
+ }
+
+ var label = primaryLabel.Trim();
+ var sb = new StringBuilder(2048);
+ sb.AppendLine($"{LogPrefix} === Label diagnostics (runtime catalog) ===");
+ sb.AppendLine($"{LogPrefix} Primary label (exact, case-sensitive): \"{label}\"");
+
+ int locatorCount = 0;
+ if (Addressables.ResourceLocators != null)
+ {
+ foreach (IResourceLocator locator in Addressables.ResourceLocators)
+ {
+ locatorCount++;
+ var id = locator?.LocatorId ?? "(null)";
+ var keyCount = 0;
+ if (locator?.Keys != null)
+ {
+ foreach (var _ in locator.Keys)
+ keyCount++;
+ }
+
+ sb.AppendLine($"{LogPrefix} Locator #{locatorCount}: id={id}, keys~={keyCount}");
+ }
+ }
+
+ if (locatorCount == 0)
+ sb.AppendLine($"{LogPrefix} WARNING: No ResourceLocators — catalog chưa load hoặc init thất bại.");
+
+ await LogLabelLookupAsync(sb, label, maxSampleLocations);
+
+ var wrongCase = char.IsUpper(label[0])
+ ? char.ToLowerInvariant(label[0]) + label.Substring(1)
+ : char.ToUpperInvariant(label[0]) + label.Substring(1);
+ if (!string.Equals(wrongCase, label, StringComparison.Ordinal))
+ {
+ sb.AppendLine($"{LogPrefix} --- Case mismatch test (should be 0 locations if case matters) ---");
+ await LogLabelLookupAsync(sb, wrongCase, 2, suffix: " [wrong case]");
+ }
+
+ await LogAllCatalogLabelsAsync(sb, label);
+
+ sb.AppendLine($"{LogPrefix} Ghi chú: Label trong Addressables Groups ≠ file trên CDN. " +
+ "CDN có catalog_*.json + bundle; label nằm TRONG catalog JSON sau khi build. " +
+ "Group Include In Build = false → entry chỉ có sau khi remote catalog tải thành công.");
+ Debug.Log(sb.ToString());
+ }
+
+ static async UniTask LogLabelLookupAsync(
+ StringBuilder sb,
+ string label,
+ int maxSampleLocations,
+ string suffix = "")
+ {
+ AsyncOperationHandle> handle = default;
+ try
+ {
+ handle = Addressables.LoadResourceLocationsAsync(label, typeof(UnityEngine.Object));
+ await handle.ToUniTask();
+
+ if (handle.Status != AsyncOperationStatus.Succeeded)
+ {
+ sb.AppendLine(
+ $"{LogPrefix} LoadResourceLocationsAsync(\"{label}\"){suffix} FAILED: {handle.OperationException?.Message}");
+ return;
+ }
+
+ var locations = handle.Result;
+ int count = locations?.Count ?? 0;
+ sb.AppendLine($"{LogPrefix} Label \"{label}\"{suffix} → {count} location(s).");
+
+ if (count == 0)
+ return;
+
+ int remoteCount = 0;
+ int sample = Mathf.Min(maxSampleLocations, count);
+ for (int i = 0; i < count; i++)
+ {
+ var loc = locations[i];
+ if (loc == null)
+ continue;
+
+ bool isRemote = IsRemoteLocation(loc);
+ if (isRemote)
+ remoteCount++;
+
+ if (i < sample)
+ {
+ sb.AppendLine(
+ $"{LogPrefix} [{i}] remote={isRemote} provider={loc.ProviderId} id={Truncate(loc.InternalId, 120)}");
+ }
+ }
+
+ sb.AppendLine($"{LogPrefix} … remote locations: {remoteCount}/{count} (rest omitted).");
+ }
+ catch (Exception e)
+ {
+ sb.AppendLine($"{LogPrefix} Label \"{label}\"{suffix} exception: {e.GetType().Name}: {e.Message}");
+ }
+ finally
+ {
+ if (handle.IsValid())
+ Addressables.Release(handle);
+ }
+ }
+
+ static async UniTask LogAllCatalogLabelsAsync(StringBuilder sb, string highlightLabel)
+ {
+ AsyncOperationHandle> handle = default;
+ try
+ {
+ handle = Addressables.GetLabels();
+ await handle.ToUniTask();
+
+ if (handle.Status != AsyncOperationStatus.Succeeded)
+ {
+ sb.AppendLine($"{LogPrefix} GetLabels() failed: {handle.OperationException?.Message}");
+ return;
+ }
+
+ var labels = handle.Result;
+ int n = labels?.Count ?? 0;
+ sb.AppendLine($"{LogPrefix} GetLabels() → {n} label(s) in loaded catalog(s):");
+
+ if (n == 0)
+ {
+ sb.AppendLine($"{LogPrefix} (empty — catalog build cũ hoặc chưa có remote catalog)");
+ return;
+ }
+
+ bool foundHighlight = false;
+ for (int i = 0; i < n; i++)
+ {
+ var l = labels[i];
+ if (string.IsNullOrEmpty(l))
+ continue;
+
+ bool match = string.Equals(l, highlightLabel, StringComparison.Ordinal);
+ bool matchIgnoreCase = string.Equals(l, highlightLabel, StringComparison.OrdinalIgnoreCase);
+ if (match)
+ foundHighlight = true;
+
+ string flag = match ? " <-- EXACT MATCH" :
+ matchIgnoreCase ? " <-- same letters, different case" : "";
+ sb.AppendLine($"{LogPrefix} [{i}] \"{l}\"{flag}");
+ }
+
+ if (!foundHighlight)
+ {
+ sb.AppendLine(
+ $"{LogPrefix} WARNING: \"{highlightLabel}\" NOT in GetLabels() list — " +
+ "InvalidKeyException khi GetDownloadSizeAsync/DownloadDependencies là đúng.");
+ }
+ }
+ catch (Exception e)
+ {
+ sb.AppendLine($"{LogPrefix} GetLabels() exception: {e.GetType().Name}: {e.Message}");
+ }
+ finally
+ {
+ if (handle.IsValid())
+ Addressables.Release(handle);
+ }
+ }
+
+ static bool IsRemoteLocation(IResourceLocation loc)
+ {
+ if (loc == null || string.IsNullOrEmpty(loc.InternalId))
+ return false;
+
+ var id = loc.InternalId;
+ if (id.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
+ id.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
+ return true;
+
+ return loc.ResourceType == typeof(IAssetBundleResource);
+ }
+
+ static string Truncate(string s, int maxLen)
+ {
+ if (string.IsNullOrEmpty(s) || s.Length <= maxLen)
+ return s ?? "";
+ return s.Substring(0, maxLen) + "...";
+ }
+ }
+}