ConditionalWeakTable<TKey,TValue> attaches managed state to a particular object reference without making the table an owner of that key. It fits data that should exist only while one live Player, creature, or other managed game object is reachable.

A table is useful when adding a field to the game type is not an option. It also avoids a static dictionary keyed by a player number, room index, or another identifier that can be reused. The key is the object instance itself, so two Player objects receive two state objects even if other game data describes them similarly.

Create state on first use

The compiled PlayerCounter example targets the recorded build. Its state and hook code is:

using System.Runtime.CompilerServices;
 
private readonly ConditionalWeakTable<Player, PlayerState> states =
    new ConditionalWeakTable<Player, PlayerState>();
 
private void PlayerUpdate(On.Player.orig_Update orig, Player self, bool eu)
{
    orig(self, eu);
    PlayerState state = states.GetValue(self, CreateState);
    state.Updates++;
    if (state.Updates % 600 == 0)
        Logger.LogDebug("Observed player updates: " + state.Updates);
}
 
private static PlayerState CreateState(Player player)
{
    return new PlayerState();
}
 
private sealed class PlayerState
{
    public ulong Updates;
}

GetValue(self, CreateState) returns the state already associated with that exact Player. If none exists, it calls CreateState, associates the result, and returns it. The ulong counter runs after orig, so it records completed passes through this handler. Its debug line reports every 600 observed update calls. That count makes no wall-clock timing promise. Hook lifecycle explains what returning from orig means when other hooks share the method.

The same shape can hold a cooldown, an observed flag, or a cached component reference. Revalidate cached game references at the point of use. A state object attached to a live player does not make a retained physical object valid after its room unloads. See Abstract and realized objects.

Conditional ownership

The table’s association can keep the value available while the key is reachable, but the association does not independently keep the key alive. Microsoft’s ConditionalWeakTable documentation specifies this ownership rule, including the case where the value refers back to the key.

flowchart TD
    R[External owner] -->|may retain| K[Player]
    R -->|may retain| V[State]
    K -. table association .-> V
    V -->|may refer back| K

If the external root is absent, a value that refers back to its key does not keep the key alive by itself. The key and value can be collected together. This property distinguishes a ConditionalWeakTable from a normal static dictionary whose key entry is a strong reference.

An external root changes the result. A static list or event delegate that reaches the Player keeps that key alive. An independently retained PlayerState also keeps the key alive if the state refers back to it. Remove those subscriptions and collection entries when their owner stops using them. The weak table removes the need for cleanup whose sole purpose would have been deleting the table association.

Collection timing is controlled by the garbage collector. Do not use collection as a gameplay event or expect the state to disappear immediately after a room transition.

Live state and persistent identity

State in a weak table follows a managed object instance. It does not survive by player slot, campaign, save file, or abstract entity identity. Respawning or loading can create a new Player and therefore a new PlayerState. A feature that must survive those transitions needs an explicit key and persistence format.

Rain World’s Player is a managed game object rather than a UnityEngine.Object subclass. Unity’s destroyed-object null behavior does not apply to the Player key. It can still apply to a Unity object stored inside PlayerState, so revalidate those references according to their own lifetime.

Use this distinction when choosing storage:

DataStorage direction
Cooldown for one current Player instanceConditionalWeakTable<Player, PlayerState>
Diagnostic count for one current creature instanceWeak table keyed by that creature
Setting shared by every playerRemix configuration or plugin state
Data that must survive save and loadCampaign save data
Reference that must follow an abstract object across room unloadsStable abstract identity with fresh lookup

Runtime checks

The public example compiled with Roslyn 5.9.0, C# 7.3, and the recorded game and hook assemblies through examples/build.ps1 -Example PlayerCounter. It was not loaded into Rain World. A game check should use two distinct player instances and confirm that each receives its own counter. Room unloading, respawn, and a new session should be observed separately because they can replace objects at different boundaries.

Garbage collection timing and Unity object destruction were not measured. A memory investigation should inspect independent roots before blaming the table. Event publishers and static collections are the first places to check when an old key or value remains reachable. Debugging and performance describes how to record the build and test sequence without logging every update.

Sources