Skip to content

Commit c7426fa

Browse files
authored
Merge pull request #192 from brunomikoski/feature/fix-missing-last-known-name
Feature/fix missing last known name
2 parents e496352 + 7880fcb commit c7426fa

7 files changed

Lines changed: 175 additions & 64 deletions

CHANGELOG.MD

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,27 @@ All notable changes to this project will be documented in this file.
33

44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6-
[Unreleased]
6+
7+
## [Unreleased]
8+
### Added
9+
- `CollectionItemPicker<T>.ToString()` override returning `[item1, item2, ...]` (empty collection renders as `[]`; null entries render as `<null>`). Uses `StringBuilder` to avoid per-call allocations on long pickers.
10+
- `CollectionItemQuery<T>.IsEmpty()` returns `true` when every `QuerySet`'s picker contains zero items — useful for skipping the `Matches` evaluation on default-constructed queries.
11+
- `CollectionItemPickerPropertyDrawer` now writes `itemLastKnownName` and `collectionLastKnownName` when an entry is added to a picker, so newly added items have populated cache names immediately instead of empty strings.
12+
13+
### Changed
14+
- Reverted `CollectionItemQuery.MatchType` enum values: `SomeNot``NotAny`, `None``NotAll`. The new names mirror `Any`/`All` and describe the actual semantics (target has *none* of the picker items / target is missing *at least one* of them). Integer values are preserved (2 and 3), so existing serialized `QuerySet` data deserializes unchanged.
15+
- Added XML documentation on `MatchType` values and the `Matches` method to surface the semantics in IntelliSense.
16+
- `CollectionItemPicker<T>` events renamed: `OnItemTypeAddedEvent``OnItemAddedEvent`, `OnItemTypeRemovedEvent``OnItemRemovedEvent`. **Breaking** for external subscribers — `+=`/`-=` call sites must be updated. `OnChangedEvent` is unchanged.
17+
- `CollectionItemIndirectReference.itemLastKnownName` and `collectionLastKnownName` are now wrapped in `#if UNITY_EDITOR` and no longer compiled into player builds. Runtime resolution relies exclusively on GUIDs.
18+
- `CollectionItemQuery<T>.Matches(IEnumerable<T> targetItems, out int)` now tolerates a `null` `targetItems` — treated as empty, so `Any`/`All` sets fail and `NotAny`/`NotAll` sets pass.
19+
20+
### Fixed
21+
- `CollectionItemQueryPropertyDrawer.IsCombinationImpossible` no longer reports false positives for `Any` + `NotAny` and `All` + `NotAll` rule pairs. Impossibility for these now requires the stricter *subset* relationship between pickers (not just any intersection), matching the real-world satisfiability. `All` + `NotAny` continues to be flagged on any intersection, which is correct.
22+
- `CollectionItemPicker` and `CollectionItemIndirectReference<T>` now implement `ISerializationCallbackReceiver` to invalidate their runtime caches on deserialization, so inspector edits to a picker during Play Mode take effect immediately instead of returning the stale cached `Items`/`Ref`.
23+
- `CollectionItemIndirectReferencePropertyDrawer` now re-syncs stale `itemLastKnownName`/`collectionLastKnownName` from the live asset and collection names on every draw (via `ApplyModifiedPropertiesWithoutUndo`), preventing the cached names from drifting when an asset is renamed outside the inspector.
24+
25+
### Removed
26+
- Removed the runtime fallback in `CollectionItemIndirectReference.TryGetCollectionItem` that, when the stored GUIDs failed to resolve, attempted to look the collection and item back up by `collectionLastKnownName`/`itemLastKnownName`. With the names now editor-only, GUID is the single runtime lookup path; rename recovery is handled at edit time by the drawer instead.
727

828
## [2.5.1] - 06/04/2026
929
### Added

