Skip to content

Commit df581c8

Browse files
Merge pull request #15 from CoderGamester/develop
Release 0.7.0
2 parents 4182bdc + 0b1e751 commit df581c8

12 files changed

+471
-17
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ All notable changes to this package will be documented in this file.
44
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
55
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
66

7+
## [0.7.0] - 2025-11-03
8+
9+
**New**:
10+
- Added *Rebind* functionality to all Observable classes (*ObservableField*, *ObservableList*, *ObservableDictionary*) allowing rebinding to new data sources without losing existing observers
11+
- Added *Rebind* methods to all Observable Resolver classes (*ObservableResolverField*, *ObservableResolverList*, *ObservableResolverDictionary*) to rebind to new origin collections and resolver functions
12+
- Added new *IObservableResolverField* interface with *Rebind* method for resolver field implementations
13+
714
## [0.6.7] - 2025-04-07
815

916
**New**:

Runtime/ColorJsonConverter.cs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
using Newtonsoft.Json;
2+
using UnityEngine;
3+
using System;
4+
5+
// ReSharper disable once CheckNamespace
6+
7+
namespace GameLovers
8+
{
9+
/// <summary>
10+
/// JSON converter for Unity's Color struct that handles serialization to/from:
11+
/// - Hex color strings (e.g. "#FF0000FF" for red)
12+
/// - RGBA object format (e.g. {"r":1,"g":0,"b":0,"a":1})
13+
/// </summary>
14+
/// <remarks>
15+
/// This converter enables proper serialization of Unity Color objects with Newtonsoft.Json.
16+
/// It's particularly useful for saving color data in JSON configuration files or network payloads.
17+
/// </remarks>
18+
public class ColorJsonConverter : JsonConverter<Color>
19+
{
20+
/// <summary>
21+
/// Reads JSON data and converts it to a Unity Color object
22+
/// </summary>
23+
/// <param name="reader">JSON reader providing the input data</param>
24+
/// <param name="objectType">Type of object to deserialize (should be Color)</param>
25+
/// <param name="existingValue">Existing value of the object being read</param>
26+
/// <param name="hasExistingValue">Whether there is an existing value</param>
27+
/// <param name="serializer">JSON serializer instance</param>
28+
/// <returns>Deserialized Color object</returns>
29+
/// <remarks>
30+
/// Supports both hex color strings and RGBA object formats:
31+
/// - "#RRGGBBAA" or "#RRGGBB" (missing alpha defaults to 1)
32+
/// - {"r":1,"g":0,"b":0,"a":1} (missing components default to 0, except alpha which defaults to 1)
33+
/// </remarks>
34+
public override Color ReadJson(JsonReader reader, Type objectType, Color existingValue, bool hasExistingValue, JsonSerializer serializer)
35+
{
36+
if (reader.TokenType == JsonToken.String)
37+
{
38+
string colorString = reader.Value.ToString();
39+
Color color;
40+
if (ColorUtility.TryParseHtmlString(colorString, out color))
41+
{
42+
return color;
43+
}
44+
}
45+
else if (reader.TokenType == JsonToken.StartObject)
46+
{
47+
float r = 0, g = 0, b = 0, a = 1;
48+
49+
reader.Read();
50+
while (reader.TokenType != JsonToken.EndObject)
51+
{
52+
string propertyName = reader.Value.ToString().ToLower();
53+
reader.Read();
54+
55+
switch (propertyName)
56+
{
57+
case "r": r = Convert.ToSingle(reader.Value); break;
58+
case "g": g = Convert.ToSingle(reader.Value); break;
59+
case "b": b = Convert.ToSingle(reader.Value); break;
60+
case "a": a = Convert.ToSingle(reader.Value); break;
61+
}
62+
63+
reader.Read();
64+
}
65+
66+
return new Color(r, g, b, a);
67+
}
68+
69+
return Color.white;
70+
}
71+
72+
/// <summary>
73+
/// Writes a Color object to JSON format
74+
/// </summary>
75+
/// <param name="writer">JSON writer for output</param>
76+
/// <param name="value">Color value to serialize</param>
77+
/// <param name="serializer">JSON serializer instance</param>
78+
/// <remarks>
79+
/// Always serializes to hex string format (e.g. "#FF0000FF" for red)
80+
/// This provides a compact and web-friendly representation
81+
/// </remarks>
82+
public override void WriteJson(JsonWriter writer, Color value, JsonSerializer serializer)
83+
{
84+
writer.WriteValue("#" + ColorUtility.ToHtmlStringRGBA(value));
85+
}
86+
}
87+
}

