Skip to content

Stope — API

Status key: stable (semver-covered), experimental (may change in a MINOR release; marked in Javadoc).

API Since Status Purpose
api.Stope 1.0.0 stable MOD_ID, shared logger.
api.event.PlayerFootfallCallback 1.0.0 experimental Server-side event when a player’s feet land on a block (new block or after being airborne). Never fires for jumping over, elytra gliding, riding, or spectating.
api.event.PlayerMountFootfallCallback 1.0.0 experimental Server-side event when a living mount ridden by a player lands on a block. Boats and minecarts never fire it.
api.event.MobFootfallCallback 1.1.0 experimental Server-side event when a mob’s feet land on a block. Never fires while the mob rides something or a player rides it.
api.data.StopeSavedData.getOrCreate 1.0.0 experimental Per-dimension saved data without the version-specific vanilla signature.
api.data.DataReloaders.register 1.0.0 experimental Server data-pack reload handler in one call.
api.registry.VanillaBlocks.shortGrass 1.0.0 experimental The short grass plant across its 1.20.3 rename.
api.event.MobHurtCallback 1.2.0 experimental Server-side event after a hit lands on a mob, with the attacker.
api.event.MobKilledCallback 1.2.0 experimental Server-side event after a mob dies, with the killer.
api.event.MobTargetChangedCallback 1.2.0 experimental Server-side event when Mob#setTarget changes the target.
api.event.AnimalBredCallback 1.2.0 experimental Server-side event when a player-fed pair produces a baby, before the baby joins the level.
api.event.MobGroupChangedCallback 1.2.0 experimental Server-side event when a mob joins or leaves a mob group.
api.event.ReactionStartedCallback 1.2.0 experimental Server-side event each time the reaction engine starts a reaction for a group.
api.group.MobGroups, GroupDefinition, MobGroup, LeaderChoice 1.2.0 experimental Same-type mob groups: definitions per consumer, live groups with leader, center, and losses; line-of-fire check.
api.ai.MobControl, TargetVeto 1.2.0 experimental Alarm, avoid, attack, move to a point, hold, leap, and calm a mob, for goal and brain mobs alike; whether a mob is engaged, has a movement order (hasMoveOrder), or stopped short of a point (moveStoppedShort); vetoes on attack targets.
api.config.PlayerGuide 1.2.0 experimental Copies a Markdown guide shipped in a mod’s jar to its config folder for players.
api.client.config.SettingsScreen, UnsavedChangesScreen 1.2.0 experimental Client: a settings screen built from toggle and number rows, with an unsaved-changes prompt.
api.reaction.ReactionBinding, Reactions, Triggers, Reaction, ReactionContext, ReactionParams, EntityMatcher 1.2.0 experimental The trigger-to-reaction vocabulary: binding codec, registries, built-in triggers, and the primitives flee, avoid, defend, join_attack, follow_leader.
api.config.ConfigFile, ConfigSpec, ConfigKey, ConfigValues 2.0.0 experimental A mod’s JSON settings file: strict parsing, per-key reset with a warning naming the key, immutable values, save for settings screens; ConfigFile.folder names a mod’s config folder.
api.event.BlockChangedCallback 2.0.0 experimental Server-side event after Level#setBlock changes a block.
api.event.BrushStrokeCallback 2.0.0 experimental Server-side event for each brush stroke a player makes on a block.
api.mob.MobOwnership 2.0.0 experimental Whether a mob is tamed, owned, saddled, ridden by a player, leashed, or in a caravan; isKeptByPlayer combines them.
api.data.DataReloaders.registerFiles, DataFile 2.0.0 experimental Reload handler that receives the JSON files of a data-pack directory and a config folder, each with its source and pack ID, in a documented load order.
api.registry.Ids, RegistryLookups 2.0.0 experimental Build and parse IDs; look up entity types, blocks, block tag contents, and tag keys by ID.
api.registry.Registrar, Registered 2.1.0 experimental Register a mod’s blocks, items, entity types, block entity types, features, creative tabs, block set types, and wood types on every loader.
api.registry.BoatSet, BoatWood, BoatKind 2.1.0 experimental A mod’s boats and chest boats, one entity type each for all its woods, with items, dispensers, and rendering on every loader.
api.worldgen.BiomeFeatures 2.1.0 experimental Add a mod’s placed feature to the biomes in a tag, with a switch read at server start, on every loader.
api.loot.LootAdditions 2.1.0 experimental Add a pool to a loot table, or a chance to replace a brushed archaeology item, on every loader.
api.client.render.BlockRendering 2.1.0 experimental Client: a block’s render layer (cutout, cutout-mipped, translucent) and block and item color providers on every loader.
api.client.render.CreativeTabIcons 2.2.0 experimental Client: a creative tab’s icon drawn from a texture (a mod’s icon) instead of an item, on every loader.
api.client.render.Colormap 2.3.0 experimental Client: a colormap read from a texture and reloaded with the client’s resources, for block colors vanilla’s colormaps do not cover.
api.registry.BlockBehaviors 2.1.0 experimental Make blocks flammable, axe-strippable, compostable, and furnace fuel, and let sign blocks use vanilla’s sign block entities, on every loader.
api.client.debug.DebugScreenLinesCallback 2.0.0 experimental Client: append lines to the F3 screen’s right column, with the targeted block; hidden under reduced debug info.
api.event.ServerStartedCallback 2.0.0 experimental Server-side event once the server has started (worlds loaded, tags bound).
api.event.DataPacksReloadedCallback 2.0.0 experimental Server-side event after a data-pack reload succeeded. Not fired at startup.
api.event.LevelTickCallback 2.0.0 experimental Server-side event at the end of each server level’s tick.
api.event.CommandRegistrationCallback 2.0.0 experimental Add commands each time the server builds its command tree.
api.platform.PlatformInfo 2.0.0 experimental Whether a mod is loaded, its version, and whether this is a development launch.
api.network.NetworkChannel, PacketType, ServerPacketHandler, ClientPacketHandler 2.0.0 experimental A versioned channel: declare packets, send to a player; handlers run on the main thread; a different protocol version is refused at login.
api.client.network.ClientNetworking 2.0.0 experimental Client: attach clientbound handlers, check and send serverbound packets.
api.client.event.ClientDisconnectCallback 2.0.0 experimental Client: event after the client leaves a world.
api.client.config.ConfigScreens 2.0.0 experimental Client: a mod’s config screen in Mod Menu (Fabric) and the Mods screen (Forge).
api.event.Event 2.0.0 experimental The registration side of every callback’s EVENT field, the same on every loader; replaces Fabric’s Event in the public callbacks.
api.gametest.GameTestFunctions 2.0.0 experimental GameTests written once in common code, registered on each loader through a @GameTestGenerator. Ships in the jar for consumers’ GameTest source sets.