Scripts/Editor/PropertyDrawers/CollectionItemIndirectReferencePropertyDrawer.cs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ public override void OnGUI(Rect position, SerializedProperty property, GUIConten
5050
collectionLastKnownNameSerializedProperty = property.FindPropertyRelative(COLLECTION_LAST_KNOW_NAME_PROPERTY_PATH);
5151

5252
TryGetCollectionItem(out ScriptableObject collectionItem);
53-
53+
ValidateLastKnownNames(collectionItem);
54+
5455
int indexOfArrayPart = property.propertyPath.IndexOf('[');
5556
if (indexOfArrayPart > -1)
5657
{
@@ -132,6 +133,31 @@ private bool TryGetCollectionItem(out ScriptableObject item)
132133
return true;
133134
}
134135

136+
private void ValidateLastKnownNames(ScriptableObject collectionItem)
137+
{
138+
if (collectionItem == null || collectionItem is not ISOCItem socItem)
139+
return;
140+
141+
bool changed = false;
142+
143+
if (!string.Equals(itemLastKnowNameSerializedProperty.stringValue, socItem.name, StringComparison.Ordinal))
144+
{
145+
itemLastKnowNameSerializedProperty.stringValue = socItem.name;
146+
changed = true;
147+
}
148+
149+
if (!string.Equals(collectionLastKnownNameSerializedProperty.stringValue, socItem.Collection.name, StringComparison.Ordinal))
150+
{
151+
collectionLastKnownNameSerializedProperty.stringValue = socItem.Collection.name;
152+
changed = true;
153+
}
154+
155+
if (changed)
156+
{
157+
itemLastKnowNameSerializedProperty.serializedObject.ApplyModifiedPropertiesWithoutUndo();
158+
}
159+
}
160+
135161
private void CreateCollectionItemPropertyDrawer(SerializedProperty serializedProperty)
136162
{
137163
collectionItemPropertyDrawer = new CollectionItemPropertyDrawer();

Scripts/Editor/PropertyDrawers/CollectionItemPickerPropertyDrawer.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,9 @@ private void AssignItemGUIDToProperty(ScriptableObject scriptableObject, Seriali
204204

205205
newProperty.FindPropertyRelative(COLLECTION_GUID_VALUE_A).longValue = collectionValues.Item1;
206206
newProperty.FindPropertyRelative(COLLECTION_GUID_VALUE_B).longValue = collectionValues.Item2;
207+
208+
newProperty.FindPropertyRelative("itemLastKnownName").stringValue = item.name;
209+
newProperty.FindPropertyRelative("collectionLastKnownName").stringValue = item.Collection.name;
207210
}
208211

209212
private void SetSelectedValuesOnPopup(PopupList<PopupItem> popupList, SerializedProperty property)

Scripts/Editor/PropertyDrawers/CollectionItemQueryPropertyDrawer.cs

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ private bool IsMatchTypeValid(SerializedProperty queryProp, int elementIndex, in
257257

258258
int otherEnumIndex = otherMatchTypeProp.enumValueIndex;
259259

260-
if (IsCombinationImpossible(candidateEnumIndex, otherEnumIndex))
260+
if (IsCombinationImpossible(candidateEnumIndex, candidateItems, otherEnumIndex, otherItems))
261261
return false;
262262
}
263263

@@ -292,7 +292,7 @@ private bool HasImpossibleRules(SerializedProperty queryProp)
292292
continue;
293293
int matchB = matchTypeBProp.enumValueIndex;
294294

295-
if (IsCombinationImpossible(matchA, matchB))
295+
if (IsCombinationImpossible(matchA, itemsA, matchB, itemsB))
296296
return true;
297297
}
298298
}
@@ -451,31 +451,43 @@ private static bool HasItemIntersection(HashSet<(long, long)> a, HashSet<(long,
451451
return false;
452452
}
453453

454-
private static bool IsCombinationImpossible(int matchA, int matchB)
454+
private static bool IsCombinationImpossible(
455+
int matchA, HashSet<(long, long)> itemsA,
456+
int matchB, HashSet<(long, long)> itemsB)
455457
{
456-
// 0 = Any, 1 = All, 2 = SomeNot (forbids any), 3 = None (forbids all)
457-
// Only truly impossible when overlapping items can never satisfy both rules:
458-
// - All + SomeNot: must have all AND must have none → impossible
459-
// - Any + SomeNot: must have at least one AND must have none → impossible
460-
// - All + None: must have all AND must not have all → impossible
461-
// - Any + None: must have at least one AND must not have all → satisfiable (partial overlap ok)
462-
int lo = Mathf.Min(matchA, matchB);
463-
int hi = Mathf.Max(matchA, matchB);
464-
465-
// All(1) + SomeNot(2)
466-
if (lo == 1 && hi == 2)
458+
// Caller guarantees itemsA ∩ itemsB is non-empty.
459+
// 0 = Any, 1 = All, 2 = NotAny, 3 = NotAll.
460+
//
461+
// All + NotAny: the overlap is forced-in by All and forced-out by NotAny → always impossible.
462+
// Any(X) + NotAny(Y): impossible iff every X item is also forbidden by Y (X ⊆ Y); otherwise an X-only item satisfies both.
463+
// All(X) + NotAll(Y): impossible iff every Y item is already required by X (Y ⊆ X); otherwise target = X misses a Y-only item.
464+
// Any + NotAll and all other pairings remain satisfiable in the general (non-degenerate-picker) case.
465+
466+
if ((matchA == 1 && matchB == 2) || (matchA == 2 && matchB == 1))
467467
return true;
468468

469-
// Any(0) + SomeNot(2)
470-
if (lo == 0 && hi == 2)
471-
return true;
469+
if (matchA == 0 && matchB == 2) return IsSubsetOf(itemsA, itemsB);
470+
if (matchA == 2 && matchB == 0) return IsSubsetOf(itemsB, itemsA);
472471

473-
// All(1) + None(3)
474-
if (lo == 1 && hi == 3)
475-
return true;
472+
if (matchA == 1 && matchB == 3) return IsSubsetOf(itemsB, itemsA);
473+
if (matchA == 3 && matchB == 1) return IsSubsetOf(itemsA, itemsB);
476474

477475
return false;
478476
}
477+
478+
private static bool IsSubsetOf(HashSet<(long, long)> candidate, HashSet<(long, long)> container)
479+
{
480+
if (candidate == null || container == null || candidate.Count == 0)
481+
return false;
482+
483+
foreach ((long, long) item in candidate)
484+
{
485+
if (!container.Contains(item))
486+
return false;
487+
}
488+
489+
return true;
490+
}
479491
}
480492
}
481493

