This tutorial adds one yellow spark to the nearest eligible rock when a player newly presses jump. It demonstrates a bounded room scan and a cosmetic result. It does not grab the rock, change its velocity, create an abstract object, or write save data.

The complete RockMarker source is in the repository. Its plugin GUID is rainworldmodding.rockmarker, which matches the included modinfo.json.

Attach the player hook

Use the subscription pattern from First plugin and remove the same named delegate when the component is disabled:

private void OnEnable()
{
    On.Player.Update += PlayerUpdate;
}
 
private void OnDisable()
{
    On.Player.Update -= PlayerUpdate;
}

The handler calls orig(self, eu) first. eu is forwarded unchanged from the room’s update parity and is not treated as elapsed time.

Accept one fresh jump

After global::Player.Update(System.Boolean) returns, reject states where interaction would be ambiguous or unsafe. The example requires a current room and a conscious player. It skips global::UpdatableAndDeletable.slatedForDeletetion, NPC players, shortcut travel, and incomplete input history.

The edge check is:

if (!self.input[0].jmp || self.input[1].jmp)
    return;

input[0] is the current input package and input[1] is the preceding package in this context. The condition passes on the first update where jump is down. Holding jump does not emit a marker on each update.

Calling orig first means the selection uses the player’s state and object positions after the game update. Hook lifecycle explains the continuation chain and its ordering limits.

Search every collision layer

global::Room.physicalObjects groups objects by collision layer. Iterate every inner collection. Searching only the player’s layer could miss a rock that the room currently tracks elsewhere.

The example starts with best = 60f * 60f and compares squared distances with <. A candidate exactly 60 world units away is excluded. For each object, it accepts only global::Rock instances that:

  • still belong to the player’s current room
  • are not slated for deletion
  • have no entries in grabbedBy
  • use global::Weapon.Mode.Free
  • pass global::Room.VisualContact(UnityEngine.Vector2, UnityEngine.Vector2) from the player’s main body chunk

Positions come from global::BodyChunk.pos, so the radius is measured in world space rather than abstract tiles. The scan only remembers the nearest rock. It does not modify candidates while iterating the room collections.

float distance = (rock.firstChunk.pos - self.mainBodyChunk.pos).sqrMagnitude;
if (distance < best && self.room.VisualContact(self.mainBodyChunk.pos, rock.firstChunk.pos))
{
    best = distance;
    nearest = rock;
}

This is interaction with a realized object. Abstract and realized objects explains why the scan starts from the active room and why keeping a global::Rock reference across room unloading would require revalidation.

Add a cosmetic marker

After the loops finish, create one global::Spark at nearest.firstChunk.pos and pass it to global::Room.AddObject(global::UpdatableAndDeletable). Adding the effect after the scan avoids changing updateList while iterating the room’s physical object groups.

The global::Spark constructor consumes Unity random values. A cosmetic marker should not advance the game’s shared random sequence, so the example saves and restores UnityEngine.Random.state in a try and finally block:

var randomState = Random.state;
try
{
    var marker = new Spark(nearest.firstChunk.pos, Vector2.up * 2f, Color.yellow, null, 8, 12);
    marker.pos = marker.lastPos = nearest.firstChunk.pos;
    marker.lifeTime = 10;
    self.room.AddObject(marker);
}
finally
{
    Random.state = randomState;
}

The marker is a realized room effect with a short lifetime. It needs no global::AbstractRoom.AddEntity call.

Build and test

Run from the repository root:

powershell -ExecutionPolicy Bypass -File ./examples/build.ps1 -GamePath "C:/Program Files (x86)/Steam/steamapps/common/Rain World" -Example RockMarker

The final public source compiled against the installed baseline using .NET SDK 10.0.400. The script performs no restore or deployment. Its output is examples/RockMarker/bin/RockMarker.dll.

The game was not launched during verification. Test fresh and held jump input, rocks inside and exactly on the radius, blocked line of sight, held rocks, room transitions, mod toggling, and more than one local player. Each cooperative player runs the hook independently, so two players may mark the same rock. Online synchronization and compatibility with other runtime hooks remain open tests. Runtime reference gives the exact game and hook assembly identities for this compile check.

Sources