Everything starts experimental until a second mod (or a second Minecraft version) proves the shape. Promote to stable in a MINOR release and note it in the changelog.

PlayerFootfallCallback.EVENT.register((player, level, pos, state) -> {
// TrailWright: add wear to the block at pos
});
  • Runs on the server thread at the end of ServerPlayer#tick.
  • pos is the supporting block under the player’s feet (Entity#getOnPos), not the block the player stands in.
  • Fires once when a grounded player’s supporting block changes, and once on every landing, even on the same block. Standing still never fires.
  • Does not fire on the first grounded tick of a new ServerPlayer (login, respawn).
  • Never fires while airborne, elytra gliding, riding, or in spectator mode. Position memory keeps updating during those, so the first step afterward compares against where the player actually was.
  • Sneaking is not filtered. Listeners that care check player.isShiftKeyDown().
  • A supporting block that changes while the feet stay grounded in the same block (snow layers building up underfoot) is not noticed until the feet move or land. The same holds for mounts and mobs.
  • Fires instead of PlayerFootfallCallback while the player rides a LivingEntity (horse, pig, strider, camel). Never fires for boats, minecarts, or other non-living vehicles.
  • Same rules as the player event, applied to the mount’s feet: pos is the block under the mount, once per new block and once per landing.
  • Player and mount share one position memory. Mounting can fire once, when the mount’s block differs from where the player last stood. The first step after dismounting compares against the mount’s last block.
  • Fires once per rider: two players on one camel each get its footfalls.
  • rider.isShiftKeyDown() and rider.isSprinting() describe the rider’s input, if a listener wants them.
  • Runs on the server thread at the end of Mob#tick, for every Mob (animals, monsters, villagers). Players are not mobs; they have their own events.
  • Same footfall rules as the player event: pos is the block under the mob’s feet, once per new block and once per landing. Standing still never fires, and neither does the first grounded tick after the mob loads.
  • Never fires while the mob is a passenger (a pig in a boat or minecart).
  • Never fires while a player rides the mob. PlayerMountFootfallCallback reports those footfalls, so each footfall reaches listeners once. A mob ridden only by other mobs (a chicken jockey’s chicken) still fires.
  • Position memory keeps updating while a player rides, so the first step after the player dismounts compares against where the mob really was.
  • Listeners run for every loaded mob that steps. Reject cheaply (config off, entity type) before doing real work.
  • One instance per dimension: the Overworld and the Nether get separate instances and files.
  • Repeated calls in one dimension return the same instance until the world unloads.
  • id becomes the file name, data/<id>.dat in that dimension’s folder. Prefix it with your mod ID.
  • Vanilla writes the data when the world saves, but only after setDirty(). Call it after every change.
  • Fires at the end of LivingEntity#hurt when it returns true, for mobs on the server. Hits that do not land (invulnerability frames, cancelled damage) never fire.
  • attacker is DamageSource#getEntity(): the shooter for arrows, null for falls, drowning, and other sourceless damage. amount is the value passed to hurt, before armor.
  • Runs inside the hurt call. Record state or give MobControl orders, which apply next tick.
  • Fires after a mob dies (Fabric ServerLivingEntityEvents.AFTER_DEATH), while it is still in the level. By then it has left its mob group.
  • Fires at the end of Mob#setTarget when the target changed, including to and from null.
  • Brain mobs that keep their target only in brain memory (piglins, hoglins, goats) never call setTarget and never fire it.
  • Fires at the start of Animal#finalizeSpawnChildFromBreeding, before the parents’ love state resets and before the baby is added to the level, so a listener can tag the baby.
  • player is the player who fed parent, else the one who fed partner. Pairs nobody fed never fire it. Turtles, frogs, and sniffers spawn no baby at breeding and never fire it.
  • Fires on every join and every leave; exactly one of left and joined is set. Moving between groups fires a leave, then a join. Starting a new group is a join (size 1).
  • Leaves include unloading, death, failing the filter, straying, and the first refresh after the group was dissolved by define (then left.isDissolved() is true).
  • For logging and bookkeeping. Do not change groups from a listener.
  • Fires after a reaction’s start returns without throwing: for triggered bindings, each then step (with the binding’s trigger), and standing behaviors (trigger null).
  • For logging. Give no orders from a listener.
MobGroups.define(new ResourceLocation("mymod", "herds"), List.of(
GroupDefinition.builder(new ResourceLocation("mymod", "cows"))
.type(EntityType.COW).tag(new ResourceLocation("mymod", "cattle"))
.radius(16).maxSize(12).leader(LeaderChoice.NONE).refreshInterval(20)
.filter(mob -> !mob.isLeashed())
.binding(ReactionBinding.parse(json, "mymod"))
.build()));
  • define replaces every definition of that owner and dissolves the owner’s live groups; mobs regroup at their next refresh. Call it from a DataReloaders handler.
  • A group holds one entity type. A definition naming several types forms separate groups per type. When two owners cover a type, the owner registered earlier wins (logged); calling define again moves an owner behind the others.
  • Each grouped mob checks membership once per refreshInterval ticks, offset by entity ID. It leaves a group whose center is farther than radius, or when it fails filter. It joins the largest nearby group of its definition and type that has room and is at least as large as its own, so neighboring groups drain into one. Otherwise it starts a group.
  • The first member’s check also recomputes the center (the mean of member positions) and the leader. A member far from the others pulls the mean toward itself, so after a split either side may keep the old MobGroup object.
  • groupOf is allocation-free. A MobGroup view is dead once isDissolved().
  • Leaders: OLDEST is the adult in the group longest; LARGEST the adult with the largest bounding box (ties to the one in longest). Babies lead only when no adult is present.
  • Losses: member deaths are counted per group (lossesWithin(ticks), up to 32 recent deaths), with the last killer.
  • Groups are not saved. They rebuild within one refresh interval after a load.
  • anyInLineOfFire(shooter, target, width): whether a member of any group (any owner), other than the two, stands between the shooter’s eyes and the middle of the target, within width of the line plus half its own width, and within width above or below its height. Allocation-free; it walks the group cells around the line.
  • One movement order (alarm, avoid, move to a point, hold) and one attack order per mob. A new order of the same kind replaces the old. Orders apply on the mob’s next tick and end by themselves after ticks.
  • Goal mobs get two Stope goals the first time they receive an order: a movement goal at priority 0 (MOVE, LOOK) and a target goal at priority 0 (TARGET). Brain mobs (any mob whose brain registers WALK_TARGET) are driven through memories from the end of their tick: WALK_TARGET for movement, IS_PANICKING while alarmed, AVOID_TARGET while avoiding, and ATTACK_TARGET, ANGRY_AT, and RAM_TARGET for attacks, each only when that brain registers it.
  • alarm runs the mob to points away from the source (vanilla PanicGoal‘s short random runs without one) at the species’ vanilla panic speed, read from its PanicGoal or AnimalPanic, or 1.25, or at the speed given to the four-argument alarm. It does not start vanilla panic, which needs real damage.
  • A moving order (alarm, avoid, move) stops a horse, donkey, mule, llama, or camel eating (AbstractHorse#isEating), checked every tick it lasts: an eating horse cannot move and starts eating at random.
  • leap(mob, awayFrom, height, push) launches a mob standing on the ground about height blocks up, pushed push blocks per tick away from awayFrom (random direction when null), and sends the motion to clients at once, as knockback does. A one-time push; any movement order carries on. Chickens flutter down slowly, as in vanilla.
  • hold stops the mob where it stands and keeps it facing lookAt (if given) for ticks. Brain mobs lose any walk target their idle behaviors set meanwhile and get LOOK_TARGET.
  • While a movement order is active, a goal mob’s own melee and bow attacks pause (Stope’s movement goal takes the MOVE and LOOK flags they need); the mob keeps its target and attacks again when the order ends. A held skeleton holds fire.
  • hasMoveOrder is true while the mob has an alarm, avoid, move, or hold order that has not ended.
  • isEngaged is true while the mob has an attack order, a live target of its own, or (brain mobs) an ATTACK_TARGET memory.
  • moveStoppedShort(mob, withinTicks) is true when the mob’s last moveTo ended short of its point within that many ticks (a goal mob’s path stopped short, or the order ran out of time); an arrival clears it. Use it to stop re-sending a mob toward a point it cannot reach.
  • attack holds the target against the mob’s own target goals. The mob fights with the attacks it has: cows only target. Neutral mobs also get persistent anger. Goats ram, aimed at the target’s position when each ram starts, until one ram hits; a ram that ends without hitting (the target moved, or vanilla aborted it) is asked for again 10 ticks later, and after the hit the order rams no more. An order on a new target, or one given after the last order ended, clears the ram cooldown; repeating the same target while its order lasts only extends that order.
  • An attack order that runs out leaves the target for vanilla AI: a goal mob’s target, a neutral mob’s anger, and a brain mob’s ATTACK_TARGET and ANGRY_AT stay. calm drops the target, anger, and every memory Stope set, including a hold’s LOOK_TARGET. An order whose target dies, leaves the level, becomes vetoed, or is replaced by another target takes back the memories Stope set. Orders end early when the attack target dies, leaves the level, or becomes vetoed, when an avoided entity dies, or when a moved mob arrives (within 1.5 blocks horizontally and 2 vertically). A move to a point it cannot reach ends once a goal mob has walked as close as a path gets; a brain mob gets its walk target back at most every 10 ticks, and a goal mob on the ground with no path at all tries again after 10 ticks.
  • TargetVetos are checked when an attack is ordered and every tick it lasts.

A binding in JSON (consumers load their own files and pass objects to ReactionBinding.parse):

{ "on": "member_hurt", "do": "flee", "who": "adults", "radius": 12,
"when": { "herd_size_at_least": 3, "source_type": ["minecraft:player", "#minecraft:raiders"], "chance": 0.5 },
"cooldown_ticks": 40, "duration_ticks": 80,
"then": { "do": "follow_leader" } }
  • on: a trigger. Without it the binding is a standing behavior. do: a reaction. IDs without a namespace resolve to stope: first, then to the consumer’s namespace.
  • who: all (default), adults, babies, leader, followers, nearest (with count, default 1, nearest to the member that fired). radius: only members within this distance of that member.
  • when: herd_size_at_least, herd_size_at_most, source_type (ID, #tag, or list; fails when the trigger has no source), chance (0 to 1).
  • cooldown_ticks: the group ignores this binding for that long after it starts.
  • then: { "do": ..., parameters, "then": ... }, started with the same members when the step before it ends.
  • Any other key must be a parameter of the trigger or the reaction. Unknown keys, IDs, and bad values throw IllegalArgumentException naming them.
  • binding.without(id -> ...) returns the binding without the steps whose reaction the predicate accepts (reactions a player switched off), or null when none is left. A dropped first step hands the binding’s trigger, who, radius, when, and cooldown to the next.

Triggers (Triggers): member_hurt (source: attacker), member_killed (source: killer), target_acquired (source: the new target), threat_seen (threat matcher, required; range 1 to 64, default 16; polled on the group schedule by one member in turn, line of sight required, creative and spectator players ignored), losses (fraction 0.01 to 1, default 0.5; window_ticks, default 200; checked on each member death). Mods add triggers with Triggers.register and fire them with Triggers.fire.

Dispatch:

  • A trigger fires only for the group of the member it happened to. Each matching binding starts at most once per event, and never while it is already running for that group.
  • Hop limit: hits and kills by a mob that holds a Stope attack order, and target changes Stope made, fire no trigger. A reaction therefore never fires another group’s trigger.
  • Standing behaviors start on the group schedule while no triggered binding is running. A triggered binding stops them, and they start again on a later schedule.
  • A reaction that throws is logged and ended; the group carries on.

Primitive reactions:

ID Parameters (default) What members do
flee duration_ticks (100), speed (species panic speed; 0.1 to 4), direction (away or random) MobControl.alarm each on its own: away from the source, or with random short runs every which way.
avoid duration_ticks (100), distance (8), speed (1.2) MobControl.avoid the source. Nothing without a source.
defend duration_ticks (200), release (false) Adults attack the source. Nothing without a living source, or when the source is in the same group. When the run ends, the target stays for vanilla AI; with release the defenders are calmed (target and anger dropped). Use release for mobs whose vanilla AI never drops a target Stope gave them, such as llamas.
join_attack duration_ticks (200), release (false) All selected members attack the source, as defend without the adult limit.
follow_leader distance (6), speed (1.0) Standing: followers farther than distance walk to the leader. Nothing with LeaderChoice.NONE.

Writing a reaction:

Reactions.register(Ids.of("mymod", "gather"), Set.of("duration_ticks"), params -> {
int duration = params.getInt("duration_ticks", 60, 1, 600);
return ctx -> {
MobGroup g = ctx.group();
for (Mob m : ctx.members()) MobControl.moveTo(m, g.centerX(), g.centerY(), g.centerZ(), 1.2, duration);
return c -> c.age() < duration; // the Run: false ends it and starts `then`
};
});
  • In the client source set: SettingsScreen.builder(parent, "mymod.config."), then rows in order: toggle(key, value, onChange), intField(key, value, valid, onChange), decimalField(key, value, valid, onChange), section(key) for a heading. Then remoteNote(key), onSave(...), thenOnHostedServer(...), build().
  • Text comes from translation keys: <prefix>title, <prefix><key> for a label or heading, <prefix><key>.tooltip for a tooltip, <prefix><remote note key>.
  • Rows report each accepted change to onChange, so edit a working copy of your settings and save it in onSave. A number field that does not parse or fails valid turns red and disables Done.
  • Done runs onSave on the client thread, then thenOnHostedServer on the integrated server’s thread (via execute, between ticks) when this client hosts a world, then returns to parent. Cancel returns without either, without asking. On a server this client does not host, the remote note shows under the title.
  • Escape returns at once when nothing changed. With unsaved changes (including text typed into a number field that is not allowed yet) it opens UnsavedChangesScreen: Save (as Done; off while a field is invalid), Discard (as Cancel), and Keep Editing, which Escape also means. Keep Editing restores the screen with every field’s text and red marks. The prompt’s strings are Stope’s own (stope.settings.unsaved.*).
  • setFieldText, setToggle, hasUnsavedChanges, canSave, save, and cancel (and UnsavedChangesScreen’s canSave, save, discard, keepEditing) act as a player would, for automated client checks; Escape is keyPressed(InputConstants.KEY_ESCAPE, 0, 0).
  • Mod Menu: keep a modmenu entrypoint in your mod returning the screen. Stope suggests Mod Menu, so your mod does not.
  • Ship the guide in the jar (for example, processResources { from('docs/RULES.md') { into 'mymod' } }) and call install(MyMod.class, "/mymod/RULES.md", configDir.resolve("RULES.md"), "My Mod", version) during initialization.
  • Players get the guide without a leading `` developer comment or a trailing --- + <sub>Artifact ID footer, with a first line naming the mod and version. The file is rewritten only when that text differs, so a player’s edits are replaced on the next launch.
  • Failures are logged through Stope’s logger, never thrown.
private static final ConfigSpec.Builder B = ConfigSpec.builder();
public static final ConfigKey<Boolean> ENABLED = B.bool("enabled", true);
public static final ConfigKey<Integer> INTERVAL = B.intRange("updateInterval", 20, 1, 200);
public static final ConfigKey<Float> MULT = B.floatRange("wearMultiplier", 1.0f, 0f, Float.MAX_VALUE);
public static final ConfigKey<Boolean> FLEE = B.bool("reactions.flee", true); // nested object
public static final ConfigSpec SPEC = B.build();
public static final ConfigFile<Settings> FILE = ConfigFile.of("mymod", "mymod.json", SPEC, Settings::from);
  • FILE.load() during initialization reads <config>/mymod/mymod.json. A missing file is written with every default.
  • Strict JSON: comments, unquoted or single-quoted names, trailing commas, NaN, duplicate names, text after the value, or a top level that is not an object make the file malformed. One warning is logged, every key takes its default, and the file is left for its owner to fix.
  • Otherwise each key is read alone. Missing, wrong JSON type (a string or true for a number, a number for a boolean, a fraction for a whole number, null), or out of range: that key takes its default with a warning naming it (Config config/mymod/mymod.json: updateInterval = "fast" is not a whole number from 1 to 200; using the default 20). A name the spec does not define is logged and ignored.
  • The file is rewritten only when its sole problem is missing keys: they are added with their defaults (other values kept, keys in spec order) and one INFO line names them (Config config/mymod/mymod.json: added missing settings with their defaults: reactions.flee). A file with any wrong-typed, out-of-range, or unknown entry, or a malformed file, is never rewritten; its missing keys are logged as warnings.
  • define(name, codec, default, valid, expected) takes any Mojang Codec; its JSON is decoded with the same strict types.
  • Warnings go to the logger named by the mod ID (ConfigFile.at(path, ...): Stope’s logger).
  • Values are immutable. get() returns the view built by the view function; values() the ConfigValues. A settings screen edits values().with(key, value) (which throws on an invalid value; key.isValid(v) checks first) and calls save(edited), which writes the file and replaces the view. get() and values() work from any thread.
  • ConfigFile.folder(modId) is <config>/<modId>, for a player guide or rule files.
BlockChangedCallback.EVENT.register((level, pos, oldState, newState) -> { ... });
  • Fires on the server thread inside Level#setBlock, after the chunk holds the new state, when the state changed and is still in place at the end of the call (hook: ServerLevel#onBlockStateChange). Players, mobs, growth and spread, fluids, pistons, explosions, and commands all go through it, as do changes vanilla makes in reaction to another (a lamp lit by a neighbor), from inside the outer change.
  • Never fires for setting the same state again, for a state a neighbor update replaced within the same call, or for world generation.
  • A property change of the same block fires (oldState.getBlock() == newState.getBlock()).
  • pos may be mutable: call immutable() before keeping it. Runs for every block change; return fast and allocate nothing in the common case.
  • A listener that throws is logged once, with its stack trace, and the others still run.
  • Fires once per stroke while a player brushes a block: on use ticks 5, 15, 25, … of holding the brush, the ticks vanilla plays the brushing sound, at the start of BrushItem#onUseTick, before vanilla brushes the block. pos is the brushed block (immutable).
  • Never fires while the brush aims at air or an entity, or for a mob using a brush.
  • Server thread; exceptions as for BlockChangedCallback.
  • isTamed: a tamed TamableAnimal (wolf, cat, parrot), or a horse, donkey, mule, or llama with an owner. Never a camel: vanilla’s Camel#isTamed is always true, so a player’s camel shows through isSaddled or isRiddenByPlayer.
  • ownerId: the owner’s UUID for tamed animals and tamed horses and llamas, else empty.
  • isSaddled (pigs, striders, horses, camels), isRiddenByPlayer (any seat; a mob riding a mob does not count), isLeashed (held or tied to a fence), isInCaravan (a llama following another; the caravan’s head is not in it).
  • isKeptByPlayer: any of leashed, tamed, saddled, ridden by a player, or in a caravan. It does not look at names or tags; a mod adds its own rules (name tags, its own markers).
  • Allocation-free except ownerId’s Optional. Call on the thread that owns the mob’s level.
DataReloaders.registerFiles(id("herd_rules"), "herd_rules", "mymod/rules", files -> {
for (DataFile f : files) try (Reader r = f.openReader()) { ... }
});
  • Same timing, thread, and duplicate-ID rule as register.
  • Data packs: every .json file under data/<namespace>/herd_rules/, subfolders included. A file a later pack replaces (same namespace and path) is listed once, from the winning pack.
  • Config folder (<config>/mymod/rules, relative to the config directory; null for packs only): every regular .json file directly in it. A missing folder adds nothing; an absolute path or one leaving the config directory throws IllegalArgumentException.
  • Load order: data-pack files by pack in the server’s pack order (vanilla and mods first, the highest-priority pack last), then by path under the directory, then by namespace; then config-folder files by file name in character order (upper case first). Apply in list order when later files should override earlier ones.
  • Each DataFile: id() (<ns>:sub/cow for data/<ns>/herd_rules/sub/cow.json; <handler namespace>:config/cow_rules for Cow Rules.json in the config folder), source() (DATA_PACK or CONFIG_FOLDER), packId() (empty for config files), location() for log lines (<ns>:herd_rules/sub/cow.json, or config/mymod/rules/Cow Rules.json), and openReader() (UTF-8). Stope does not parse the files; the handler decides what to do with a broken one.
  • Registrar.of(modId) takes a loaded mod’s ID. Every entry it registers is modId:path. An invalid namespace throws IllegalArgumentException; a mod that is not loaded throws IllegalStateException.
  • block, item, entityType, blockEntityType, feature, and creativeTab each return a Registered<T>. Its id() is known at once. Its get() returns the value, and throws IllegalStateException until the loader has registered it.
  • Register during the mod’s initialization: Fabric’s onInitialize, or the Forge mod constructor (before Forge’s registry event).
  • Factories run at once on Fabric, in call order. On Forge they run during the registry event, in vanilla’s registry order. A factory may read entries from a registry that comes before its own (an item factory reading a block).
  • creativeTab(path, title, icon, contents) builds the tab with the loader’s own builder. contents is read each time the tab is rebuilt.
  • Registering an ID twice fails at startup. Not thread-safe: initialization only.
  • blockSetType(path) and woodType(path, setType) register at once on both loaders and return the type, named modId:path, with oak’s sounds. blockSetType(path, like) and woodType(path, setType, like) copy another type’s sounds (BlockSetType.CHERRY, WoodType.CHERRY). A name already registered throws IllegalStateException.
  • A wood type’s sign textures are modId:textures/entity/signs/<path>.png and modId:textures/entity/signs/hanging/<path>.png; its hanging sign edit screen shows modId:textures/gui/hanging_signs/<path>.png. Stope adds each wood type’s sign materials in client setup on both loaders.
  • Registrar.boats() registers modId:boat and modId:chest_boat and returns the mod’s BoatSet. One per mod; a second call throws IllegalStateException.
  • set.wood(path, planks) registers the items modId:<path>_boat and modId:<path>_chest_boat (vanilla BoatItems, stack of 1) and their dispenser behavior, and returns the BoatWood. A path already in the set throws IllegalStateException.
  • BoatKind (BOAT, CHEST_BOAT) names the two kinds; set.entityType(kind), wood.item(kind), and wood.texture(kind) give each kind’s entity type, item, and texture. boatType(), chestBoatType(), boat(), and chestBoat() are shorthands.
  • The boat carries its wood as synced entity data, saved under the NBT key stopeWood. A saved wood the set no longer has loads as the set’s first wood (set.firstWood()). The boat keeps the resolved wood until the synced ID changes, so drawing and ticking a boat do not parse its ID.
  • A broken boat drops its own item; pick block gives it; a crash drops the wood’s planks.
  • Textures: <namespace>:textures/entity/boat/<path>.png and <namespace>:textures/entity/chest_boat/<path>.png, laid out like vanilla’s. Stope registers the renderers and the model layers modId:boat#main and modId:chest_boat#main (vanilla’s boat and chest boat models) on both loaders.
  • Stope boats are boat-shaped only: no rafts. Names come from the consumer’s lang file: entity.<modId>.boat, entity.<modId>.chest_boat, and the item keys.
  • Recipes and the minecraft:boats / minecraft:chest_boats item tags are the consumer’s data.
  • BiomeFeatures.add(feature, biomeTag, step, enabled) adds a placed feature to every biome in the tag at that step. Call during mod initialization.
  • The placed and configured features are the mod’s data files. A key with no placed feature fails when the server starts.
  • enabled is read while each server start sets up its biomes, possibly once per biome, so a settings change takes effect at the next world load.
  • Fabric uses Fabric API’s BiomeModifications. Forge uses Stope’s biome modifier type stope:features, which Stope’s Forge jar enables with data/stope/forge/biome_modifier/features.json; consumers ship no Forge JSON.
  • Only adds; vanilla features stay.
  • addPool(table, pool, enabled) adds a pool built from pool each time loot tables load (server start, /reload), when enabled is true then. It applies to the table as loaded, a data pack’s replacement included. Fabric adds it to the table through LootTableEvents.MODIFY. Forge rolls it into the table’s loot through Stope’s global loot modifier stope:pools, which Stope’s Forge jar enables; consumers ship no Forge JSON.
  • Two Forge differences follow from that modifier:
    • It matches the table the game rolled, so a pool added to a table that another table reaches through a loot_table entry rolls on Fabric but not on Forge. Add pools to the table the game rolls (chest tables are rolled directly).
    • A data pack that replaces data/forge/loot_modifiers/global_loot_modifiers.json with "replace": true and leaves out stope:pools turns off every added pool on Forge.
  • replaceBrushed(table, chance, item, enabled) may replace the item a suspicious block of that archaeology table gives on its first brush stroke. Vanilla allows one item per archaeology table, so this replaces it. Additions for one table are tried in the order added; the first whose roll hits and whose item is not empty wins. Rolls use the level’s random. Same on both loaders: one common injection in BrushableBlockEntity.
  • A chance outside 0 to 1 throws IllegalArgumentException. Call during initialization.
  • Client only. Call from client initialization: Fabric’s onInitializeClient, or on Forge from the mod constructor when the dist is the client (Forge’s color events come before client setup). Suppliers may be Registered handles. On Forge, a call made after the moment it is applied (client setup, or the color events) throws IllegalStateException.
  • cutout, cutoutMipped, and translucent set a block’s chunk render layer. Fabric uses BlockRenderLayerMap; Forge uses ItemBlockRenderTypes.setRenderLayer in client setup, so consumers need no render_type in their model JSON. A render_type there still wins on Forge.
  • Leaves need no call: vanilla draws every LeavesBlock cutout-mipped (Fancy) or solid (Fast). Saplings, potted saplings, doors, and trapdoors need cutout.
  • blockColor(block, color) and itemColor(item, color) register color providers. Fabric uses ColorProviderRegistry; Forge uses RegisterColorHandlersEvent. Only model faces with a tintindex are tinted.
// Client initialization:
Colormap leaves = Colormap.of(Ids.of("mymod", "textures/colormap/leaves.png"), 0x48B518);
BlockRendering.blockColor(MyBlocks.LEAVES, (state, level, pos, tint) ->
pos == null ? leaves.color(0, 0) : leaves.color(pos.getX(), pos.getZ()));
  • Client only. Call of during client initialization: Fabric’s onInitializeClient, or on Forge the mod constructor when the dist is the client. A call after the client’s first resource load throws IllegalStateException.
  • The texture is read at every client resource reload (the first load, F3 + T, resource pack changes): a Fabric client reload listener, Forge’s RegisterClientReloadListenersEvent. Chunks rebuild after a reload, so block colors follow at once. Textures are decoded on the render thread in the reload’s apply stage, which costs little for a few small colormaps.
  • Block colors are baked into a chunk section when it is built. A color that changes over time (by the day, say) shows when the section is next rebuilt, as with any block color; Stope does not rebuild sections for it.
  • color(u, v) reads column u, row v; both wrap around the texture’s size, negative numbers included. Any size works; vanilla’s colormaps are 256 by 256.
  • Before the first load, and when the texture is missing or unreadable, color returns the fallback; a warning names the texture at each such reload.
  • of is safe during Forge’s parallel mod construction. color is safe from any thread, chunk-building threads included: each reload publishes a new pixel array. It does not allocate.
// Client initialization, after the tab is registered (or with its Registered handle):
CreativeTabIcons.texture(MyTabs.MAIN, Ids.of(MyMod.MOD_ID, "icon.png"));
  • Client only. Call from client initialization, as for BlockRendering. The tab may be a Registered handle; Stope reads it the first time the creative inventory draws a tab.
  • The creative inventory draws the whole texture into the 16 by 16 square where vanilla draws the tab’s icon item, whatever the image’s size; a square image reads best.
  • The tab keeps its icon item. Anything else that reads it (another mod’s tab list), and a creative screen a mod draws its own way, shows that item. Vanilla still draws that item’s count and durability bar over the texture, so give the tab an icon item with neither (a single, undamaged stack, as tab icons usually are).
  • One texture per tab. The same handle given twice throws IllegalStateException at once; two handles to one tab, or a handle that gives nothing, throw it the first time the creative inventory draws a tab, naming the tab and both textures. The handles cannot be read earlier: they are registry entries that do not exist during client initialization.
  • Same on both loaders: one client mixin on vanilla’s tab button drawing (CreativeModeInventoryScreen#renderTabButton), which Forge leaves unpatched.
  • flammable(block, igniteOdds, burnOdds), strippable(log, stripped), compostable(item, chance), and fuel(item, burnTicks) take suppliers, so a Registered handle works before its value exists.
  • Call them during mod initialization, after registering the entries. Stope applies them once every entry is registered: at once on Fabric, on the main thread in common setup on Forge.
  • Flammability, stripping, and composting write vanilla’s tables (FireBlock, AxeItem.STRIPPABLES, ComposterBlock.COMPOSTABLES), which Fabric API’s content registries and Forge also read. Fuel uses Fabric’s FuelRegistry and Forge’s burn-time event.
  • strippable takes pillar blocks: vanilla copies the log’s axis to the stripped block.
  • sign(block) adds a sign block to the vanilla block entity type its block entity uses: standing and wall signs to BlockEntityType.SIGN, hanging signs to BlockEntityType.HANGING_SIGN. Call it for all four sign blocks of a wood set; vanilla draws a block entity only when its type lists the block.
  • Items in vanilla’s fuel tags (logs, planks, wooden slabs, boats) already burn; fuel is for the rest.
  • Negative odds, a chance outside 0 to 1, or a burn time of 0 or less throw IllegalArgumentException. A later call for the same block or item replaces the earlier value. Not thread-safe: initialization only.
  • Ids.of(namespace, path), Ids.parse(text) (no namespace means minecraft), and Ids.tryParse(text). Invalid IDs throw IllegalArgumentException (or give empty). Pure; no game bootstrap needed.
  • RegistryLookups.entityType(id), block(id): empty when nothing is registered under the ID (a mod that is not installed). entityTypeId(type) is the reverse.
  • RegistryLookups.blockTag(tagId): the tag’s blocks as last loaded from data packs; empty when no pack defines the tag. Call on the server thread after startup or in a reload handler.
  • blockTagKey, entityTypeTagKey, biomeTagKey: tag keys for vanilla is(...) checks.

DebugScreenLinesCallback behavior (client)

Section titled “DebugScreenLinesCallback behavior (client)”
DebugScreenLinesCallback.EVENT.register((lines, targetedBlock) -> {
if (targetedBlock == null) return;
lines.add("");
lines.add("My mod: " + describe(targetedBlock));
});
  • In the client source set. Runs on the render thread every frame F3 is drawn, at the end of DebugScreenOverlay#getSystemInformation: the right column, after vanilla’s system lines and its targeted block, fluid, and entity lines. Lines are plain strings; resolve translations yourself (I18n.get). Append only.
  • targetedBlock is the block F3 reports as targeted (the camera’s block pick within 20 blocks), or null.
  • Stope skips every listener while reduced debug info is on (Minecraft#showOnlyReducedInfo: the reducedDebugInfo game rule or the client option) and outside a world. Vanilla hides its own targeted-block lines then, and a mod’s lines about the targeted block reveal the same kind of information, so the check lives in Stope and callers need not repeat it.
  • Every frame: return fast and throttle any server query. A listener that throws is logged once.
  • Register during mod initialization. The handler then runs once at server startup and once after every /reload, with the server data packs (datapacks and mods’ data/ folders).
  • On /reload it runs on the server thread between ticks. At startup it runs before the server starts ticking. Either way it never overlaps tick code, so it may replace state that tick code reads.
  • Order between handlers differs per loader: Forge runs them after vanilla’s reload listeners, in registration order, and ignores the IDs; Fabric API orders them by its own rules. Do not depend on one handler running before another.

Server events: ServerStartedCallback, DataPacksReloadedCallback, LevelTickCallback, CommandRegistrationCallback

Section titled “Server events: ServerStartedCallback, DataPacksReloadedCallback, LevelTickCallback, CommandRegistrationCallback”
ServerStartedCallback.EVENT.register(server -> MyRules.rebuild());
DataPacksReloadedCallback.EVENT.register(server -> MyRules.rebuild());
LevelTickCallback.EVENT.register(level -> MyData.get(level).maybePrune(level));
CommandRegistrationCallback.EVENT.register((dispatcher, buildContext) -> MyCommand.register(dispatcher));
  • All four fire on the logical server only (dedicated or integrated), from each loader’s own event: Fabric ServerLifecycleEvents.SERVER_STARTED, END_DATA_PACK_RELOAD, ServerTickEvents.END_WORLD_TICK, and command-api-v2’s CommandRegistrationCallback; Forge ServerStartedEvent, OnDatapackSyncEvent with no player, LevelTickEvent at phase END on a server level, and RegisterCommandsEvent.
  • ServerStartedCallback: once per server start, on the server thread, before any player joins. Tags are bound, so tag-based state can be built here.
  • DataPacksReloadedCallback: after a /reload (or a data pack enabled or disabled) that succeeded, on the server thread. Not at startup and not after a failed reload, on either loader: Fabric’s event also reports failures, and Stope drops them to match Forge. To build from tags at every load, listen to this and ServerStartedCallback.
  • LevelTickCallback: after every tick of every server level, on the server thread. A hot path: return fast, no allocation in the common case.
  • CommandRegistrationCallback: each time the server builds its command tree, at start and after every reload; register the commands every time. Only add to the dispatcher here.
  • A listener that throws is logged once and the others still run.
  • isModLoaded(id), modVersion(id) (the version the mod’s metadata declares, or empty), isDevelopment() (a Gradle dev or GameTest launch). Either side, any thread, from mod initialization on.

NetworkChannel and ClientNetworking behavior

Section titled “NetworkChannel and ClientNetworking behavior”
// Common init, both sides:
static final NetworkChannel NET = NetworkChannel.create(MyMod.id("main"), 1);
static final PacketType<Query> QUERY = NET.serverbound("query", Query::write, Query::read,
(query, player) -> answer(player, query)); // server thread
static final PacketType<Info> INFO = NET.clientbound("info", Info::write, Info::read);
// Server, to reply:
NET.sendToPlayer(player, INFO, info);
// Client init:
ClientNetworking.receive(INFO, info -> latest = info); // render thread
// Client, while connected:
if (ClientNetworking.canSend(QUERY)) ClientNetworking.send(QUERY, new Query(pos));
  • Create channels and declare packets during common initialization, the same packets in the same order on both sides. A packet’s ID is <channel namespace>:<channel path>/<name>. Duplicate channel IDs or packet names, invalid names, and negative versions throw.
  • Decoders run on a network thread; handlers run on the server thread (serverbound) or the render thread (clientbound). A decoder that throws, returns null, or leaves bytes unread drops that packet; a handler that throws is logged. Each packet logs its first failure of each kind only, so a client cannot flood the log.
  • Server handlers get the sending ServerPlayer and must validate every field: loaded position within reach, values in range, permission.
  • Sends to a side without the channel do nothing; canSend exists to skip building a message. A clientbound packet with no client handler attached is dropped.
  • Protocol version: a client that has the channel with a different version is refused at login. A client or server without the mod still connects. Fabric checks with a login query on <channel>/protocol and disconnects with stope.network.protocol_mismatch (channel, server version, client version); Forge checks with its channel versions (acceptMissingOr) and shows its own channel-mismatch screen.
  • canSend(player, clientbound): on Fabric true only once the client’s mod called ClientNetworking.receive for that packet; on Forge true whenever the client has the channel.
  • Wire format differs per loader (Fabric: one channel per packet; Forge: one SimpleChannel per channel with Stope’s packet index first). Both ends always run the same loader.

ClientDisconnectCallback behavior (client)

Section titled “ClientDisconnectCallback behavior (client)”
  • After the client leaves a server or single-player world, on the render thread. A loader may report one departure more than once. Fabric ClientPlayConnectionEvents.DISCONNECT, Forge ClientPlayerNetworkEvent.LoggingOut. Clear per-connection state; make the listener safe to run with nothing to clear.
ConfigScreens.register(MyMod.MOD_ID, parent -> MySettings.screen(parent));
  • Call once per mod during client initialization. The mod must be loaded; a second registration for the same mod throws.
  • Fabric: Stope’s own modmenu entry point provides the screen to Mod Menu (7.x reads provided screens on the first config-screen lookup, after every mod’s client init). The consumer needs no modmenu entry point and no Mod Menu dependency. Without Mod Menu, the call does nothing visible.
  • Forge: registered as the mod’s ConfigScreenHandler.ConfigScreenFactory, shown by the Config button of the Mods screen.
// Common GameTest source set:
public final class MyGameTests {
@GameTest
public static void placesBlock(GameTestHelper helper) { ... helper.succeed(); }
}
// Fabric (fabric-gametest entry point):
public final class MyFabricGameTests implements FabricGameTest {
@GameTestGenerator
public Collection<TestFunction> tests() { return GameTestFunctions.of(MyGameTests.class, MyMod.MOD_ID); }
}
// Forge:
@GameTestHolder(MyMod.MOD_ID)
public final class MyForgeGameTests {
@GameTestGenerator
public static Collection<TestFunction> tests() { return GameTestFunctions.of(MyGameTests.class, MyMod.MOD_ID); }
}
  • Every static @GameTest method becomes a test named <class>.<method>, lowercase; an instance method throws. Batch, timeout, setup ticks, rotation, required, and attempts carry over from the annotation.
  • A template without a namespace is looked up in the mod’s; a blank one means empty, so ship data/<modid>/structures/empty.nbt. Tests ported from Fabric’s EMPTY_STRUCTURE expect an 8x8x8 air structure; Stope’s own is that size.