Scripts/Runtime/Core/CollectionItemIndirectReference.cs

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ public abstract class CollectionItemIndirectReference: IEquatable<CollectionItem
2020
protected long collectionGUIDValueB;
2121
protected LongGuid CollectionGUID => new LongGuid(collectionGUIDValueA, collectionGUIDValueB);
2222

23+
#if UNITY_EDITOR
2324
[SerializeField]
2425
protected string itemLastKnownName;
2526
[SerializeField, FormerlySerializedAs("collectionLastKnowName")]
2627
protected string collectionLastKnownName;
28+
#endif
2729

2830
public bool Equals(CollectionItemIndirectReference other)
2931
{
@@ -65,7 +67,7 @@ public bool IsValid()
6567
}
6668

6769
[Serializable]
68-
public class CollectionItemIndirectReference<TObject> : CollectionItemIndirectReference
70+
public class CollectionItemIndirectReference<TObject> : CollectionItemIndirectReference, ISerializationCallbackReceiver
6971
where TObject : ScriptableObject, ISOCItem
7072
{
7173
[NonSerialized]
@@ -111,30 +113,6 @@ private bool TryResolveReference(out TObject result)
111113
return true;
112114
}
113115
}
114-
else
115-
{
116-
if (!string.IsNullOrEmpty(collectionLastKnownName))
117-
{
118-
if (CollectionsRegistry.Instance.TryGetCollectionByName(collectionLastKnownName, out collection))
119-
{
120-
SetCollection(collection);
121-
122-
if (!string.IsNullOrEmpty(itemLastKnownName))
123-
{
124-
if(collection.TryGetItemByName(itemLastKnownName, out ScriptableObject possibleResult))
125-
{
126-
result = possibleResult as TObject;
127-
if (result == null)
128-
{
129-
return false;
130-
}
131-
SetCollectionItem(result);
132-
return true;
133-
}
134-
}
135-
}
136-
}
137-
}
138116

139117
result = null;
140118
return false;
@@ -160,15 +138,29 @@ private void SetCollectionItem(ISOCItem item)
160138
(long, long) collectionItemValues = item.GUID.GetRawValues();
161139
collectionItemGUIDValueA = collectionItemValues.Item1;
162140
collectionItemGUIDValueB = collectionItemValues.Item2;
141+
#if UNITY_EDITOR
163142
itemLastKnownName = item.name;
143+
#endif
164144
}
165145