Runtime/ColorJsonConverter.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Runtime/ObservableDictionary.cs

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,17 @@ public interface IObservableResolverDictionary<TKey, TValue, TKeyOrigin, TValueO
176176
/// Clear's to the origin dictionary
177177
/// </remarks>
178178
void ClearOrigin();
179+
180+
/// <summary>
181+
/// Rebinds this dictionary to a new origin dictionary and resolver functions without losing existing observers.
182+
/// The internal dictionary will be rebuilt from the new origin dictionary using the new resolvers.
183+
/// </summary>
184+
/// <param name="dictionary">The new origin dictionary to bind to</param>
185+
/// <param name="fromOrignResolver">The new function to convert from origin types to this dictionary's types</param>
186+
/// <param name="toOrignResolver">The new function to convert from this dictionary's types to origin types</param>
187+
void Rebind(IDictionary<TKeyOrigin, TValueOrigin> dictionary,
188+
Func<KeyValuePair<TKeyOrigin, TValueOrigin>, KeyValuePair<TKey, TValue>> fromOrignResolver,
189+
Func<TKey, TValue, KeyValuePair<TKeyOrigin, TValueOrigin>> toOrignResolver);
179190
}
180191

181192
/// <inheritdoc />
@@ -193,7 +204,7 @@ public class ObservableDictionary<TKey, TValue> : IObservableDictionary<TKey, TV
193204
/// <inheritdoc />
194205
public ReadOnlyDictionary<TKey, TValue> ReadOnlyDictionary => new ReadOnlyDictionary<TKey, TValue>(Dictionary);
195206

196-
protected virtual IDictionary<TKey, TValue> Dictionary { get; }
207+
protected virtual IDictionary<TKey, TValue> Dictionary { get; set; }
197208

198209
private ObservableDictionary() { }
199210

@@ -203,6 +214,15 @@ public ObservableDictionary(IDictionary<TKey, TValue> dictionary)
203214
ObservableUpdateFlag = ObservableUpdateFlag.KeyUpdateOnly;
204215
}
205216

217+
/// <summary>
218+
/// Rebinds this dictionary to a new dictionary without losing existing observers.
219+
/// </summary>
220+
/// <param name="dictionary">The new dictionary to bind to</param>
221+
public void Rebind(IDictionary<TKey, TValue> dictionary)
222+
{
223+
Dictionary = dictionary;
224+
}
225+
206226
/// <inheritdoc cref="Dictionary{TKey,TValue}.this" />
207227
public TValue this[TKey key]
208228
{
@@ -468,14 +488,14 @@ private int AdjustIndex(int index, Action<TKey, TValue, TValue, ObservableUpdate
468488
}
469489
}
470490

471-
/// <inheritdoc />
491+
/// <inheritdoc cref="IObservableResolverDictionary{TKey, TValue, TKeyOrigin, TValueOrigin}"/>
472492
public class ObservableResolverDictionary<TKey, TValue, TKeyOrigin, TValueOrigin> :
473493
ObservableDictionary<TKey, TValue>,
474494
IObservableResolverDictionary<TKey, TValue, TKeyOrigin, TValueOrigin>
475495
{
476-
private readonly IDictionary<TKeyOrigin, TValueOrigin> _dictionary;
477-
private readonly Func<TKey, TValue, KeyValuePair<TKeyOrigin, TValueOrigin>> _toOrignResolver;
478-
private readonly Func<KeyValuePair<TKeyOrigin, TValueOrigin>, KeyValuePair<TKey, TValue>> _fromOrignResolver;
496+
private IDictionary<TKeyOrigin, TValueOrigin> _dictionary;
497+
private Func<TKey, TValue, KeyValuePair<TKeyOrigin, TValueOrigin>> _toOrignResolver;
498+
private Func<KeyValuePair<TKeyOrigin, TValueOrigin>, KeyValuePair<TKey, TValue>> _fromOrignResolver;
479499

480500
/// <inheritdoc />
481501
public ReadOnlyDictionary<TKeyOrigin, TValueOrigin> OriginDictionary => new ReadOnlyDictionary<TKeyOrigin, TValueOrigin>(_dictionary);
@@ -495,6 +515,23 @@ public ObservableResolverDictionary(IDictionary<TKeyOrigin, TValueOrigin> dictio
495515
}
496516
}
497517

