From 5042f44c98de4a3bcb74431e548906a60cc86518 Mon Sep 17 00:00:00 2001 From: Erik Bylund Date: Tue, 22 Sep 2026 11:59:00 +0200 Subject: [PATCH 1/2] fix: replace GetInstanceID with GetEntityId on Unity 6.5+ Unity 6.5+ obsoletes Object.GetInstanceID with a compile error (CS0619), which broke the SDK on Unity 6.5/6.6. Guard the migration behind UNITY_6000_5_OR_NEWER so older supported editors keep working. Also add tests locking in the IANA timezone casing/sign convention produced by LootLockerTimezoneConverter (Etc/GMT, not ETC/GMT). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Runtime/Client/LootLockerLifecycleManager.cs | 29 ++++++++-- .../PlayMode/TimezoneConverterTests.cs | 57 +++++++++++++++++++ .../PlayMode/TimezoneConverterTests.cs.meta | 11 ++++ 3 files changed, 91 insertions(+), 6 deletions(-) create mode 100644 Tests/LootLockerTests/PlayMode/TimezoneConverterTests.cs create mode 100644 Tests/LootLockerTests/PlayMode/TimezoneConverterTests.cs.meta diff --git a/Runtime/Client/LootLockerLifecycleManager.cs b/Runtime/Client/LootLockerLifecycleManager.cs index 65a2c6fb5..f63dd2f39 100644 --- a/Runtime/Client/LootLockerLifecycleManager.cs +++ b/Runtime/Client/LootLockerLifecycleManager.cs @@ -46,7 +46,11 @@ public class LootLockerLifecycleManager : MonoBehaviour #region Instance Handling private static LootLockerLifecycleManager _instance; - private static int _instanceId = 0; +#if UNITY_6000_5_OR_NEWER + private static EntityId _instanceId = default; +#else + private static int _instanceId = default; +#endif private static GameObject _hostingGameObject = null; private static readonly object _instanceLock = new object(); @@ -70,7 +74,7 @@ private static void AutoInitialize() { Debug.LogWarning($"[LootLocker] LifecycleManager auto-initialization failed: {ex.Message}. Services will be initialized on first use."); _instance = null; - _instanceId = 0; + _instanceId = default; if (_hostingGameObject != null) { Destroy(_hostingGameObject); @@ -115,7 +119,11 @@ private static void Instantiate() { var gameObject = new GameObject("LootLockerLifecycleManager"); _instance = gameObject.AddComponent(); +#if UNITY_6000_5_OR_NEWER + _instanceId = _instance.GetEntityId(); +#else _instanceId = _instance.GetInstanceID(); +#endif _hostingGameObject = gameObject; if (Application.isPlaying) @@ -150,7 +158,7 @@ private static void TeardownInstance() #endif _instance = null; - _instanceId = 0; + _instanceId = default; _hostingGameObject = null; } @@ -168,13 +176,22 @@ public static IEnumerator CleanUpOldInstances() #endif foreach (LootLockerLifecycleManager manager in managers) { - if (manager != null && _instanceId != manager.GetInstanceID() && manager.gameObject != null && ((LootLockerLifecycleManager)manager)._isInitialized) + if (manager != null && manager.gameObject != null && ((LootLockerLifecycleManager)manager)._isInitialized) { + bool isCurrentInstance; +#if UNITY_6000_5_OR_NEWER + isCurrentInstance = _instanceId == manager.GetEntityId(); +#else + isCurrentInstance = _instanceId == manager.GetInstanceID(); +#endif + if (!isCurrentInstance) + { #if UNITY_EDITOR - DestroyImmediate(manager.gameObject); + DestroyImmediate(manager.gameObject); #else - Destroy(manager.gameObject); + Destroy(manager.gameObject); #endif + } } } yield return null; diff --git a/Tests/LootLockerTests/PlayMode/TimezoneConverterTests.cs b/Tests/LootLockerTests/PlayMode/TimezoneConverterTests.cs new file mode 100644 index 000000000..da1c1d8f2 --- /dev/null +++ b/Tests/LootLockerTests/PlayMode/TimezoneConverterTests.cs @@ -0,0 +1,57 @@ +using LootLocker; +using NUnit.Framework; + +namespace LootLockerTests.PlayMode +{ + /// + /// Guards against regressions in the IANA timezone strings produced by the SDK. + /// The backend rejects invalid IANA timezones (e.g. wrong casing such as "ETC/GMT") + /// with "invalid JSON input" errors when starting a session. + /// + public class TimezoneConverterTests + { + [Test, Category("LootLocker"), Category("LootLockerCIFast")] + public void ConvertUTCOffsetToIana_UsesCorrectCasingAndSign() + { + // IANA sign convention is reversed: Etc/GMT+X means UTC-X + Assert.AreEqual("Etc/GMT+5", LootLockerTimezoneConverter.convertUTCOffsetToIanaTzString(-5)); + Assert.AreEqual("Etc/GMT-2", LootLockerTimezoneConverter.convertUTCOffsetToIanaTzString(2)); + Assert.AreEqual("Etc/UTC", LootLockerTimezoneConverter.convertUTCOffsetToIanaTzString(0)); + // Out-of-range offsets are clamped + Assert.AreEqual("Etc/GMT+12", LootLockerTimezoneConverter.convertUTCOffsetToIanaTzString(-999)); + Assert.AreEqual("Etc/GMT-14", LootLockerTimezoneConverter.convertUTCOffsetToIanaTzString(999)); + } + + [Test, Category("LootLocker"), Category("LootLockerCIFast")] + public void ConvertGMTOffsetToIana_UsesCorrectCasingAndSign() + { + Assert.AreEqual("Etc/GMT-5", LootLockerTimezoneConverter.convertGMTOffsetToIanaTzString(-5)); + Assert.AreEqual("Etc/GMT+2", LootLockerTimezoneConverter.convertGMTOffsetToIanaTzString(2)); + Assert.AreEqual("Etc/UTC", LootLockerTimezoneConverter.convertGMTOffsetToIanaTzString(0)); + } + + [Test, Category("LootLocker"), Category("LootLockerCIFast")] + public void TryConvertStringToIana_HandlesOffsetsWindowsAndIanaInputs() + { + // Numeric offset string + Assert.IsTrue(LootLockerTimezoneConverter.TryConvertStringToIanaTzString("2", out string fromOffset)); + Assert.AreEqual("Etc/GMT-2", fromOffset); + + // IANA passthrough + Assert.IsTrue(LootLockerTimezoneConverter.TryConvertStringToIanaTzString("Asia/Tokyo", out string iana)); + Assert.AreEqual("Asia/Tokyo", iana); + + // Windows timezone converted + Assert.IsTrue(LootLockerTimezoneConverter.TryConvertStringToIanaTzString("Romance Standard Time", out string fromWindows)); + Assert.AreEqual("Europe/Paris", fromWindows); + + // Unknown input falls back to Etc/UTC (correctly cased) + Assert.IsFalse(LootLockerTimezoneConverter.TryConvertStringToIanaTzString("NotATimezone", out string fallback)); + Assert.AreEqual("Etc/UTC", fallback); + + // Empty input falls back to Etc/UTC + Assert.IsFalse(LootLockerTimezoneConverter.TryConvertStringToIanaTzString("", out string emptyFallback)); + Assert.AreEqual("Etc/UTC", emptyFallback); + } + } +} diff --git a/Tests/LootLockerTests/PlayMode/TimezoneConverterTests.cs.meta b/Tests/LootLockerTests/PlayMode/TimezoneConverterTests.cs.meta new file mode 100644 index 000000000..799f7fd45 --- /dev/null +++ b/Tests/LootLockerTests/PlayMode/TimezoneConverterTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f237609cb88f46e0bb05d583a91dba17 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From 0a1a363fb2747f55d36197f82ee10d95836ef40b Mon Sep 17 00:00:00 2001 From: Erik Bylund Date: Tue, 22 Sep 2026 14:32:31 +0200 Subject: [PATCH 2/2] Add LootLockerCI category and guard clamping asserts in timezone tests - Tag tests with the LootLockerCI category so they run on PRs targeting main. - The out-of-range clamping assertions only hold from UNITY_2021_1_OR_NEWER, since the converter only clamps under that define; guard them accordingly. --- .../PlayMode/TimezoneConverterTests.cs | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/Tests/LootLockerTests/PlayMode/TimezoneConverterTests.cs b/Tests/LootLockerTests/PlayMode/TimezoneConverterTests.cs index da1c1d8f2..8e643e7f4 100644 --- a/Tests/LootLockerTests/PlayMode/TimezoneConverterTests.cs +++ b/Tests/LootLockerTests/PlayMode/TimezoneConverterTests.cs @@ -10,28 +10,30 @@ namespace LootLockerTests.PlayMode /// public class TimezoneConverterTests { - [Test, Category("LootLocker"), Category("LootLockerCIFast")] + [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public void ConvertUTCOffsetToIana_UsesCorrectCasingAndSign() { // IANA sign convention is reversed: Etc/GMT+X means UTC-X Assert.AreEqual("Etc/GMT+5", LootLockerTimezoneConverter.convertUTCOffsetToIanaTzString(-5)); Assert.AreEqual("Etc/GMT-2", LootLockerTimezoneConverter.convertUTCOffsetToIanaTzString(2)); Assert.AreEqual("Etc/UTC", LootLockerTimezoneConverter.convertUTCOffsetToIanaTzString(0)); - // Out-of-range offsets are clamped - Assert.AreEqual("Etc/GMT+12", LootLockerTimezoneConverter.convertUTCOffsetToIanaTzString(-999)); - Assert.AreEqual("Etc/GMT-14", LootLockerTimezoneConverter.convertUTCOffsetToIanaTzString(999)); - } + #if UNITY_2021_1_OR_NEWER + // Out-of-range offsets are clamped (the converter only clamps from UNITY_2021_1_OR_NEWER) + Assert.AreEqual("Etc/GMT+12", LootLockerTimezoneConverter.convertUTCOffsetToIanaTzString(-999)); + Assert.AreEqual("Etc/GMT-14", LootLockerTimezoneConverter.convertUTCOffsetToIanaTzString(999)); + #endif + } - [Test, Category("LootLocker"), Category("LootLockerCIFast")] - public void ConvertGMTOffsetToIana_UsesCorrectCasingAndSign() - { - Assert.AreEqual("Etc/GMT-5", LootLockerTimezoneConverter.convertGMTOffsetToIanaTzString(-5)); - Assert.AreEqual("Etc/GMT+2", LootLockerTimezoneConverter.convertGMTOffsetToIanaTzString(2)); - Assert.AreEqual("Etc/UTC", LootLockerTimezoneConverter.convertGMTOffsetToIanaTzString(0)); - } + [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] + public void ConvertGMTOffsetToIana_UsesCorrectCasingAndSign() + { + Assert.AreEqual("Etc/GMT-5", LootLockerTimezoneConverter.convertGMTOffsetToIanaTzString(-5)); + Assert.AreEqual("Etc/GMT+2", LootLockerTimezoneConverter.convertGMTOffsetToIanaTzString(2)); + Assert.AreEqual("Etc/UTC", LootLockerTimezoneConverter.convertGMTOffsetToIanaTzString(0)); + } - [Test, Category("LootLocker"), Category("LootLockerCIFast")] - public void TryConvertStringToIana_HandlesOffsetsWindowsAndIanaInputs() + [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] + public void TryConvertStringToIana_HandlesOffsetsWindowsAndIanaInputs() { // Numeric offset string Assert.IsTrue(LootLockerTimezoneConverter.TryConvertStringToIanaTzString("2", out string fromOffset));