166146
public void SetCollection(ScriptableObjectCollection targetCollection)
167147
{
168148
(long,long) collectionGUIDValues = targetCollection.GUID.GetRawValues();
169149
collectionGUIDValueA = collectionGUIDValues.Item1;
170150
collectionGUIDValueB = collectionGUIDValues.Item2;
151+
#if UNITY_EDITOR
171152
collectionLastKnownName = targetCollection.name;
153+
#endif
154+
}
155+
156+
public void OnBeforeSerialize()
157+
{
158+
}
159+
160+
public void OnAfterDeserialize()
161+
{
162+
hasCachedRef = false;
163+
cachedRef = null;
172164
}
173165
}
174166
}

Scripts/Runtime/Core/CollectionItemPicker.cs

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections;
33
using System.Collections.Generic;
4+
using System.Text;
45
using UnityEngine;
56
using UnityEngine.Serialization;
67

@@ -11,14 +12,14 @@ namespace BrunoMikoski.ScriptableObjectCollections.Picker
1112
/// work if the enum had the [Flags] attribute applied to it.
1213
/// </summary>
1314
[Serializable]
14-
public class CollectionItemPicker<TItemType> : IList<TItemType>, IEquatable<IList<TItemType>>, IEquatable<CollectionItemPicker<TItemType>>
15+
public class CollectionItemPicker<TItemType> : IList<TItemType>, IEquatable<IList<TItemType>>, IEquatable<CollectionItemPicker<TItemType>>, ISerializationCallbackReceiver
1516
where TItemType : ScriptableObject, ISOCItem
1617
{
1718
[SerializeField, FormerlySerializedAs("cachedIndirectReferences")]
1819
private List<CollectionItemIndirectReference<TItemType>> indirectReferences = new();
1920

20-
public event Action<TItemType> OnItemTypeAddedEvent;
21-
public event Action<TItemType> OnItemTypeRemovedEvent;
21+
public event Action<TItemType> OnItemAddedEvent;
22+
public event Action<TItemType> OnItemRemovedEvent;
2223
public event Action OnChangedEvent;
2324

2425
private bool isDirty = true;
@@ -201,7 +202,7 @@ public void Add(TItemType item)
201202

202203
indirectReferences.Add(new CollectionItemIndirectReference<TItemType>(item));
203204
isDirty = true;
204-
OnItemTypeAddedEvent?.Invoke(item);
205+
OnItemAddedEvent?.Invoke(item);
205206
OnChangedEvent?.Invoke();
206207
}
207208

@@ -262,7 +263,7 @@ public bool Remove(TItemType item)
262263
{
263264
isDirty = true;
264265
OnChangedEvent?.Invoke();
265-
OnItemTypeRemovedEvent?.Invoke(removedItem.Ref);
266+
OnItemRemovedEvent?.Invoke(removedItem.Ref);
266267
}
267268

268269
return removed;
@@ -295,7 +296,7 @@ public void RemoveAt(int index)
295296
indirectReferences.RemoveAt(index);
296297
isDirty = true;
297298
OnChangedEvent?.Invoke();
298-
OnItemTypeRemovedEvent?.Invoke(removedItem.Ref);
299+
OnItemRemovedEvent?.Invoke(removedItem.Ref);
299300
}
300301

301302
public TItemType this[int index]
@@ -343,5 +344,33 @@ public bool Equals(CollectionItemPicker<TItemType> other)
343344

344345
return true;
345346
}
347+
348+
public void OnBeforeSerialize()
349+
{
350+
}
351+
352+
public void OnAfterDeserialize()
353+
{
354+
isDirty = true;
355+
}
356+
357+
public override string ToString()
358+
{
359+
if (Items.Count == 0)
360+
return "[]";
361+
362+
StringBuilder builder = new StringBuilder();
363+
builder.Append('[');
364+
for (int i = 0; i < Items.Count; i++)
365+
{
366+
if (i > 0)
367+
builder.Append(", ");
368+
369+
TItemType item = Items[i];
370+
builder.Append(item != null ? item.name : "<null>");
371+
}
372+
builder.Append(']');
373+
return builder.ToString();
374+
}
346375
}
347-
}
376+
}

0 commit comments

Comments
 (0)