518+
/// <inheritdoc />
519+
public void Rebind(IDictionary<TKeyOrigin, TValueOrigin> dictionary,
520+
Func<KeyValuePair<TKeyOrigin, TValueOrigin>, KeyValuePair<TKey, TValue>> fromOrignResolver,
521+
Func<TKey, TValue, KeyValuePair<TKeyOrigin, TValueOrigin>> toOrignResolver)
522+
{
523+
_dictionary = dictionary;
524+
_toOrignResolver = toOrignResolver;
525+
_fromOrignResolver = fromOrignResolver;
526+
527+
// Rebuild the internal dictionary from the new origin dictionary
528+
Dictionary.Clear();
529+
foreach (var pair in dictionary)
530+
{
531+
Dictionary.Add(fromOrignResolver(pair));
532+
}
533+
}
534+
498535
/// <inheritdoc />
499536
public TValueOrigin GetOriginValue(TKey key)
500537
{

Runtime/ObservableField.cs

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,26 @@ public interface IObservableField<T> : IObservableFieldReader<T>
5050
/// The field value with possibility to be changed
5151
/// </summary>
5252
new T Value { get; set; }
53+
54+
/// <summary>
55+
/// Rebinds this field to a new value without losing existing observers.
56+
/// </summary>
57+
/// <param name="initialValue">The new initial value for the field</param>
58+
void Rebind(T initialValue);
59+
}
60+
61+
/// <inheritdoc />
62+
/// <remarks>
63+
/// A resolver field with the possibility to rebind to new resolver functions
64+
/// </remarks>
65+
public interface IObservableResolverField<T> : IObservableField<T>
66+
{
67+
/// <summary>
68+
/// Rebinds this field to new resolver functions without losing existing observers
69+
/// </summary>
70+
/// <param name="fieldResolver">The new getter function for the field</param>
71+
/// <param name="fieldSetter">The new setter function for the field</param>
72+
void Rebind(Func<T> fieldResolver, Action<T> fieldSetter);
5373
}
5474

5575
/// <inheritdoc />
@@ -84,6 +104,12 @@ public ObservableField(T initialValue)
84104

85105
public static implicit operator T(ObservableField<T> value) => value.Value;
86106

107+
/// <inheritdoc />
108+
public void Rebind(T initialValue)
109+
{
110+
_value = initialValue;
111+
}
112+
87113
/// <inheritdoc />
88114
public void Observe(Action<T, T> onUpdate)
89115
{
@@ -137,11 +163,11 @@ protected void InvokeUpdate(T previousValue)
137163
}
138164
}
139165

140-
/// <inheritdoc />
141-
public class ObservableResolverField<T> : ObservableField<T>
166+
/// <inheritdoc cref="IObservableResolverField{T}"/>
167+
public class ObservableResolverField<T> : ObservableField<T>, IObservableResolverField<T>
142168
{
143-
private readonly Func<T> _fieldResolver;
144-
private readonly Action<T> _fieldSetter;
169+
private Func<T> _fieldResolver;
170+
private Action<T> _fieldSetter;
145171

146172
/// <inheritdoc cref="IObservableField{T}.Value" />
147173
public override T Value
@@ -165,6 +191,17 @@ public ObservableResolverField(Func<T> fieldResolver, Action<T> fieldSetter)
165191
_fieldSetter = fieldSetter;
166192
}
167193

194+
/// <summary>
195+
/// Rebinds this field to new resolver functions without losing existing observers
196+
/// </summary>
197+
/// <param name="fieldResolver">The new getter function for the field</param>
198+
/// <param name="fieldSetter">The new setter function for the field</param>
199+
public void Rebind(Func<T> fieldResolver, Action<T> fieldSetter)
200+
{
201+
_fieldResolver = fieldResolver;
202+
_fieldSetter = fieldSetter;
203+
}
204+
168205
public static implicit operator T(ObservableResolverField<T> value) => value.Value;
169206
}
170207
}

