declare_mob reference

Declare custom mobs with AI goals — captured once at load, ticked by the Go goal selector

View as Markdown

declare_mob registers a custom mob type. Like everything in Sulfur’s plugin API it is a load-time capture: the declaration is stored once when the module body runs; each spawned mob gets a fresh per-mob AI referencing the shared frozen callables. The Starlark interpreter fires only inside a running goal’s callbacks — an idle declared mob makes zero Starlark calls per tick.

Signature

1declare_mob(
2 name = "wanderer", # unique mob name (duplicate = load error)
3 base_type = "pig", # an existing vanilla entity type (see the allowed list)
4 attributes = {...}, # optional: attribute overrides (string -> number)
5 goals = [goal(...), ...], # optional: AI goals
6 skills = [skill(...), ...], # optional: declared skills — see the Skills page
7)

base_type — custom means custom behavior

A declared mob is a custom behavior, not a new wire entity type: base_type resolves to an existing 26.2 entity, and the spawned mob uses that type’s wire id, hitbox dimensions, and attribute supplier — so an unmodified vanilla client renders it natively. An unknown base_type is a loud load error.

Allowed values: pig, cow, sheep, chicken, skeleton, creeper, spider, zombie, cat, witch, villager, silverfish, wolf, husk, mooshroom, rabbit, enderman, fox, endermite, turtle, ocelot, pillager, vindicator, evoker, ravager, iron_golem, sulfur_cube, happy_ghast.

attributes

A dict of attribute overrides applied on top of the base type’s real attribute supplier. Friendly names are aliased to registry keys; any attribute the entity’s supplier registers can be overridden:

NameMeaning
max_healthMax health (mob spawns at full health).
movement_speedMovement speed; also scales the declared mob’s walk pace proportionally.
follow_rangeBounds the target-goal scan box (hostiles).
attack_damageMelee damage dealt by Mob.doHurtTarget.
armorFolds into the victim-side armor curve.

Non-string keys and non-numeric values are load errors. An attribute the base type doesn’t have is skipped at spawn (the base type itself was validated at load).

goal(...)

1goal(
2 priority, # int — lower runs first (the vanilla goal-selector slot)
3 flags, # list of control flags: "MOVE", "LOOK", "JUMP", "TARGET"
4 tick = None, # callable(entity, world, nav) — runs each tick while the goal is running
5 can_use = None, # callable -> truthy — may the goal start? (default: always)
6 start = None, # callable — fires on the not-running -> running edge
7 stop = None, # callable — fires on the running -> not-running edge
8 can_continue = None, # callable -> truthy — may it keep running? (default: can_use)
9 requires_update_every_tick = False,# bool — tick on the every-tick path (jar goals that need it)
10 kind = None, # string — route to a Go-native jar-ported goal instead of callbacks
11 avoid_type = None, # string — required iff kind == "avoid_entity" (the class to flee)
12)

Rules, all enforced at load:

  • flags must be from the closed set MOVE, LOOK, JUMP, TARGET (the exact four vanilla Goal$Flag values). Goals claiming TARGET are arbitrated by the independent target selector, exactly as vanilla routes HurtByTargetGoal/NearestAttackableTargetGoal.
  • A callback goal needs at least one of tick / start / can_use.
  • A kind= goal must supply no callbacks and no requires_update_every_tick — the Go-native goal owns the behavior and its cadence. Its declared flags must match the native goal’s own flag set.
  • avoid_type is required for kind = "avoid_entity" and forbidden everywhere else.

All callbacks receive the three handles (entity, world, nav) — see Handles & capabilities. Callbacks run on fresh step-budgeted threads with error isolation: a failing can_use counts as False, a failing tick is logged and skipped, and the tick loop survives.

kind= — Go-native goals

Some vanilla goals are the same class for every mob (combat targeting, melee chase, sun-flee, …) with nothing per-mob to express in a script — and re-expressing their RNG in Starlark would risk drift. A kind= goal declares only the priority + flags and routes to the verbatim Go port. The mob’s per-entity RNG stream is shared between native and scripted goals, so both stay in lockstep.

Available kinds include (each a 1:1 port of the named jar goal):

