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.
Example
Section titled “Example”PlayerFootfallCallback.EVENT.register((player, level, pos, state) -> { // TrailWright: add wear to the block at pos});PlayerFootfallCallback behavior
Section titled “PlayerFootfallCallback behavior”- Runs on the server thread at the end of
ServerPlayer#tick. posis 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.
PlayerMountFootfallCallback behavior
Section titled “PlayerMountFootfallCallback behavior”- Fires instead of
PlayerFootfallCallbackwhile the player rides aLivingEntity(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:
posis 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()andrider.isSprinting()describe the rider’s input, if a listener wants them.
MobFootfallCallback behavior
Section titled “MobFootfallCallback behavior”- Runs on the server thread at the end of
Mob#tick, for everyMob(animals, monsters, villagers). Players are not mobs; they have their own events. - Same footfall rules as the player event:
posis 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.
PlayerMountFootfallCallbackreports 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.
StopeSavedData.getOrCreate behavior
Section titled “StopeSavedData.getOrCreate behavior”- 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.
idbecomes the file name,data/<id>.datin 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.
MobHurtCallback behavior
Section titled “MobHurtCallback behavior”- Fires at the end of
LivingEntity#hurtwhen it returns true, for mobs on the server. Hits that do not land (invulnerability frames, cancelled damage) never fire. attackerisDamageSource#getEntity(): the shooter for arrows, null for falls, drowning, and other sourceless damage.amountis the value passed tohurt, before armor.- Runs inside the hurt call. Record state or give
MobControlorders, which apply next tick.
MobKilledCallback behavior
Section titled “MobKilledCallback behavior”- Fires after a mob dies (Fabric
ServerLivingEntityEvents.AFTER_DEATH), while it is still in the level. By then it has left its mob group.
MobTargetChangedCallback behavior
Section titled “MobTargetChangedCallback behavior”- Fires at the end of
Mob#setTargetwhen the target changed, including to and from null. - Brain mobs that keep their target only in brain memory (piglins, hoglins, goats) never
call
setTargetand never fire it.
AnimalBredCallback behavior
Section titled “AnimalBredCallback behavior”- 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. playeris the player who fedparent, else the one who fedpartner. Pairs nobody fed never fire it. Turtles, frogs, and sniffers spawn no baby at breeding and never fire it.
MobGroupChangedCallback behavior
Section titled “MobGroupChangedCallback behavior”- Fires on every join and every leave; exactly one of
leftandjoinedis 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(thenleft.isDissolved()is true). - For logging and bookkeeping. Do not change groups from a listener.
ReactionStartedCallback behavior
Section titled “ReactionStartedCallback behavior”- Fires after a reaction’s
startreturns without throwing: for triggered bindings, eachthenstep (with the binding’s trigger), and standing behaviors (triggernull). - For logging. Give no orders from a listener.
Mob groups
Section titled “Mob groups”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()));definereplaces every definition of that owner and dissolves the owner’s live groups; mobs regroup at their next refresh. Call it from aDataReloadershandler.- 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
defineagain moves an owner behind the others. - Each grouped mob checks membership once per
refreshIntervalticks, offset by entity ID. It leaves a group whose center is farther thanradius, or when it failsfilter. 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
MobGroupobject. groupOfis allocation-free. AMobGroupview is dead onceisDissolved().- Leaders:
OLDESTis the adult in the group longest;LARGESTthe 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, withinwidthof the line plus half its own width, and withinwidthabove or below its height. Allocation-free; it walks the group cells around the line.
MobControl behavior
Section titled “MobControl behavior”- 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 registersWALK_TARGET) are driven through memories from the end of their tick:WALK_TARGETfor movement,IS_PANICKINGwhile alarmed,AVOID_TARGETwhile avoiding, andATTACK_TARGET,ANGRY_AT, andRAM_TARGETfor attacks, each only when that brain registers it. alarmruns the mob to points away from the source (vanillaPanicGoal‘s short random runs without one) at the species’ vanilla panic speed, read from itsPanicGoalorAnimalPanic, or 1.25, or at thespeedgiven to the four-argumentalarm. 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 aboutheightblocks up, pushedpushblocks per tick away fromawayFrom(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.holdstops the mob where it stands and keeps it facinglookAt(if given) forticks. Brain mobs lose any walk target their idle behaviors set meanwhile and getLOOK_TARGET.- While a movement order is active, a goal mob’s own melee and bow attacks pause (Stope’s
movement goal takes the
MOVEandLOOKflags they need); the mob keeps its target and attacks again when the order ends. A held skeleton holds fire. hasMoveOrderis true while the mob has an alarm, avoid, move, or hold order that has not ended.isEngagedis true while the mob has an attack order, a live target of its own, or (brain mobs) anATTACK_TARGETmemory.moveStoppedShort(mob, withinTicks)is true when the mob’s lastmoveToended 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.attackholds 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_TARGETandANGRY_ATstay.calmdrops the target, anger, and every memory Stope set, including a hold’sLOOK_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.
Reactions
Section titled “Reactions”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 tostope:first, then to the consumer’s namespace.who:all(default),adults,babies,leader,followers,nearest(withcount, 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
IllegalArgumentExceptionnaming 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` };});SettingsScreen behavior (client)
Section titled “SettingsScreen behavior (client)”- 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. ThenremoteNote(key),onSave(...),thenOnHostedServer(...),build(). - Text comes from translation keys:
<prefix>title,<prefix><key>for a label or heading,<prefix><key>.tooltipfor a tooltip,<prefix><remote note key>. - Rows report each accepted change to
onChange, so edit a working copy of your settings and save it inonSave. A number field that does not parse or failsvalidturns red and disables Done. - Done runs
onSaveon the client thread, thenthenOnHostedServeron the integrated server’s thread (viaexecute, between ticks) when this client hosts a world, then returns toparent. 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, andcancel(andUnsavedChangesScreen’scanSave,save,discard,keepEditing) act as a player would, for automated client checks; Escape iskeyPressed(InputConstants.KEY_ESCAPE, 0, 0).- Mod Menu: keep a
modmenuentrypoint in your mod returning the screen. Stope suggests Mod Menu, so your mod does not.
PlayerGuide.install behavior
Section titled “PlayerGuide.install behavior”- Ship the guide in the jar (for example,
processResources { from('docs/RULES.md') { into 'mymod' } }) and callinstall(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 IDfooter, 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.
ConfigFile behavior
Section titled “ConfigFile behavior”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 objectpublic 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
truefor 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 MojangCodec; 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()theConfigValues. A settings screen editsvalues().with(key, value)(which throws on an invalid value;key.isValid(v)checks first) and callssave(edited), which writes the file and replaces the view.get()andvalues()work from any thread. ConfigFile.folder(modId)is<config>/<modId>, for a player guide or rule files.
BlockChangedCallback behavior
Section titled “BlockChangedCallback behavior”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()). posmay be mutable: callimmutable()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.
BrushStrokeCallback behavior
Section titled “BrushStrokeCallback behavior”- 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.posis 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.
MobOwnership behavior
Section titled “MobOwnership behavior”isTamed: a tamedTamableAnimal(wolf, cat, parrot), or a horse, donkey, mule, or llama with an owner. Never a camel: vanilla’sCamel#isTamedis always true, so a player’s camel shows throughisSaddledorisRiddenByPlayer.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’sOptional. Call on the thread that owns the mob’s level.
DataReloaders.registerFiles behavior
Section titled “DataReloaders.registerFiles behavior”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
.jsonfile underdata/<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.jsonfile directly in it. A missing folder adds nothing; an absolute path or one leaving the config directory throwsIllegalArgumentException. - 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/cowfordata/<ns>/herd_rules/sub/cow.json;<handler namespace>:config/cow_rulesforCow Rules.jsonin the config folder),source()(DATA_PACKorCONFIG_FOLDER),packId()(empty for config files),location()for log lines (<ns>:herd_rules/sub/cow.json, orconfig/mymod/rules/Cow Rules.json), andopenReader()(UTF-8). Stope does not parse the files; the handler decides what to do with a broken one.
Registrar and Registered behavior
Section titled “Registrar and Registered behavior”Registrar.of(modId)takes a loaded mod’s ID. Every entry it registers ismodId:path. An invalid namespace throwsIllegalArgumentException; a mod that is not loaded throwsIllegalStateException.block,item,entityType,blockEntityType,feature, andcreativeTabeach return aRegistered<T>. Itsid()is known at once. Itsget()returns the value, and throwsIllegalStateExceptionuntil 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.contentsis read each time the tab is rebuilt.- Registering an ID twice fails at startup. Not thread-safe: initialization only.
blockSetType(path)andwoodType(path, setType)register at once on both loaders and return the type, namedmodId:path, with oak’s sounds.blockSetType(path, like)andwoodType(path, setType, like)copy another type’s sounds (BlockSetType.CHERRY,WoodType.CHERRY). A name already registered throwsIllegalStateException.- A wood type’s sign textures are
modId:textures/entity/signs/<path>.pngandmodId:textures/entity/signs/hanging/<path>.png; its hanging sign edit screen showsmodId:textures/gui/hanging_signs/<path>.png. Stope adds each wood type’s sign materials in client setup on both loaders.
BoatSet and BoatWood behavior
Section titled “BoatSet and BoatWood behavior”Registrar.boats()registersmodId:boatandmodId:chest_boatand returns the mod’sBoatSet. One per mod; a second call throwsIllegalStateException.set.wood(path, planks)registers the itemsmodId:<path>_boatandmodId:<path>_chest_boat(vanillaBoatItems, stack of 1) and their dispenser behavior, and returns theBoatWood. A path already in the set throwsIllegalStateException.BoatKind(BOAT,CHEST_BOAT) names the two kinds;set.entityType(kind),wood.item(kind), andwood.texture(kind)give each kind’s entity type, item, and texture.boatType(),chestBoatType(),boat(), andchestBoat()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>.pngand<namespace>:textures/entity/chest_boat/<path>.png, laid out like vanilla’s. Stope registers the renderers and the model layersmodId:boat#mainandmodId: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_boatsitem tags are the consumer’s data.
BiomeFeatures behavior
Section titled “BiomeFeatures behavior”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.
enabledis 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 typestope:features, which Stope’s Forge jar enables withdata/stope/forge/biome_modifier/features.json; consumers ship no Forge JSON. - Only adds; vanilla features stay.
LootAdditions behavior
Section titled “LootAdditions behavior”addPool(table, pool, enabled)adds a pool built frompooleach time loot tables load (server start,/reload), whenenabledis true then. It applies to the table as loaded, a data pack’s replacement included. Fabric adds it to the table throughLootTableEvents.MODIFY. Forge rolls it into the table’s loot through Stope’s global loot modifierstope: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_tableentry 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.jsonwith"replace": trueand leaves outstope:poolsturns off every added pool on Forge.
- It matches the table the game rolled, so a pool added to a table that another table
reaches through a
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 whoseitemis not empty wins. Rolls use the level’s random. Same on both loaders: one common injection inBrushableBlockEntity.- A chance outside 0 to 1 throws
IllegalArgumentException. Call during initialization.
BlockRendering behavior
Section titled “BlockRendering behavior”- 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 beRegisteredhandles. On Forge, a call made after the moment it is applied (client setup, or the color events) throwsIllegalStateException. cutout,cutoutMipped, andtranslucentset a block’s chunk render layer. Fabric usesBlockRenderLayerMap; Forge usesItemBlockRenderTypes.setRenderLayerin client setup, so consumers need norender_typein their model JSON. Arender_typethere still wins on Forge.- Leaves need no call: vanilla draws every
LeavesBlockcutout-mipped (Fancy) or solid (Fast). Saplings, potted saplings, doors, and trapdoors needcutout. blockColor(block, color)anditemColor(item, color)register color providers. Fabric usesColorProviderRegistry; Forge usesRegisterColorHandlersEvent. Only model faces with atintindexare tinted.
Colormap behavior
Section titled “Colormap behavior”// 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
ofduring client initialization: Fabric’sonInitializeClient, or on Forge the mod constructor when the dist is the client. A call after the client’s first resource load throwsIllegalStateException. - 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 columnu, rowv; 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,
colorreturns the fallback; a warning names the texture at each such reload. ofis safe during Forge’s parallel mod construction.coloris safe from any thread, chunk-building threads included: each reload publishes a new pixel array. It does not allocate.
CreativeTabIcons behavior
Section titled “CreativeTabIcons behavior”// 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 aRegisteredhandle; 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
IllegalStateExceptionat 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.
BlockBehaviors behavior
Section titled “BlockBehaviors behavior”flammable(block, igniteOdds, burnOdds),strippable(log, stripped),compostable(item, chance), andfuel(item, burnTicks)take suppliers, so aRegisteredhandle 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’sFuelRegistryand Forge’s burn-time event. strippabletakes 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 toBlockEntityType.SIGN, hanging signs toBlockEntityType.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;
fuelis 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 and RegistryLookups behavior
Section titled “Ids and RegistryLookups behavior”Ids.of(namespace, path),Ids.parse(text)(no namespace meansminecraft), andIds.tryParse(text). Invalid IDs throwIllegalArgumentException(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 vanillais(...)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. targetedBlockis 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: thereducedDebugInfogame 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.
DataReloaders.register behavior
Section titled “DataReloaders.register behavior”- 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
/reloadit 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’sCommandRegistrationCallback; ForgeServerStartedEvent,OnDatapackSyncEventwith no player,LevelTickEventat phaseENDon a server level, andRegisterCommandsEvent. 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 andServerStartedCallback.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.
PlatformInfo behavior
Section titled “PlatformInfo behavior”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 threadstatic 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
ServerPlayerand must validate every field: loaded position within reach, values in range, permission. - Sends to a side without the channel do nothing;
canSendexists 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>/protocoland disconnects withstope.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 calledClientNetworking.receivefor that packet; on Forge true whenever the client has the channel.- Wire format differs per loader (Fabric: one channel per packet; Forge: one
SimpleChannelper 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, ForgeClientPlayerNetworkEvent.LoggingOut. Clear per-connection state; make the listener safe to run with nothing to clear.
ConfigScreens behavior (client)
Section titled “ConfigScreens behavior (client)”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
modmenuentry 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 nomodmenuentry 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.
GameTestFunctions behavior
Section titled “GameTestFunctions behavior”// 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
@GameTestmethod 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 shipdata/<modid>/structures/empty.nbt. Tests ported from Fabric’sEMPTY_STRUCTUREexpect an 8x8x8 air structure; Stope’s own is that size.