Runtime/ObservableList.cs

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ public interface IObservableList<T> : IObservableListReader<T>
9494
/// <remarks>
9595
/// This interface resolves between 2 lists with different types of values
9696
/// </remarks>
97-
public interface IObservableResolverListReader<T, TOrigin> : IObservableListReader<T>
97+
public interface IObservableResolverListReader<T, out TOrigin> : IObservableListReader<T>
9898
{
9999
/// <summary>
100100
/// The Original List that is being resolved across the entire interface
@@ -134,6 +134,15 @@ public interface IObservableResolverList<T, TOrigin> :
134134
/// Clear's to the origin list
135135
/// </remarks>
136136
void ClearOrigin();
137+
138+
/// <summary>
139+
/// Rebinds this list to a new origin list and resolver functions without losing existing observers.
140+
/// The internal list will be rebuilt from the new origin list using the new resolvers.
141+
/// </summary>
142+
/// <param name="originList">The new origin list to bind to</param>
143+
/// <param name="fromOrignResolver">The new function to convert from origin type to this list's type</param>
144+
/// <param name="toOrignResolver">The new function to convert from this list's type to origin type</param>
145+
void Rebind(IList<TOrigin> originList, Func<TOrigin, T> fromOrignResolver, Func<T, TOrigin> toOrignResolver);
137146
}
138147

139148
/// <inheritdoc />
@@ -160,7 +169,7 @@ public T this[int index]
160169
/// <inheritdoc />
161170
public IReadOnlyList<T> ReadOnlyList => new List<T>(List);
162171

163-
protected virtual List<T> List { get; }
172+
protected virtual List<T> List { get; set; }
164173

165174
protected ObservableList() { }
166175

@@ -169,6 +178,15 @@ public ObservableList(IList<T> list)
169178
List = list as List<T> ?? list.ToList();
170179
}
171180

181+
/// <summary>
182+
/// Rebinds this list to a new list without losing existing observers.
183+
/// </summary>
184+
/// <param name="list">The new list to bind to</param>
185+
public void Rebind(IList<T> list)
186+
{
187+
List = list as List<T> ?? list.ToList();
188+
}
189+
172190
/// <inheritdoc cref="List{T}.GetEnumerator"/>
173191
public List<T>.Enumerator GetEnumerator()
174192
{
@@ -332,12 +350,15 @@ private int AdjustIndex(int index, Action<int, T, T, ObservableUpdateType> actio
332350
}
333351
}
334352

335-
/// <inheritdoc />
353+
/// <inheritdoc cref="IObservableResolverList{T, TOrigin}"/>
354+
/// <remarks>
355+
/// This class resolves between 2 lists with different types of values
356+
/// </remarks>
336357
public class ObservableResolverList<T, TOrigin> : ObservableList<T>, IObservableResolverList<T, TOrigin>
337358
{
338-
private readonly IList<TOrigin> _originList;
339-
private readonly Func<TOrigin, T> _fromOrignResolver;
340-
private readonly Func<T, TOrigin> _toOrignResolver;
359+
private IList<TOrigin> _originList;
360+
private Func<TOrigin, T> _fromOrignResolver;
361+
private Func<T, TOrigin> _toOrignResolver;
341362

342363
/// <inheritdoc />
343364
public IReadOnlyList<TOrigin> OriginList => new List<TOrigin>(_originList);
@@ -357,6 +378,23 @@ public ObservableResolverList(IList<TOrigin> originList,
357378
}
358379
}
359380

381+
/// <inheritdoc />
382+
public void Rebind(IList<TOrigin> originList,
383+
Func<TOrigin, T> fromOrignResolver,
384+
Func<T, TOrigin> toOrignResolver)
385+
{
386+
_originList = originList;
387+
_fromOrignResolver = fromOrignResolver;
388+
_toOrignResolver = toOrignResolver;
389+
390+
// Rebuild the internal list from the new origin list
391+
List.Clear();
392+
for (var i = 0; i < originList.Count; i++)
393+
{
394+
List.Add(fromOrignResolver(originList[i]));
395+
}
396+
}
397+
360398
/// <inheritdoc />
361399
public override void Add(T data)
362400
{

0 commit comments

Comments
 (0)