nearest_attackable_target, hurt_by_target, melee_attack, ranged_bow_attack, spider_attack, leap_at_target, avoid_entity, float, climb_on_powder_snow, creeper_swell, witch_ranged_attack, restrict_sun, flee_sun, sit, follow_owner, owner_hurt_by, owner_hurt, angry_player_target, skeleton_target, enderman_look_for_player, enderman_freeze_when_looked_at, enderman_take_block, enderman_leave_block, silverfish_merge_stone, silverfish_wake_friends, cube_float, cube_random_direction, cube_keep_on_jumping, fox_faceplant, fox_stalk, fox_pounce, fox_seek_shelter, fox_sleep, fox_perch_search, fox_defend_trusted, fox_land_target, fox_search_items, turtle_goto_water, turtle_go_home, turtle_travel, turtle_lay_egg, nearest_healable_raider_target, cat_relax_on_owner, cat_lie_on_bed, cat_sit_on_block, long_distance_patrol, iron_golem_hostile_target, pillager_crossbow_attack, evoker_casting_spell, evoker_summon_spell, evoker_attack_spell, evoker_wololo_spell.

An unknown kind fails loudly — a hostile with a typo’d combat goal never boots silently disarmed.

Example: a minimal wandering mob

The wandermob test plugin declares a pig-based mob with a single MOVE goal that keeps it walking east via the real Go pathfinder:

1# on_wander_tick fires while the MOVE goal runs. It receives (entity, world, nav) handles.
2# If the mob has no active path it picks a nearby target and asks the Go nav to path there.
3# The Go nav + physics then walk the mob (the goal SETS a target; it never moves the mob).
4def on_wander_tick(entity, world, nav):
5 if not nav.has_path():
6 nav.path_to(entity.x + 8.0, entity.y, entity.z)
7
8declare_mob(
9 name = "wanderer",
10 base_type = "pig",
11 attributes = {
12 "max_health": 12.0,
13 "movement_speed": 0.25,
14 },
15 goals = [
16 goal(priority = 6, flags = ["MOVE"], tick = on_wander_tick),
17 ],
18)

Example: a real vanilla mob (excerpt)

The bundled vanilla_cat plugin is a method-for-method port of the 26.2 jar’s Cat.registerGoals, mixing scripted passive goals with kind= natives:

plugins/mobs/vanilla_cat/main.star (goals excerpt)
1declare_mob(
2 name = "vanilla_cat",
3 base_type = "cat",
4 # ...
5 goals = [
6 # @1 FloatGoal / TamableAnimalPanicGoal — scripted passive callbacks (shared port)
7 # ...
8 # @2 SitWhenOrderedToGoal [JUMP, MOVE] — Go-native
9 goal(priority = 2, flags = ["JUMP", "MOVE"], kind = "sit"),
10 # @3 CatRelaxOnOwnerGoal (no flags) — Go-native comfort goal
11 goal(priority = 3, flags = [], kind = "cat_relax_on_owner"),
12 # @4 CatTemptGoal(0.6, CAT_FOOD, true) [MOVE, LOOK] — scripted
13 goal(
14 priority = 4,
15 flags = ["MOVE", "LOOK"],
16 can_use = tempt_food_can_use,
17 tick = tempt_food_tick,
18 stop = tempt_food_stop,
19 can_continue = tempt_food_continue,
20 ),
21 # @6 FollowOwnerGoal(1.0, 10.0, 5.0) [MOVE] — Go-native
22 goal(priority = 6, flags = ["MOVE"], kind = "follow_owner"),
23 # @10 BreedGoal(0.8) [MOVE, LOOK] — scripted
24 goal(
25 priority = 10,
26 flags = ["MOVE", "LOOK"],
27 can_use = breed_can_use,
28 tick = breed_tick,
29 stop = breed_stop,
30 can_continue = breed_continue,
31 ),
32 # @11 WaterAvoidingRandomStrollGoal(0.8) [MOVE] — scripted
33 goal(
34 priority = 11,
35 flags = ["MOVE"],
36 can_use = stroll_can_use,
37 stop = stroll_stop,
38 can_continue = stroll_continue,
39 ),
40 # @12 LookAtPlayerGoal(Player, 10.0) [LOOK] — scripted
41 goal(
42 priority = 12,
43 flags = ["LOOK"],
44 can_use = look_can_use,
45 start = look_start,
46 tick = look_tick,
47 stop = look_stop,
48 can_continue = look_continue,
49 ),
50 ],
51)

How a declared mob ticks

  1. Spawn builds a fresh entity from the base type (wire id, hitbox, attribute supplier), applies the declared attribute overrides, initializes health to max, and builds a fresh AI whose goals reference the shared frozen callables. Each mob’s RNG is reseeded by entity id so two mobs of the same declaration wander independently.
  2. Arbitration runs through the same goal selector as Go-native mobs: goals claim their control flags by priority; a scripted goal is not a bypass and not a parallel tick.
  3. Callbacks fire only for a running goal (can_use is evaluated when the selector offers the goal its flags; tick only while it holds them). Per-goal, per-mob mutable state lives in the get_state/set_state scratch.
  4. If the declaration carries skills, a per-mob skill runner is attached and the spawn trigger fires — see Skills.