Handles & capabilities

The entity, world, and nav handles goal callbacks receive — and the capability each op requires

View as Markdown

Goal callbacks declared with declare_mob receive three handles as positional arguments:

1def my_goal_tick(entity, world, nav):
2 ...

A handle is a thin, id-carrying value — it never holds a live entity pointer. Every access re-resolves the entity on the owning region’s store on the tick goroutine, so reading a mob that has since despawned returns a clean Starlark error (entity 42 no longer exists), never a stale reference. Handles are built fresh per callback call; stashing one across calls is safe but it still re-resolves on each access.

Every operation checks the owning plugin’s manifest capabilities at the operation boundary. A denied call raises a visible error:

capability denied: this plugin lacks "world.write"

The entity handle

The mob the goal is running for. str(entity) is <entity 42>; type(entity) is "entity".

Reads — require entities.read

AttributeTypeMeaning
x, y, zfloatPosition.
yaw, pitchfloatBody rotation.
on_groundboolGrounded flag.
typestringRegistry entity-type name (e.g. "pig").
velocity(float, float, float)Current (vx, vy, vz).
healthfloatAttribute-derived health value.
was_hurtboolTrue while the mob is in its hurt-flash window (hurtTime > 0).
last_damage_typeintDamage-type id of the mob’s last damage source.
has_last_damageboolWhether a real last-damage source was recorded (mirrors getLastDamageSource() != null).
in_waterboolAABB intersects water (the FloatGoal.canUse predicate).
fluid_heightfloatWater fluid height at the mob (max over the AABB).
in_lavaboolAABB intersects lava.
is_in_loveboolAnimal.isInLove() — fed and ready to breed.
is_babyboolAgeableMob.isBaby() (breed_age < 0).
breed_ageintThe signed age machine: <0 baby, >0 breed cooldown, ==0 adult.

Read methods

MethodCapabilityReturns
attribute(name)entities.readThe folded value of a named attribute (e.g. "movement_speed").
damage_in_tag(tag)entities.readWhether the last damage source is in the named damage-type tag (e.g. "panic_causes"). Membership lives host-side.
nearest_player_holding_food(tag, range)entities.readPosition tuple (x, y, z) of the nearest player holding an item in the named food tag ("cow_food", "cat_food", …), or None. The tempt-goal scan.
nearest_player_holding_pig_food(range) / nearest_player_holding_carrot_on_a_stick(range)entities.readPig-specific tempt scans (fixed item sets, host-owned).
nearest_breeding_partner(range)entities.readPosition of the nearest valid in-love same-class partner, or None (the BreedGoal.getFreePartner scan).
nearest_adult_parent(range)entities.readPosition of the nearest same-class adult for a baby’s follow goal, or None.

Mutations

MethodCapabilityEffect
move_to(x, y, z)entities.write and navSets a navigation target (routes through the async A* pathfinder). The Go nav/physics move the mob; this never teleports.
set_velocity(vx, vy, vz)entities.writeDirect velocity write (the Entity.setDeltaMovement analogue); integrated by physics next tick.
set_attribute(name, value)entities.writeOverrides the base value of a named attribute the entity has (errors if it doesn’t).
set_look(yaw, pitch=0.0)entities.writeSets head + body yaw (and pitch) instantly. Non-finite values are clamped to 0.
set_look_at(x, y, z)entities.writeAims the mob at a world point (the LookControl.setLookAt analogue; yaw derived host-side).
try_breed(range)entities.writeHost-side breed: re-finds the partner and runs the full vanilla breed (child spawn, RNG draws, XP). Returns True if a breed fired.
eat_grass_block()world.writeThe sheep EatBlockGoal eat: converts the grass block below to dirt, regrows wool. Only ever touches the mob’s own below-position.
eat_broadcast_byte10()entities.writeBroadcasts the eat-animation entity event (byte 10) to trackers.

Per-mob RNG and goal state — no capability required

MethodReturns
rand_int(n)Random int in [0, n) from the mob’s per-entity seeded RNG (the Mob.getRandom() stream).
rand_float()Random float in [0, 1) (nextFloat).
rand_double()Random float in [0, 1) (nextDouble — a distinct draw from rand_float; use whichever the vanilla goal you’re porting uses, or the RNG stream diverges).
get_state(key, default=0.0)Reads a per-(mob, goal) scratch value — the analogue of a Go goal’s struct fields (countdowns, cached targets).
set_state(key, value)Writes a scratch value (numbers only). Each spawned mob gets its own scratch per goal; two mobs never share a countdown.

The RNG methods draw from the mob’s own deterministic per-entity stream. Sulfur’s vanilla-mob plugins rely on this to stay in RNG lockstep with the ported Java code — a plugin-driven pig and a Go-native pig draw byte-identical streams.

The world handle

type(world) is "world".

MethodCapabilityReturns / effect
block_at(x, y, z)world.readTuple (state_id, ok)ok is False for an unloaded column (never a silent 0).
set_block(x, y, z, state)world.writeSets a block state and broadcasts the update to clients. Returns True if the block changed.
entities_near(x, z, radius_chunks)world.readList of entity handles for mobs in the per-section buckets around the position (same capability set carried; reading them still requires entities.read). Scoped to the calling region’s store.
nearest_player(x, y, z, max_dist)world.readPosition tuple (px, py, pz) of the nearest player in range, or None. Players are not in entities_near — use this.
is_dark()world.readTrue when it is dark enough for hostiles (the day/night gate used by spawn logic and the spider’s daylight-flee).

The nav handle

The mob’s navigation. type(nav) is "nav".

MethodCapabilityEffect
path_to(x, y, z)navRequests a path to the target via the async A*. Sets a want; the Go navigation and physics do the moving.
has_path()noneTrue while the nav has an active target (!navigation.isDone()); flips false on arrival.
stop()navClears the pending nav target (navigation.stop()).
jump()navArms the mob’s jump control (one impulse per tick; repeated calls within a tick are idempotent).

path_to also accepts an advanced 31-float form (10 raw stroll candidates plus a land-mode flag) used by the vanilla stroll-goal ports to hand candidate selection to the host. For custom mobs, the 3-argument form is what you want.

Design rules the handles enforce

  • Goals set targets; Go moves mobs. There is no raw position write. Movement always flows through navigation → physics → collision, so a plugin mob obeys the same movement rules as a vanilla one.
  • Single-owner tick. Callbacks run on the owning region’s goroutine; handles resolve against that region’s entity store. A callback can only see its own region’s entities (matching the Folia-style region-ownership discipline).
  • Reads return copies. Scalars and tuples only — no live references ever cross into Starlark.