From 7020b7f78815267c99bb794ba7a827fd6acb05e6 Mon Sep 17 00:00:00 2001 From: tudor <7089284+tudddorrr@users.noreply.github.com> Date: Mon, 9 Mar 2026 20:45:48 +0000 Subject: [PATCH 1/4] add prop array support --- .../Talo/Runtime/Entities/EntityWithProps.cs | 126 ++++++++++++++++- .../Talo/Runtime/Entities/Player.cs | 41 ++++++ .../Talo/Runtime/Utils/SocketException.cs | 2 +- .../Talo/Tests/EntityWithProps.meta | 8 ++ .../EntityWithProps/DeletePropArrayTest.cs | 74 ++++++++++ .../DeletePropArrayTest.cs.meta | 2 + .../Tests/EntityWithProps/DeletePropTest.cs | 52 +++++++ .../EntityWithProps/DeletePropTest.cs.meta | 2 + .../Tests/EntityWithProps/GetPropArrayTest.cs | 67 +++++++++ .../EntityWithProps/GetPropArrayTest.cs.meta | 2 + .../Talo/Tests/EntityWithProps/GetPropTest.cs | 56 ++++++++ .../Tests/EntityWithProps/GetPropTest.cs.meta | 2 + .../InsertIntoPropArrayTest.cs | 114 +++++++++++++++ .../InsertIntoPropArrayTest.cs.meta | 2 + .../RemoveFromPropArrayTest.cs | 124 +++++++++++++++++ .../RemoveFromPropArrayTest.cs.meta | 2 + .../Tests/EntityWithProps/SetPropArrayTest.cs | 130 ++++++++++++++++++ .../EntityWithProps/SetPropArrayTest.cs.meta | 2 + .../Talo/Tests/EntityWithProps/SetPropTest.cs | 44 ++++++ .../Tests/EntityWithProps/SetPropTest.cs.meta | 2 + .../Talo/Tests/LiveConfig/GetPropTest.cs | 6 +- 21 files changed, 849 insertions(+), 11 deletions(-) create mode 100644 Assets/Talo Game Services/Talo/Tests/EntityWithProps.meta create mode 100644 Assets/Talo Game Services/Talo/Tests/EntityWithProps/DeletePropArrayTest.cs create mode 100644 Assets/Talo Game Services/Talo/Tests/EntityWithProps/DeletePropArrayTest.cs.meta create mode 100644 Assets/Talo Game Services/Talo/Tests/EntityWithProps/DeletePropTest.cs create mode 100644 Assets/Talo Game Services/Talo/Tests/EntityWithProps/DeletePropTest.cs.meta create mode 100644 Assets/Talo Game Services/Talo/Tests/EntityWithProps/GetPropArrayTest.cs create mode 100644 Assets/Talo Game Services/Talo/Tests/EntityWithProps/GetPropArrayTest.cs.meta create mode 100644 Assets/Talo Game Services/Talo/Tests/EntityWithProps/GetPropTest.cs create mode 100644 Assets/Talo Game Services/Talo/Tests/EntityWithProps/GetPropTest.cs.meta create mode 100644 Assets/Talo Game Services/Talo/Tests/EntityWithProps/InsertIntoPropArrayTest.cs create mode 100644 Assets/Talo Game Services/Talo/Tests/EntityWithProps/InsertIntoPropArrayTest.cs.meta create mode 100644 Assets/Talo Game Services/Talo/Tests/EntityWithProps/RemoveFromPropArrayTest.cs create mode 100644 Assets/Talo Game Services/Talo/Tests/EntityWithProps/RemoveFromPropArrayTest.cs.meta create mode 100644 Assets/Talo Game Services/Talo/Tests/EntityWithProps/SetPropArrayTest.cs create mode 100644 Assets/Talo Game Services/Talo/Tests/EntityWithProps/SetPropArrayTest.cs.meta create mode 100644 Assets/Talo Game Services/Talo/Tests/EntityWithProps/SetPropTest.cs create mode 100644 Assets/Talo Game Services/Talo/Tests/EntityWithProps/SetPropTest.cs.meta diff --git a/Assets/Talo Game Services/Talo/Runtime/Entities/EntityWithProps.cs b/Assets/Talo Game Services/Talo/Runtime/Entities/EntityWithProps.cs index 8595aef..f8043a7 100644 --- a/Assets/Talo Game Services/Talo/Runtime/Entities/EntityWithProps.cs +++ b/Assets/Talo Game Services/Talo/Runtime/Entities/EntityWithProps.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; namespace TaloGameServices @@ -19,24 +20,135 @@ public void SetProp(string key, string value) { props = props.Select((prop) => { - if (prop.key == key) prop.value = value; + if (prop.key == key) + { + prop.value = value; + } + return prop; }).ToArray(); } else { - var propList = props.ToList(); - propList.Add(new Prop((key, value))); - props = propList.ToArray(); + props = props.Append(new Prop((key, value))).ToArray(); } } public void DeleteProp(string key) { - Prop prop = props.FirstOrDefault((prop) => prop.key == key); - if (prop == null) throw new Exception($"Prop with key {key} does not exist"); + var prop = props.FirstOrDefault((prop) => prop.key == key) ?? + throw new Exception($"Prop with key {key} does not exist"); prop.value = null; } + + private string ToArrayKey(string key) + { + if (key.EndsWith("[]")) + { + return key; + } + + return $"{key}[]"; + } + + public IReadOnlyList GetPropArray(string key) + { + var items = props + .Where((prop) => prop.key == ToArrayKey(key) && prop.value != null) + .Select((prop) => prop.value); + + return items.ToList().AsReadOnly(); + } + + private void EnsurePropArraySentinelRemoved(string key) + { + var arrayKey = ToArrayKey(key); + + var hasSentinel = props.Any((prop) => prop.key == arrayKey && prop.value == null); + if (hasSentinel) + { + props = props.Where((prop) => prop.key != arrayKey).ToArray(); + } + } + + public void SetPropArray(string key, IEnumerable values) + { + var validValues = values.Where((value) => !string.IsNullOrEmpty(value)).Distinct().ToList(); + + if (validValues.Count == 0) + { + throw new Exception($"Values for prop array {key} must contain at least one non-empty value"); + } + + var arrayKey = ToArrayKey(key); + props = props.Where((prop) => prop.key != arrayKey).ToArray(); + + props = props.Concat(validValues.Select((value) => new Prop((arrayKey, value)))).ToArray(); + } + + public void DeletePropArray(string key) + { + var arrayKey = ToArrayKey(key); + + if (!props.Any((prop) => prop.key == arrayKey)) + { + throw new Exception($"Prop array with key {key} does not exist"); + } + + props = props + .Where((prop) => prop.key != arrayKey) + // set a single value to null - this ensures the array is cleared + .Append(new Prop((arrayKey, null))) + .ToArray(); + } + + public void InsertIntoPropArray(string key, string value) + { + if (string.IsNullOrEmpty(value)) + { + throw new Exception($"Value for prop array {key} cannot be null or empty"); + } + + var arrayKey = ToArrayKey(key); + + var hasDupe = props.Any((prop) => prop.key == arrayKey && prop.value == value); + if (!hasDupe) + { + EnsurePropArraySentinelRemoved(key); + props = props.Append(new Prop((arrayKey, value))).ToArray(); + } + } + + private void EnsurePropArrayHasSentinel(string key) + { + var hasItems = props + .Where((prop) => prop.key == ToArrayKey(key)) + .Any(); + + if (!hasItems) + { + props = props.Append(new Prop((ToArrayKey(key), null))).ToArray(); + } + } + + public void RemoveFromPropArray(string key, string value) + { + var arrayKey = ToArrayKey(key); + EnsurePropArraySentinelRemoved(key); + + if (!props.Any((prop) => prop.key == arrayKey && prop.value == value)) + { + EnsurePropArrayHasSentinel(key); + throw new Exception($"Value {value} does not exist in prop array {key}"); + } + + props = props.Where((prop) => { + var isMatchingValue = prop.key == arrayKey && prop.value == value; + return !isMatchingValue; + }).ToArray(); + + EnsurePropArrayHasSentinel(key); + } } -} \ No newline at end of file +} diff --git a/Assets/Talo Game Services/Talo/Runtime/Entities/Player.cs b/Assets/Talo Game Services/Talo/Runtime/Entities/Player.cs index 7d3e059..73b4d2a 100644 --- a/Assets/Talo Game Services/Talo/Runtime/Entities/Player.cs +++ b/Assets/Talo Game Services/Talo/Runtime/Entities/Player.cs @@ -1,6 +1,7 @@ using UnityEngine; using System.Linq; using System; +using System.Collections.Generic; namespace TaloGameServices { @@ -37,6 +38,46 @@ public void DeleteProp(string key, bool update = true) } } + public void SetPropArray(string key, IEnumerable values, bool update = true) + { + base.SetPropArray(key, values); + + if (update) + { + Talo.Players.DebounceUpdate(); + } + } + + public void DeletePropArray(string key, bool update = true) + { + base.DeletePropArray(key); + + if (update) + { + Talo.Players.DebounceUpdate(); + } + } + + public void InsertIntoPropArray(string key, string value, bool update = true) + { + base.InsertIntoPropArray(key, value); + + if (update) + { + Talo.Players.DebounceUpdate(); + } + } + + public void RemoveFromPropArray(string key, string value, bool update = true) + { + base.RemoveFromPropArray(key, value); + + if (update) + { + Talo.Players.DebounceUpdate(); + } + } + public bool IsInGroupID(string groupId) { return groups.Any((group) => group.id == groupId); diff --git a/Assets/Talo Game Services/Talo/Runtime/Utils/SocketException.cs b/Assets/Talo Game Services/Talo/Runtime/Utils/SocketException.cs index 6241aa6..9268a3e 100644 --- a/Assets/Talo Game Services/Talo/Runtime/Utils/SocketException.cs +++ b/Assets/Talo Game Services/Talo/Runtime/Utils/SocketException.cs @@ -18,7 +18,7 @@ public enum SocketErrorCode { public class SocketException : Exception { - private SocketError errorData; + private readonly SocketError errorData; public string Req => errorData?.req ?? "unknown"; public SocketErrorCode ErrorCode => GetErrorCode(); diff --git a/Assets/Talo Game Services/Talo/Tests/EntityWithProps.meta b/Assets/Talo Game Services/Talo/Tests/EntityWithProps.meta new file mode 100644 index 0000000..9c0769c --- /dev/null +++ b/Assets/Talo Game Services/Talo/Tests/EntityWithProps.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1e7b30c636e9842449299ce2357f9971 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Talo Game Services/Talo/Tests/EntityWithProps/DeletePropArrayTest.cs b/Assets/Talo Game Services/Talo/Tests/EntityWithProps/DeletePropArrayTest.cs new file mode 100644 index 0000000..c534038 --- /dev/null +++ b/Assets/Talo Game Services/Talo/Tests/EntityWithProps/DeletePropArrayTest.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections; +using System.Linq; +using NUnit.Framework; +using UnityEngine.TestTools; + +namespace TaloGameServices.Test { + internal class DeletePropArrayTest + { + [UnityTest] + public IEnumerator DeletePropArray_ArrayIsEmptyAndSentinelNullExistsInProps() + { + var player = new Player + { + props = new[] { + new Prop(("colours[]", "red")), + new Prop(("colours[]", "blue")) + } + }; + + player.DeletePropArray("colours", false); + + Assert.AreEqual(0, player.GetPropArray("colours").Count); + // ensure the sentinel null value is in there + Assert.IsTrue(player.props.Any((prop) => prop.key == "colours[]" && prop.value == null)); + + yield return null; + } + + [UnityTest] + public IEnumerator DeletePropArray_WhenTheArrayDoesNotExist_ThrowsAnError() + { + var player = new Player + { + props = new[] { + new Prop(("colours[]", "red")), + new Prop(("colours[]", "blue")) + } + }; + + try + { + player.DeletePropArray("sizes", false); + Assert.Fail("Expected exception was not thrown"); + } + catch (Exception ex) + { + Assert.AreEqual(ex.Message, $"Prop array with key sizes does not exist"); + } + + yield return null; + } + + [UnityTest] + public IEnumerator DeletePropArray_AcceptsKeyWithOrWithoutBrackets() + { + var player = new Player + { + props = new[] { + new Prop(("colours[]", "red")), + new Prop(("colours[]", "blue")) + } + }; + + player.DeletePropArray("colours[]", false); + + Assert.AreEqual(0, player.GetPropArray("colours").Count); + // ensure the sentinel null value is in there + Assert.IsTrue(player.props.Any((prop) => prop.key == "colours[]" && prop.value == null)); + + yield return null; + } + } +} diff --git a/Assets/Talo Game Services/Talo/Tests/EntityWithProps/DeletePropArrayTest.cs.meta b/Assets/Talo Game Services/Talo/Tests/EntityWithProps/DeletePropArrayTest.cs.meta new file mode 100644 index 0000000..00f53cc --- /dev/null +++ b/Assets/Talo Game Services/Talo/Tests/EntityWithProps/DeletePropArrayTest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0df551e83888145c69b3d678d344bd06 \ No newline at end of file diff --git a/Assets/Talo Game Services/Talo/Tests/EntityWithProps/DeletePropTest.cs b/Assets/Talo Game Services/Talo/Tests/EntityWithProps/DeletePropTest.cs new file mode 100644 index 0000000..5d919da --- /dev/null +++ b/Assets/Talo Game Services/Talo/Tests/EntityWithProps/DeletePropTest.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections; +using NUnit.Framework; +using UnityEngine.TestTools; + +namespace TaloGameServices.Test { + internal class DeleteProp + { + [UnityTest] + public IEnumerator DeleteProp_WhenThePropExists_SetsTheValueToNull() + { + var player = new Player + { + props = new[] { + new Prop(("key1", "value1")), + new Prop(("key2", "value2")) + } + }; + + player.DeleteProp("key2", false); + + Assert.IsNull(player.GetProp("key2")); + + yield return null; + } + + [UnityTest] + public IEnumerator DeleteProp_WhenThePropDoesNotExist_ThrowsAnError() + { + var player = new Player + { + props = new[] { + new Prop(("key1", "value1")), + new Prop(("key2", "value2")) + } + }; + + try + { + player.DeleteProp("key3", false); + Assert.Fail("Expected exception was not thrown"); + } + catch (Exception ex) + { + Assert.AreEqual(ex.Message, $"Prop with key key3 does not exist"); + } + + yield return null; + } + + } +} diff --git a/Assets/Talo Game Services/Talo/Tests/EntityWithProps/DeletePropTest.cs.meta b/Assets/Talo Game Services/Talo/Tests/EntityWithProps/DeletePropTest.cs.meta new file mode 100644 index 0000000..dd700c3 --- /dev/null +++ b/Assets/Talo Game Services/Talo/Tests/EntityWithProps/DeletePropTest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b98f8dd24460e4244ae518eb72e9f4c6 \ No newline at end of file diff --git a/Assets/Talo Game Services/Talo/Tests/EntityWithProps/GetPropArrayTest.cs b/Assets/Talo Game Services/Talo/Tests/EntityWithProps/GetPropArrayTest.cs new file mode 100644 index 0000000..7508af5 --- /dev/null +++ b/Assets/Talo Game Services/Talo/Tests/EntityWithProps/GetPropArrayTest.cs @@ -0,0 +1,67 @@ +using System.Collections; +using System.Linq; +using NUnit.Framework; +using UnityEngine.TestTools; + +namespace TaloGameServices.Test { + internal class GetPropArrayTest + { + [UnityTest] + public IEnumerator GetPropArray_WithExistingItems_ReturnsAllValues() + { + var player = new Player + { + props = new[] { + new Prop(("colours[]", "red")), + new Prop(("colours[]", "blue")), + new Prop(("colours[]", "green")) + } + }; + + var result = player.GetPropArray("colours").ToList(); + + Assert.AreEqual(3, result.Count); + Assert.Contains("red", result); + Assert.Contains("blue", result); + Assert.Contains("green", result); + + yield return null; + } + + [UnityTest] + public IEnumerator GetPropArray_WithNoMatchingKey_ReturnsEmptyList() + { + var player = new Player + { + props = new[] { + new Prop(("colours[]", "red")) + } + }; + + var result = player.GetPropArray("sizes").ToList(); + + Assert.AreEqual(0, result.Count); + + yield return null; + } + + [UnityTest] + public IEnumerator GetPropArray_AcceptsKeyWithOrWithoutBrackets() + { + var player = new Player + { + props = new[] { + new Prop(("colours[]", "red")), + new Prop(("colours[]", "blue")) + } + }; + + var withBrackets = player.GetPropArray("colours[]").ToList(); + var withoutBrackets = player.GetPropArray("colours").ToList(); + + Assert.AreEqual(withBrackets, withoutBrackets); + + yield return null; + } + } +} diff --git a/Assets/Talo Game Services/Talo/Tests/EntityWithProps/GetPropArrayTest.cs.meta b/Assets/Talo Game Services/Talo/Tests/EntityWithProps/GetPropArrayTest.cs.meta new file mode 100644 index 0000000..a85a681 --- /dev/null +++ b/Assets/Talo Game Services/Talo/Tests/EntityWithProps/GetPropArrayTest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ae0e505d39dc34b1da859569bde11d4d \ No newline at end of file diff --git a/Assets/Talo Game Services/Talo/Tests/EntityWithProps/GetPropTest.cs b/Assets/Talo Game Services/Talo/Tests/EntityWithProps/GetPropTest.cs new file mode 100644 index 0000000..f64e1f3 --- /dev/null +++ b/Assets/Talo Game Services/Talo/Tests/EntityWithProps/GetPropTest.cs @@ -0,0 +1,56 @@ +using System.Collections; +using NUnit.Framework; +using UnityEngine.TestTools; + +namespace TaloGameServices.Test { + internal class GetPropTest + { + [UnityTest] + public IEnumerator GetProp_WithAnExistingKey_ReturnsCorrectValue() + { + var player = new Player + { + props = new[] { + new Prop(("key1", "value1")), + new Prop(("key2", "value2")) + } + }; + + Assert.AreEqual(player.GetProp("key2"), "value2"); + + yield return null; + } + + [UnityTest] + public IEnumerator GetProp_WithAMissingKey_ReturnsNull() + { + var player = new Player + { + props = new[] { + new Prop(("key1", "value1")), + new Prop(("key2", "value2")) + } + }; + + Assert.IsNull(player.GetProp("key3")); + + yield return null; + } + + [UnityTest] + public IEnumerator GetProp_WithAMissingKeyAndFallback_ReturnsNull() + { + var player = new Player + { + props = new[] { + new Prop(("key1", "value1")), + new Prop(("key2", "value2")) + } + }; + + Assert.AreEqual(player.GetProp("key3", "value3"), "value3"); + + yield return null; + } + } +} diff --git a/Assets/Talo Game Services/Talo/Tests/EntityWithProps/GetPropTest.cs.meta b/Assets/Talo Game Services/Talo/Tests/EntityWithProps/GetPropTest.cs.meta new file mode 100644 index 0000000..0ad303e --- /dev/null +++ b/Assets/Talo Game Services/Talo/Tests/EntityWithProps/GetPropTest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1b0e702d1e55140de812128ea8c8e618 \ No newline at end of file diff --git a/Assets/Talo Game Services/Talo/Tests/EntityWithProps/InsertIntoPropArrayTest.cs b/Assets/Talo Game Services/Talo/Tests/EntityWithProps/InsertIntoPropArrayTest.cs new file mode 100644 index 0000000..fb6e2b7 --- /dev/null +++ b/Assets/Talo Game Services/Talo/Tests/EntityWithProps/InsertIntoPropArrayTest.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections; +using System.Linq; +using NUnit.Framework; +using UnityEngine.TestTools; + +namespace TaloGameServices.Test { + internal class InsertIntoPropArrayTest + { + [UnityTest] + public IEnumerator InsertIntoPropArray_AddsValueToExistingArray() + { + var player = new Player + { + props = new[] { + new Prop(("colours[]", "red")), + new Prop(("colours[]", "blue")) + } + }; + + player.InsertIntoPropArray("colours", "green", false); + + var result = player.GetPropArray("colours").ToList(); + Assert.AreEqual(3, result.Count); + Assert.Contains("green", result); + + yield return null; + } + + [UnityTest] + public IEnumerator InsertIntoPropArray_DoesNotAddDuplicateValue() + { + var player = new Player + { + props = new[] { + new Prop(("colours[]", "red")), + new Prop(("colours[]", "blue")) + } + }; + + player.InsertIntoPropArray("colours", "red", false); + + var result = player.GetPropArray("colours").ToList(); + Assert.AreEqual(2, result.Count); + + yield return null; + } + + [UnityTest] + public IEnumerator InsertIntoPropArray_WhenArrayWasPreviouslyDeleted_ClearsNullEntryAndInsertsValue() + { + var player = new Player + { + props = new[] { + new Prop(("colours[]", null)) + } + }; + + player.InsertIntoPropArray("colours", "red", false); + + var result = player.GetPropArray("colours").ToList(); + Assert.AreEqual(1, result.Count); + Assert.Contains("red", result); + + yield return null; + } + + [UnityTest] + public IEnumerator InsertIntoPropArray_WithNullValue_ThrowsAnError() + { + var player = new Player + { + props = new[] { + new Prop(("colours[]", "red")) + } + }; + + try + { + player.InsertIntoPropArray("colours", null, false); + Assert.Fail("Expected exception was not thrown"); + } + catch (Exception ex) + { + Assert.AreEqual(ex.Message, "Value for prop array colours cannot be null or empty"); + } + + yield return null; + } + + [UnityTest] + public IEnumerator InsertIntoPropArray_WithEmptyValue_ThrowsAnError() + { + var player = new Player + { + props = new[] { + new Prop(("colours[]", "red")) + } + }; + + try + { + player.InsertIntoPropArray("colours", "", false); + Assert.Fail("Expected exception was not thrown"); + } + catch (Exception ex) + { + Assert.AreEqual(ex.Message, "Value for prop array colours cannot be null or empty"); + } + + yield return null; + } + } +} diff --git a/Assets/Talo Game Services/Talo/Tests/EntityWithProps/InsertIntoPropArrayTest.cs.meta b/Assets/Talo Game Services/Talo/Tests/EntityWithProps/InsertIntoPropArrayTest.cs.meta new file mode 100644 index 0000000..c2c228f --- /dev/null +++ b/Assets/Talo Game Services/Talo/Tests/EntityWithProps/InsertIntoPropArrayTest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3dbbb63e838794e97b25fff9792501b9 \ No newline at end of file diff --git a/Assets/Talo Game Services/Talo/Tests/EntityWithProps/RemoveFromPropArrayTest.cs b/Assets/Talo Game Services/Talo/Tests/EntityWithProps/RemoveFromPropArrayTest.cs new file mode 100644 index 0000000..53b7ec7 --- /dev/null +++ b/Assets/Talo Game Services/Talo/Tests/EntityWithProps/RemoveFromPropArrayTest.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections; +using System.Linq; +using NUnit.Framework; +using UnityEngine.TestTools; + +namespace TaloGameServices.Test { + internal class RemoveFromPropArrayTest + { + [UnityTest] + public IEnumerator RemoveFromPropArray_RemovesMatchingValue() + { + var player = new Player + { + props = new[] { + new Prop(("colours[]", "red")), + new Prop(("colours[]", "blue")), + new Prop(("colours[]", "green")) + } + }; + + player.RemoveFromPropArray("colours", "blue", false); + + var result = player.GetPropArray("colours").ToList(); + Assert.AreEqual(2, result.Count); + Assert.IsFalse(result.Contains("blue")); + + yield return null; + } + + [UnityTest] + public IEnumerator RemoveFromPropArray_WhenLastItemRemoved_ArrayIsEmptyAndSentinelNullExistsInProps() + { + var player = new Player + { + props = new[] { + new Prop(("colours[]", "red")) + } + }; + + player.RemoveFromPropArray("colours", "red", false); + + Assert.AreEqual(0, player.GetPropArray("colours").Count); + // ensure the sentinel null value is in there + Assert.IsTrue(player.props.Any((prop) => prop.key == "colours[]" && prop.value == null)); + + yield return null; + } + + [UnityTest] + public IEnumerator RemoveFromPropArray_WhenArrayIsAlreadyDeleted_RemainsDeleted() + { + var player = new Player + { + props = new[] { + new Prop(("colours[]", "red")) + } + }; + + player.RemoveFromPropArray("colours", "red", false); + try + { + player.RemoveFromPropArray("colours", "red", false); + Assert.Fail("Expected exception was not thrown"); + } + catch (Exception ex) + { + Assert.AreEqual(ex.Message, "Value red does not exist in prop array colours"); + } + + Assert.AreEqual(0, player.GetPropArray("colours").Count); + Assert.IsTrue(player.props.Any((prop) => prop.key == "colours[]" && prop.value == null)); + + yield return null; + } + + [UnityTest] + public IEnumerator RemoveFromPropArray_WhenValueDoesNotExist_ThrowsAnError() + { + var player = new Player + { + props = new[] { + new Prop(("colours[]", "red")), + new Prop(("colours[]", "blue")) + } + }; + + try + { + player.RemoveFromPropArray("colours", "green", false); + Assert.Fail("Expected exception was not thrown"); + } + catch (Exception ex) + { + Assert.AreEqual(ex.Message, "Value green does not exist in prop array colours"); + } + + yield return null; + } + + [UnityTest] + public IEnumerator RemoveFromPropArray_WhenArrayIsAlreadyDeleted_ThrowsAnError() + { + var player = new Player + { + props = new[] { + new Prop(("colours[]", null)) + } + }; + + try + { + player.RemoveFromPropArray("colours", "red", false); + Assert.Fail("Expected exception was not thrown"); + } + catch (Exception ex) + { + Assert.AreEqual(ex.Message, "Value red does not exist in prop array colours"); + } + + yield return null; + } + } +} diff --git a/Assets/Talo Game Services/Talo/Tests/EntityWithProps/RemoveFromPropArrayTest.cs.meta b/Assets/Talo Game Services/Talo/Tests/EntityWithProps/RemoveFromPropArrayTest.cs.meta new file mode 100644 index 0000000..16426c5 --- /dev/null +++ b/Assets/Talo Game Services/Talo/Tests/EntityWithProps/RemoveFromPropArrayTest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7efb75481ea2f4c59af082dd72f873ab \ No newline at end of file diff --git a/Assets/Talo Game Services/Talo/Tests/EntityWithProps/SetPropArrayTest.cs b/Assets/Talo Game Services/Talo/Tests/EntityWithProps/SetPropArrayTest.cs new file mode 100644 index 0000000..a434805 --- /dev/null +++ b/Assets/Talo Game Services/Talo/Tests/EntityWithProps/SetPropArrayTest.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections; +using System.Linq; +using NUnit.Framework; +using UnityEngine.TestTools; + +namespace TaloGameServices.Test { + internal class SetPropArrayTest + { + [UnityTest] + public IEnumerator SetPropArray_WithNewKey_AddsAllValues() + { + var player = new Player + { + props = new Prop[] { } + }; + + player.SetPropArray("colours", new [] { "red", "blue", "green" }, false); + + var result = player.GetPropArray("colours").ToList(); + Assert.AreEqual(3, result.Count); + Assert.Contains("red", result); + Assert.Contains("blue", result); + Assert.Contains("green", result); + + yield return null; + } + + [UnityTest] + public IEnumerator SetPropArray_DeduplicatesValues() + { + var player = new Player + { + props = new Prop[] { } + }; + + player.SetPropArray("colours", new [] { "red", "red", "blue" }, false); + + var result = player.GetPropArray("colours").ToList(); + Assert.AreEqual(2, result.Count); + Assert.Contains("red", result); + Assert.Contains("blue", result); + + yield return null; + } + + [UnityTest] + public IEnumerator SetPropArray_WithAllNullOrEmptyValues_ThrowsAnError() + { + var player = new Player + { + props = new Prop[] { } + }; + + try + { + player.SetPropArray("colours", new string[] { null, "" }, false); + Assert.Fail("Expected exception was not thrown"); + } + catch (Exception ex) + { + Assert.AreEqual(ex.Message, "Values for prop array colours must contain at least one non-empty value"); + } + + yield return null; + } + + [UnityTest] + public IEnumerator SetPropArray_OnExistingPopulatedArray_ReplacesOldValues() + { + var player = new Player + { + props = new[] { + new Prop(("colours[]", "red")), + new Prop(("colours[]", "blue")) + } + }; + + player.SetPropArray("colours", new [] { "green", "yellow" }, false); + + var result = player.GetPropArray("colours").ToList(); + Assert.AreEqual(2, result.Count); + Assert.Contains("green", result); + Assert.Contains("yellow", result); + + yield return null; + } + + [UnityTest] + public IEnumerator SetPropArray_WithEmptyCollection_ThrowsAnError() + { + var player = new Player + { + props = new Prop[] { } + }; + + try + { + player.SetPropArray("colours", new string[] {}, false); + Assert.Fail("Expected exception was not thrown"); + } + catch (Exception ex) + { + Assert.AreEqual(ex.Message, "Values for prop array colours must contain at least one non-empty value"); + } + + yield return null; + } + + [UnityTest] + public IEnumerator SetPropArray_WhenArrayWasPreviouslyDeleted_ClearsNullEntryAndSetsNewValues() + { + var player = new Player + { + props = new[] { + new Prop(("colours[]", null)) + } + }; + + player.SetPropArray("colours", new [] { "red", "blue" }, false); + + var result = player.GetPropArray("colours").ToList(); + Assert.AreEqual(2, result.Count); + Assert.Contains("red", result); + Assert.Contains("blue", result); + + yield return null; + } + } +} diff --git a/Assets/Talo Game Services/Talo/Tests/EntityWithProps/SetPropArrayTest.cs.meta b/Assets/Talo Game Services/Talo/Tests/EntityWithProps/SetPropArrayTest.cs.meta new file mode 100644 index 0000000..f679a4a --- /dev/null +++ b/Assets/Talo Game Services/Talo/Tests/EntityWithProps/SetPropArrayTest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7871f8c73d8da4381bf5a466a67c66c9 \ No newline at end of file diff --git a/Assets/Talo Game Services/Talo/Tests/EntityWithProps/SetPropTest.cs b/Assets/Talo Game Services/Talo/Tests/EntityWithProps/SetPropTest.cs new file mode 100644 index 0000000..a11da48 --- /dev/null +++ b/Assets/Talo Game Services/Talo/Tests/EntityWithProps/SetPropTest.cs @@ -0,0 +1,44 @@ +using System.Collections; +using NUnit.Framework; +using UnityEngine.TestTools; + +namespace TaloGameServices.Test { + internal class SetPropTest + { + [UnityTest] + public IEnumerator SetProp_WhenPropDoesNotAlreadyExist_AppendsTheNewProp() + { + var player = new Player + { + props = new[] { + new Prop(("key1", "value1")), + new Prop(("key2", "value2")) + } + }; + + player.SetProp("key3", "value3", false); + + Assert.AreEqual(player.GetProp("key3"), "value3"); + + yield return null; + } + + [UnityTest] + public IEnumerator SetProp_WhenPropAlreadyExists_UpdatesTheProp() + { + var player = new Player + { + props = new[] { + new Prop(("key1", "value1")), + new Prop(("key2", "value2")) + } + }; + + player.SetProp("key2", "2value2updated", false); + + Assert.AreEqual(player.GetProp("key2"), "2value2updated"); + + yield return null; + } + } +} diff --git a/Assets/Talo Game Services/Talo/Tests/EntityWithProps/SetPropTest.cs.meta b/Assets/Talo Game Services/Talo/Tests/EntityWithProps/SetPropTest.cs.meta new file mode 100644 index 0000000..a998aef --- /dev/null +++ b/Assets/Talo Game Services/Talo/Tests/EntityWithProps/SetPropTest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 89e1b69e7e74b487d80948e45f5bd9b3 \ No newline at end of file diff --git a/Assets/Talo Game Services/Talo/Tests/LiveConfig/GetPropTest.cs b/Assets/Talo Game Services/Talo/Tests/LiveConfig/GetPropTest.cs index 19ab055..97a40c3 100644 --- a/Assets/Talo Game Services/Talo/Tests/LiveConfig/GetPropTest.cs +++ b/Assets/Talo Game Services/Talo/Tests/LiveConfig/GetPropTest.cs @@ -4,7 +4,7 @@ using System; namespace TaloGameServices.Test { - internal class GetPropTest + internal class LiveConfigGetPropTest { [UnityTest] public IEnumerator GetProp_WithALiveConfigThatHasValues_ReturnsCorrectValue() @@ -41,7 +41,7 @@ public IEnumerator GetProp_WhenConvertingTypeToBoolean_ReturnsCorrectValue() { var config = new LiveConfig(new[] { new Prop(("halloweenEventEnabled", "True")) }); - Assert.AreEqual(true, config.GetProp("halloweenEventEnabled", false)); + Assert.AreEqual(true, config.GetProp("halloweenEventEnabled", false)); yield return null; } @@ -51,7 +51,7 @@ public IEnumerator GetProp_WhenConvertingTypeToNumber_ReturnsCorrectValue() { var config = new LiveConfig(new[] { new Prop(("maxLevel", "60")) }); - Assert.AreEqual(60, config.GetProp("maxLevel", 0)); + Assert.AreEqual(60, config.GetProp("maxLevel", 0)); yield return null; } From f455c7f6e0987b47ac256fee14fd7794cf7d5e05 Mon Sep 17 00:00:00 2001 From: tudor <7089284+tudddorrr@users.noreply.github.com> Date: Sat, 21 Mar 2026 19:07:39 +0000 Subject: [PATCH 2/4] ensure test mode only runs in the unity editor --- Assets/Talo Game Services/Talo/Runtime/Talo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Assets/Talo Game Services/Talo/Runtime/Talo.cs b/Assets/Talo Game Services/Talo/Runtime/Talo.cs index e00efe3..3abb680 100644 --- a/Assets/Talo Game Services/Talo/Runtime/Talo.cs +++ b/Assets/Talo Game Services/Talo/Runtime/Talo.cs @@ -218,7 +218,7 @@ public static bool IsOffline() internal static bool CheckTestMode() { -#if UNITY_EDITOR || DEBUG +#if UNITY_EDITOR var assembly = AppDomain.CurrentDomain.GetAssemblies() .FirstOrDefault((assembly) => assembly.FullName.ToLowerInvariant().StartsWith("nunit.framework")); From 333c17103c19e917112f9d976465257831ebdc7e Mon Sep 17 00:00:00 2001 From: tudor <7089284+tudddorrr@users.noreply.github.com> Date: Sun, 22 Mar 2026 11:43:56 +0000 Subject: [PATCH 3/4] add player auth migrate account api --- .../Talo/Runtime/APIs/PlayerAuthAPI.cs | 14 +++++++++++ .../PlayerAuthMigrateAccountRequest.cs | 10 ++++++++ .../PlayerAuthMigrateAccountRequest.cs.meta | 2 ++ .../PlayerAuthMigrateAccountResponse.cs | 8 +++++++ .../PlayerAuthMigrateAccountResponse.cs.meta | 2 ++ .../Talo/Runtime/Utils/PlayerAuthException.cs | 3 ++- .../Talo/Runtime/Utils/SessionManager.cs | 23 +++++++++++++++---- 7 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 Assets/Talo Game Services/Talo/Runtime/Requests/PlayerAuthMigrateAccountRequest.cs create mode 100644 Assets/Talo Game Services/Talo/Runtime/Requests/PlayerAuthMigrateAccountRequest.cs.meta create mode 100644 Assets/Talo Game Services/Talo/Runtime/Responses/PlayerAuthMigrateAccountResponse.cs create mode 100644 Assets/Talo Game Services/Talo/Runtime/Responses/PlayerAuthMigrateAccountResponse.cs.meta diff --git a/Assets/Talo Game Services/Talo/Runtime/APIs/PlayerAuthAPI.cs b/Assets/Talo Game Services/Talo/Runtime/APIs/PlayerAuthAPI.cs index 1bc3b55..6288dd1 100644 --- a/Assets/Talo Game Services/Talo/Runtime/APIs/PlayerAuthAPI.cs +++ b/Assets/Talo Game Services/Talo/Runtime/APIs/PlayerAuthAPI.cs @@ -167,5 +167,19 @@ public async Task DeleteAccount(string currentPassword) await _sessionManager.ClearSession(); } + + public async Task MigrateAccount(string currentPassword, string service, string identifier) + { + var uri = new Uri($"{baseUrl}/migrate"); + string content = JsonUtility.ToJson(new PlayerAuthMigrateAccountRequest { + currentPassword = currentPassword, + service = service, + identifier = identifier + }); + var json = await Call(uri, "POST", content); + + var res = JsonUtility.FromJson(json); + await _sessionManager.HandleAccountMigrated(res); + } } } diff --git a/Assets/Talo Game Services/Talo/Runtime/Requests/PlayerAuthMigrateAccountRequest.cs b/Assets/Talo Game Services/Talo/Runtime/Requests/PlayerAuthMigrateAccountRequest.cs new file mode 100644 index 0000000..ac0d541 --- /dev/null +++ b/Assets/Talo Game Services/Talo/Runtime/Requests/PlayerAuthMigrateAccountRequest.cs @@ -0,0 +1,10 @@ +namespace TaloGameServices +{ + [System.Serializable] + public class PlayerAuthMigrateAccountRequest + { + public string currentPassword; + public string service; + public string identifier; + } +} diff --git a/Assets/Talo Game Services/Talo/Runtime/Requests/PlayerAuthMigrateAccountRequest.cs.meta b/Assets/Talo Game Services/Talo/Runtime/Requests/PlayerAuthMigrateAccountRequest.cs.meta new file mode 100644 index 0000000..f2bf1c9 --- /dev/null +++ b/Assets/Talo Game Services/Talo/Runtime/Requests/PlayerAuthMigrateAccountRequest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: da0b6376123d24cd1b0afd97c6144877 \ No newline at end of file diff --git a/Assets/Talo Game Services/Talo/Runtime/Responses/PlayerAuthMigrateAccountResponse.cs b/Assets/Talo Game Services/Talo/Runtime/Responses/PlayerAuthMigrateAccountResponse.cs new file mode 100644 index 0000000..8cd4eeb --- /dev/null +++ b/Assets/Talo Game Services/Talo/Runtime/Responses/PlayerAuthMigrateAccountResponse.cs @@ -0,0 +1,8 @@ +namespace TaloGameServices +{ + [System.Serializable] + public class PlayerAuthMigrateAccountResponse + { + public PlayerAlias alias; + } +} diff --git a/Assets/Talo Game Services/Talo/Runtime/Responses/PlayerAuthMigrateAccountResponse.cs.meta b/Assets/Talo Game Services/Talo/Runtime/Responses/PlayerAuthMigrateAccountResponse.cs.meta new file mode 100644 index 0000000..0faea94 --- /dev/null +++ b/Assets/Talo Game Services/Talo/Runtime/Responses/PlayerAuthMigrateAccountResponse.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: dc41a5cdf5fc940658d7a99cec0bcd7c \ No newline at end of file diff --git a/Assets/Talo Game Services/Talo/Runtime/Utils/PlayerAuthException.cs b/Assets/Talo Game Services/Talo/Runtime/Utils/PlayerAuthException.cs index 9852771..565b7b2 100644 --- a/Assets/Talo Game Services/Talo/Runtime/Utils/PlayerAuthException.cs +++ b/Assets/Talo Game Services/Talo/Runtime/Utils/PlayerAuthException.cs @@ -15,7 +15,8 @@ public enum PlayerAuthErrorCode { PASSWORD_RESET_CODE_INVALID, VERIFICATION_EMAIL_REQUIRED, INVALID_EMAIL, - NEW_IDENTIFIER_MATCHES_CURRENT_IDENTIFIER + NEW_IDENTIFIER_MATCHES_CURRENT_IDENTIFIER, + INVALID_MIGRATION_TARGET } public class PlayerAuthException : Exception diff --git a/Assets/Talo Game Services/Talo/Runtime/Utils/SessionManager.cs b/Assets/Talo Game Services/Talo/Runtime/Utils/SessionManager.cs index 6ac0fab..3fba573 100644 --- a/Assets/Talo Game Services/Talo/Runtime/Utils/SessionManager.cs +++ b/Assets/Talo Game Services/Talo/Runtime/Utils/SessionManager.cs @@ -26,11 +26,14 @@ private void SaveSession(string sessionToken) SetIdentifierPlayerPref(); } - public async Task ClearSession() + public async Task ClearSession(bool resetSocket = true) { Talo.CurrentAlias = null; PlayerPrefs.DeleteKey("TaloSessionToken"); - await Talo.Socket.ResetConnection(); + if (resetSocket) + { + await Talo.Socket.ResetConnection(); + } } public string GetSessionToken() @@ -48,11 +51,23 @@ public bool CheckForSession() return !string.IsNullOrEmpty(GetSessionToken()); } + private void SetNewAlias(PlayerAlias alias) + { + Talo.CurrentAlias = alias; + alias.WriteOfflineAlias(); + } + public void HandleIdentifierUpdated(PlayerAuthChangeIdentifierResponse res) { - Talo.CurrentAlias = res.alias; - Talo.CurrentAlias.WriteOfflineAlias(); + SetNewAlias(res.alias); SetIdentifierPlayerPref(); } + + public async Task HandleAccountMigrated(PlayerAuthMigrateAccountResponse res) + { + await ClearSession(false); + SetNewAlias(res.alias); + Talo.Players.InvokeIdentifiedEvent(); + } } } From 6f21dfeecc9aa24068a169aae31fa77e0d9f6038 Mon Sep 17 00:00:00 2001 From: tudor <7089284+tudddorrr@users.noreply.github.com> Date: Fri, 27 Mar 2026 02:10:29 +0000 Subject: [PATCH 4/4] 0.55.0 --- Assets/Talo Game Services/Talo/Runtime/APIs/BaseAPI.cs | 2 +- Assets/Talo Game Services/Talo/VERSION | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Assets/Talo Game Services/Talo/Runtime/APIs/BaseAPI.cs b/Assets/Talo Game Services/Talo/Runtime/APIs/BaseAPI.cs index 244a64f..d05150f 100644 --- a/Assets/Talo Game Services/Talo/Runtime/APIs/BaseAPI.cs +++ b/Assets/Talo Game Services/Talo/Runtime/APIs/BaseAPI.cs @@ -9,7 +9,7 @@ namespace TaloGameServices public class BaseAPI { // automatically updated with a pre-commit hook - private const string ClientVersion = "0.54.0"; + private const string ClientVersion = "0.55.0"; protected string baseUrl; diff --git a/Assets/Talo Game Services/Talo/VERSION b/Assets/Talo Game Services/Talo/VERSION index 524456c..316ba4b 100644 --- a/Assets/Talo Game Services/Talo/VERSION +++ b/Assets/Talo Game Services/Talo/VERSION @@ -1 +1 @@ -0.54.0 +0.55.0