ConditionalWeakTable per-instance cache¶
ConditionalWeakTable<TKey, TValue> stores a per-instance value keyed weakly on TKey. When the key is GC'd, the entry is removed automatically; no manual cleanup code. Ideal for "one piece of state per Thing instance" mod caches whose lifetime should match the underlying instance.
Problem¶
Mods that attach state to specific game objects ("the dish currently auto-aiming at target X") need somewhere to store it. Options:
- Patch the class to add a field: invasive, conflicts with other mods.
Dictionary<TKey, TValue>keyed byReferenceId: survives the Thing's destruction, leaks forever.Dictionary<Thing, TValue>: holds a strong reference to the Thing, preventing collection.ConditionalWeakTable<Thing, TValue>: weak-keyed, entry cleaned automatically on GC.
Solution / recipe¶
F0044 (Mods/PowerTransmitterPlus/RESEARCH.md:211-239, primary; auto-aim implementation):
AutoAimState(static): -ConditionalWeakTable<WirelessPower, StrongBox<long>> _target: per-dish cache. Lifetime-tied to the dish instance, self-cleans on GC. -[ThreadStatic] bool _suppressReset: re-entry flag set during our own servo writes. -HandleWrite(dish, newId): - Cache hit -> early return. -newId == 0-> cache 0, no aim change. -Thing.Find(newId) == null || target == dish-> return WITHOUT updating cache (so a later rewrite of the same id re-attempts lookup). - Otherwise: pivot-to-pivot geometry -> setRotatableBehaviour.TargetHorizontal/TargetVerticalunder suppression flag.
F0219y (Plans/StationpediaPlus/PLAN.md:3708-3721) summarizes the pattern:
PowerTransmitterPlus's
AutoAimPatchesstores per-dish target state in aConditionalWeakTable<WirelessPower, StrongBox<long>>. GC-tied cleanup: when aWirelessPowerinstance is collected, its cache entry is automatically reclaimed. No manual lifecycle management. Cache entries are set byMicrowaveAutoAimTargetwrites and cleared by manual Horizontal/Vertical adjustments via postfixes onRotatableBehavioursetters (with a re-entry flag to suppress clearing during auto-aim's own writes).
Value-type wrapping¶
ConditionalWeakTable<TKey, TValue> requires TValue to be a reference type. For primitive payloads (long, int), wrap in StrongBox<T>:
private static readonly ConditionalWeakTable<WirelessPower, StrongBox<long>> _target =
new ConditionalWeakTable<WirelessPower, StrongBox<long>>();
// Write:
_target.AddOrUpdate(dish, new StrongBox<long>(newId));
// Read:
if (_target.TryGetValue(dish, out var box)) { var id = box.Value; }
The StrongBox<T> is a managed object; the weak key still lets the dish be collected.
Re-entry suppression¶
F0044 also documents the [ThreadStatic] bool _suppressReset companion: when the mod's own logic writes to RotatableBehaviour.TargetHorizontal / TargetVertical, a postfix on that setter clears the cache. Without a suppression flag, the mod's own write triggers cache-clear immediately after it was set. The pattern applies generally whenever the cache is maintained by patching the same setters the mod itself calls.
Cited verifications¶
- F0044: primary source with full implementation context (auto-aim in PowerTransmitterPlus).
- F0219y: Stationpedia-Plus's summary cite, independently confirming the pattern.
Verification history¶
- 2026-04-20: page created from the Research migration; F0044 primary, F0219y cited summary.
Open questions¶
None at creation.