Project / what the port actually required
NeoForge 26.1.2 API notes
This SDK is further from mainline 1.21.x than the version numbers suggest, and
almost every entry below cost a compile error or a crash to find. Read it before
writing anything in neoforge-26.1.2/ that touches an unfamiliar
Minecraft system — particularly rendering, the GUI layer, saved data, or
anything posted from inside the chunk system. It is also the closest thing the
project has to a map of what a future version port will have to redo.
This SDK is further from mainline 1.21.x than the version numbers suggest.
tameWithNamedoes not fireAnimalTameEvent.AbstractHorse.tameWithNamesets the owner, sets tamed, fires the advancement trigger and broadcasts the hearts - that is all. The NeoForge event is fired by the caller:RunAroundLikeCrazyGoalcallsEventHooks.onAnimalTamefirst and tames only if nothing cancelled it. So taming a horse your own way and callingtameWithNamebare gives a horse that is tamed and owned and that no listener ever hears about. It has cost this project twice - hand-taming throughCrouchFeedGoal(gap 154), then the debug stick (2026-09-11), where a stick-tamed horse filled no Breeds row and looked like a breed-registry bug. Copy the goal:if (!EventHooks.onAnimalTame(horse, player)) horse.tameWithName(player);EntityLeaveLevelEventfires from inside the chunk system - never touch block state in its handler. It is posted fromServerLevel$EntityCallbacks.onTrackingEnd->PersistentEntitySectionManager .stopTracking, which runs insideDistanceManager.runAllUpdates. Anylevel.getBlockState(pos)/getChunkthere forces a chunk load that re-enters the same update pass and blows up withIllegalStateException: Entity is already tracked!(ChunkMap.addEntity). The owner hit this going to the horse dimension: teleporting out of the overworld unloads chunks, untracks every glowing horse in them, andGeneAbilityHandler.onEntityLeavewas callingclearLight(agetBlockState) synchronously. Fixed 2026-09-05:onEntityLeavenow just queues aPendingClear(dimension, pos)and aServerTickEvent.Postdrains the queue next tick (safe - not insiderunAllUpdates). Same family as theapplyTraitsToEntity/SCALEcrash the herd work fought (see the debug-pen section);server.executeis not a safe deferral here either (it drains next topollTask).ResourceLocationis nownet.minecraft.resources.Identifier- same surface (withDefaultNamespace,fromNamespaceAndPath), different name.- Horse classes moved
net.minecraft.world.entity.animal.horse->...animal.equine(Horse,Variant,Markings,AbstractHorse). HorseRendererisfinal.GeneticHorseRendererextendsAbstractHorseRenderer<Horse, HorseRenderState, HorseModel>and copies vanillaHorseRenderer's constructor body verbatim. Render-state stays the vanillaHorseRenderStateso the copied layers type-check;createRenderState()covariantly returns theGeneticHorseRenderStatesubclass and every instance really is that subclass.getTextureLocation(S)is still the texture hook - thesubmit()/SubmitNodeCollectorsplit didn't swallow it for entities.PlayerInteractEventitself is not cancellable - only its subclasses are.setCanceled/setCancellationResultare declared onRightClickBlock,RightClickItem,EntityInteractandEntityInteractSpecificindividually, so a sharedconsume(PlayerInteractEvent)helper does not compile and you write the four handlers out. Worth knowing which four: a right-click reaches exactly one of them depending on what the crosshair is over, and an item that handles onlyRightClickBlock+RightClickItemsilently does nothing when aimed at a mob (seeCustomHorseSpawnEggClient).- An access transformer on a client-only class is fine on a dedicated server.
AbstractMountInventoryScreenis@OnlyIn(Dist.CLIENT)and this mod'saccesstransformer.cfgopens itsmountfield;runServerboots clean, with no missing-target error or warning. The field is the only reliable answer to "which horse is this inventory screen showing" -player.getVehicle()is right only while you are riding, and shift-right-clicking a tamed horse opens the same screen without mounting. EventBusSubscriberdroppedbus()/Bus.IModBusEventsubtypes auto-route to the mod bus, everything else to the game bus.AttachmentType.Builder#serializetakes aMapCodec, notCodec.AttachmentType.builder(Function<IAttachmentHolder, T>)overload lets the default read the holder (e.g.entity.getUUID()).KeyMappingconflict-context ctor takes aKeyMapping.Category(a record keyed by anIdentifier), not a lang key. Debug keybind reusesKeyMapping.Category.MISC.EntityType#createneeds anEntitySpawnReason-create(level, EntitySpawnReason.COMMAND).DynamicTexture(NativeImage)is gone - useDynamicTexture(Supplier<String> label, NativeImage image); it takes ownership and closes the image (don't also close it).NativeImage.getPixel (x,y)/setPixel(x,y,argb)are ARGB (ARGB.fromABGR/toABGRunder the hood);getWidth()/getHeight().GeneticCoatTextureFactorybuilds a freshnew NativeImage(128,128,false),setPixels the composer'sint[]in, and hands it to theDynamicTexture.- Client reload listeners go through
AddClientReloadListenersEvent(mod bus, fired once whileMinecraftis constructed), not aRegisterClientReloadListenersEvent- that name does not exist here, and the server-side twin is the differently-prefixedAddServerReloadListenersEvent.event.addListener(Identifier, PreparableReloadListener); a mod listener sorts after every vanilla one unless you calladdDependency, which is usually what you want, since the vanillaTextureManagerhas finished reloading by then.PreparableReloadListener.reloaditself is now(SharedState, Executor taskExecutor, PreparationBarrier, Executor reloadExecutor)- the resource manager arrives insideSharedStateand the profiler arguments are gone - so implementResourceManagerReloadListenerand overrideonResourceManagerReload(ResourceManager)rather than writing the future by hand.CoatAssetReloadis the mod's only client listener. - Serverbound packets:
PacketDistributor.sendToServer->net.neoforged.neoforge.client.network.ClientPacketDistributor.sendToPlayer/sendToPlayersTrackingEntitystayed onPacketDistributor. - Reach the server via
((ServerLevel) player.level()).getServer()orplayer.level().getServer()(Level#getServer()exists;ServerPlayer#getServer()was the one that didn't resolve). dimension_typeJSON schema changed heavily. Gone:ultrawarm,natural,piglin_safe,respawn_anchor_works,bed_works,has_raids,fixed_time,effects. New required:has_ender_dragon_fight(bool).effects->skybox("none"/"overworld"/"end"). Time-of-day moved to a WorldClock/Timeline registry (permanent noon would needdefault_clock/timelines). A staledimension_typeJSON failsRegistryDataLoaderand takes down the client (ReportedException->Stopping!) the moment a world is created - reads as "singleplayer is broken".data/horsegenetics/dimension_type/debug_pens.jsonismin_y: 0, height/logical_height: 512(512 for random-Y plot headroom).- Flat-generator
dimension/debug_pens.jsonschema unchanged;"structure_overrides": []disables villages/etc. It's nowthe_voidbiome + a singleairlayer (generates nothing -DebugPenManagerbuilds the floor itself). Scoreboard.removePlayerFromTeam(name, team)THROWS when the entry is not on that team. It is written for/team leave, where membership is the precondition, and it raisesIllegalStateException: Player is either on another team or not on any teamrather than returning false. Ask first -scoreboard.getPlayersTeam(name) == team- or use the one-argumentremovePlayerFromTeam(String), which returns a boolean. The two-argument form crashed the server tick on 2026-09-12 (see coding notes); its siblingaddPlayerToTeamis the forgiving one, which is what makes the asymmetry easy to miss.- A world's spawn point is its respawn data, and a
ResourceKey's id isidentifier(). There is noLevel#getSharedSpawnPos()any more: the world spawn islevel.getLevelData().getRespawnData().pos()(LevelData.RespawnDatais a record of aGlobalPosplus yaw and pitch, withpos()as the convenience). AndResourceKey#location()is nowidentifier(), which bites anywhere a registry key is turned into a printable id -biome.unwrapKey().map(k -> k.identifier().toString()). Both found writing the test world's spawn-biome report (DebugTestWorldHandler). com.mojang.authlib.GameProfileis a record -profile.name()/profile.id(), nogetName().- Attributes are
Holder<Attribute>.LivingEntity#getAttributeValue( Holder<Attribute>)-> double;#getAttribute(Holder<Attribute>)->@Nullable AttributeInstancewithsetBaseValue(double).Attributes. MOVEMENT_SPEED/MAX_HEALTHare holders. Horses register both by default (createBaseHorseAttributes);WATER_MOVEMENT_EFFICIENCYis not on horses, so don't rely on it. - Ride/movement gates:
Entity#isLocalInstanceAuthoritative()(public final) is the "this side simulates the movement" check - true on the controlling client for a player-ridden mob, on the server otherwise.Entity#getControllingPassenger()->@Nullable LivingEntity.Mob#moveTo(...)was renamedsnapTo(...)(same overloads; y arg must bedouble). No water auto-dismount anywhere inEntity/LivingEntity/Player. - The GUI layer is retained-mode.
GuiGraphics->net.minecraft.client .gui.GuiGraphicsExtractor; screens/widgets overrideextractRenderState(GuiGraphicsExtractor, int mouseX, int mouseY, float)instead ofrender(...); textgraphics.text(font, comp, x, y, argb)/centeredText(...); rectsgraphics.fill(x0,y0,x1,y1,argb); texturesgraphics.blit(Identifier, x0,y0,x1,y1, u0,u1,v0,v1). Mouse:mouseClicked(MouseButtonEvent event, boolean doubleClick)(event.x()/.y()/.button()), same formouseReleased/mouseDragged. NeoForgeScreenEvent.Init.Post#addListenerand theScreenEvent.Render.*events (each#getGuiGraphics()) still bolt widgets/overlays onto vanilla screens.AbstractContainerScreen#getGuiLeft()/getGuiTop()are deprecated-for-removal but still work (used byHorseScreenHooks).ScreenEvent.Rendertiming:Prefires before the dimmed backdrop,Backgroundafter the backdrop but before the widget stratum,Postafter everything. To draw a panel behind your own buttons but over the backdrop, useBackground(that's whatHorseScreenHookslearned the hard way -Pregets painted over,Postcovers the buttons).- Swallowing a keybind while a text field is focused:
ScreenEvent.KeyPressed.Pre(getKeyCode()/getKeyEvent()), cancel it so the screen'skeyInventoryhandler doesn't close the GUI; forward the key to the box yourself for backspace/arrows.CharacterTypedis a separate event, so letters still type (HorseScreenHooks.onKeyPressed). - Scaling GUI text:
g.pose()is aMatrix3x2fStack-pushMatrix()/translate(x,y)/scale(s)/popMatrix()around ag.text(font, comp, 0, 0, argb)call scales it about(x,y)(FamilyTreeScreen.drawFitted).g.enableScissor(x0,y0,x1,y1)/disableScissor()clip everything includingg.entity(...). - A live 3D entity in a screen: build the render state yourself (
Minecraft.getEntityRenderDispatcher().getRenderer(e).createRenderState(e, 1f), thenstate.shadowPieces.clear(), pokeLivingEntityRenderStateangles) andg.entity(state, scale, translation, rot, camRot, x0,y0,x1,y1)- see
FamilyTreeScreen.drawHorseModel, which uses a throwaway client-onlyHorseand injects the coat ontoGeneticHorseRenderStatedirectly (noClientCoatCachepollution).
- see
SavedDatais gutted to adirtyflag - nosave(CompoundTag). Persistence isSavedDataType<T>(Identifier id, Supplier<T> ctor, Codec<T> codec)viaSavedDataStorage#computeIfAbsent(ServerLevel#getDataStorage()per level,MinecraftServer#getDataStorage()server-global - the ancestry DB uses the server-global one, so it lands in<save>/data/= per world).- Block registry:
DeferredRegister.createBlocks(modid)->.registerBlock(name, Function<Properties, ? extends B>, Supplier<Properties>)- third arg is aSupplier<Properties>, not a bareProperties->DeferredBlock<B>.ModBlocks.register(modEventBus)andModBlockEntities.register(modEventBus)are called from theHorseGeneticsconstructor. NoBlockItemforhay_portal(placed only by code). hay_portalblock entity + renderer:HayPortalBlock extends BaseEntityBlock,getRenderShape->RenderShape.INVISIBLE,newBlockEntity->HayPortalBlockEntity extends TheEndPortalBlockEntity(subclassed only soAbstractEndPortalRenderer'sT extends TheEndPortalBlockEntitybound is satisfied; its protected ctor is callable from a subclass).ModBlockEntitiesregisters the type vianew BlockEntityType<>(HayPortalBlockEntity::new, ModBlocks.HAY_PORTAL.get()).HayPortalRenderer extends AbstractEndPortalRenderer<HayPortalBlockEntity, HayPortalRenderState>for the plumbing only (facesToShowpopulation + BER registration) -submit(...)no longer touches the End-portal shader.HayPortalRenderState extends EndPortalRenderStateaddsaxis(from the block'sAXISstate), set in anextractRenderStateoverride.- Geometry:
AXIS(HORIZONTAL_AXIS) is the axis the portal plane runs along (vanilla nether convention). The slab is thin along the other axis (thinAxis= X<->Z swap), inset toSLAB_MIN..SLAB_MAX(0.25..0.75) there, and only the two faces withdir.getAxis() == thinAxisare drawn - so it faces the player, not edge-on. (Getting this backwards was the "portal is sideways" bug.)emitFace(dir, from, to, ...), windingright = up x normal, corners BL/BR/TR/TL. - Two passes per face: opaque black (
RenderTypes.entitySolid(TEXTURE), colour 0,0,0 -ENTITY_SOLIDhas no alpha discard + writes depth, so the sky / clouds / water can't show through) then the animated swirl (RenderTypes.entityTranslucentEmissive(TEXTURE, false), colour white, full-bright0x00F000F0,OverlayTexture.NO_OVERLAY). - Texture
assets/horsegenetics/textures/block/hay_portal.png- 64-wide vertical strip of 36 64x64 frames. Frame chosen by V offset;HayPortalClientAnim.currentFrame(36)advances it by wall-clock time at a speed that ramps 12 fps -> 48 fps as a client-estimated "charge" (local player standing in ahay_portalblock, ramp ~10 s / decay ~2 s) rises. The.png.mcmeta(36 frame entries) is not read - kept for docs / a possible future atlas move. HayPortalBlockEntity#shouldRenderFacerenders a face only if the neighbour isn't anotherHayPortalBlock(multi-block portal shows just its outer shell). Registered inClientSetup. Blockstate JSON: 2 axis variants -> one model that only sets aparticletexture.- To restore the End-portal starfield: put back
submitCube( state.facesToShow, RenderTypes.endPortal(), poseStack, submitNodeCollector)insubmit(git history).
- Geometry:
- The walk animation ignores
Attributes.SCALE, and that is a trap for any mod that changes it.LivingEntity.calculateEntityAnimationfeeds the raw world distance moved intoupdateWalkAnimation, which ismin(distance * 4, 1)intowalkAnimation.update(target, 0.4F, isBaby() ? 3.0F : 1.0F). That third argument is the only size compensation in the whole path - a foal's legs cycle 3x faster because they are short - and nothing anywhere consults the scale attribute.LivingEntityRendererthen scales the model bystate.scale(=entity.getScale(), the SCALE attribute alone;getAgeScale()is separate). So a scaled entity swings ordinary-rate legs over ordinary ground with longer limbs, and its feet slide. The fix needs no mixin: dividestate.walkAnimationPosin the renderer'sextractRenderStateaftersuper- the phase is a monotonic accumulator, so scaling it after the fact is identical to having accumulated it slower. LeavewalkAnimationSpeedalone; it is an amplitude multiplier on an angle, and an angle already scales with the model. SeeGeneticHorseRenderer.stretchGaitToSize. - Mojang un-typo'd
BlockBehaviour.Properties.noCollission()->noCollision(). - Spawn point: no
Level#getSharedSpawnPos(). Uselevel.getRespawnData().pos()(LevelData.RespawnDatarecord:globalPos()/yaw()/pitch()+ apos()convenience). - Cross-dimension teleport for any entity:
Entity#teleportTo(ServerLevel level, double x,y,z, Set<Relative>, float yRot, float xRot, boolean resetCamera)- pulled up toEntity.Set.of()target-types toSet<Relative>. Verified 2026-08-30 for non-player entities (horses come back through the return portal). - Leash:
Mob implements Leashable;isLeashed(),getLeashHolder()(->Entity),dropLeash()(drops the lead item),removeLeash()(no drop). Verified 2026-09-01: the roped-horse portal shortcut sends the horse through and drops the lead. - Event hooks used this pass:
LivingIncomingDamageEvent(cancelable, pre-mitigation - horse invulnerability);EntityTickEvent.Post(portal dwell timers, water riding, tamer tracking);PlayerInteractEvent.RightClickBlock(getPos(),getItemStack(),setCanceled+setCancellationResult(InteractionResult));PlayerEvent.PlayerChangedDimensionEvent(getFrom()/getTo()->ResourceKey<Level>),PlayerLoggedIn/OutEvent. PlayerInteractEvent.EntityInteractfires on both sides. For interactions that vanilla would otherwise turn into a mount (any item on a tamed horse), you mustsetCanceled(true)on the client too, or the client predicts the mount and rubber-bands. Do the state mutation server-only, cancel on both. (HorseInteractionHandlerclock/stick.)- Standing signs (not currently used - kept for reference):
Blocks.OAK_SIGN+StandingSignBlock.ROTATION(0-15, fromRotationSegment.convertToSegment(...)); text viaSignBlockEntity# getFrontText()->SignText#setMessage(line, Component)(returns a new one) ->setText(text, true)+setChanged()+sendBlockUpdated. - Wall signs, placed from code (the stall sign does this):
Blocks.OAK_WALL_SIGN.defaultBlockState().setValue(WallSignBlock.FACING, dir)(FACINGis a horizontalEnumProperty<Direction>),level.setBlock(pos, state, Block.UPDATE_ALL), then((SignBlockEntity) level.getBlockEntity(pos)) .updateText(t -> t.setMessage(line, Component), /*front=*/true)-updateTexttakes aUnaryOperator<SignText>andSignText.setMessagereturns a newSignText(chain the calls). BreakBlockEvent(net.neoforged.neoforge.event.level.block): fires before removal, soevent.getState()is still the block being broken;getLevel()->LevelAccessor,getPos(),getPlayer(). Cancelable.SavedDataType<T>(Identifier, Supplier<T>, Codec<T>)+server .getDataStorage().computeIfAbsent(TYPE)is the server-global SavedData pattern (HorseAncestryData,StallData).ResourceKey<Level>round-trips in a codec viaResourceKey.codec(Registries.DIMENSION).ServerLevel#sendParticles(ServerPlayer, T, boolean overrideLimiter, boolean alwaysRender, x, y, z, int count, dx, dy, dz, double speed)- the per-player overload;alwaysRender = truedefeats distance culling (used for the stall debug outline).- Villager trades are datapack registries now.
VillagerTrades.ItemListingis gone.minecraft:villager_trade(one JSON per offer:wants/gives/max_uses/xp/given_item_modifiers) andminecraft:trade_set(aHolderSetof trades plus anamountto draw) both live inRegistryDataLoader, so a mod ships them asdata/<ns>/villager_trade/**.jsonanddata/<ns>/trade_set/**.json.VillagerProfessiononly names fiveResourceKey<TradeSet>. The upshot for modders is large: there is no trade-generation code to write and no event to hook, and a "random item" trade is a loot function ingiven_item_modifiers, which runs when the offer is generated - i.e. per restock, not per purchase. See the horseman. VillagerProfessionis a built-in registry, its trades are a datapack one. The profession itself is arecordregistered throughDeferredRegister.create(Registries.VILLAGER_PROFESSION, ...)- vanilla fills it fromVillagerProfession.bootstrap- while theTradeSets it points at are JSON. Half in code, half in data, and the halves are not interchangeable.PoiTyperegistration isnew PoiType(Set<BlockState>, maxTickets, validRange)onRegistries.POINT_OF_INTEREST_TYPE(built-in). Getting the states:block.getStateDefinition().getPossibleStates(). A job site is only acquirable once it is in theminecraft:acquirable_job_sitePOI tag - and datapack tags merge, so adding to a vanilla tag is additive and safe, where overriding a vanilla worldgen file is not.- There is no data-driven way to append to a vanilla template pool, and no NeoForge
StructureModifierfor one (the stock modifiers only touch spawns and such). A datapack can only replace the wholetemplate_poolfile, which hard-codes today's vanilla contents into your mod and silently drops other mods' additions. The append is a runtime edit of the liveStructureTemplatePoolonServerAboutToStartEvent: add totemplates(the weight-expandedObjectArrayListthe generator draws from), replacerawTemplates(what the codec would re-serialise and what other mods read), and reset the lazily cachedmaxSize. Make it idempotent - a single-player session that quits to title and loads again reuses the same frozen pool objects, so a blind append stacks a second copy every load. - The first access transformer in this repo lives at
neoforge-26.1.2/src/main/resources/META-INF/accesstransformer.cfg. ModDevGradle 2.x auto-detects that exact path in any main-sourceset resource dir - nobuild.gradlechange and nomods.tomlentry.publicwidens;public-falso stripsfinal, which is what an assignable field needs. Five lines: three onStructureTemplatePoolfor the pool append above, one onAbstractHorse.generateSpeed(protected static, so unreachable without extending it) soHorseSpeedFloorcan ask the game for its own slowest horse instead of writing0.1125into prose, and one onVillager.increaseMerchantCareerfor the dev-only clock shortcut that promotes a horseman a tier. That last one is worth its own note, because the alternative looks available and is not -setVillagerData(data.withLevel(n))is public and raises the number, but re-rolling the trades to match needsupdateTrades(protected) or a null offer list (offersis protected too, andgetOffersonly rebuilds when it is null). So a "levelled" villager keeps offering his old tier's trades, which is worse than not levelling him. Nothing in the shipped gameplay path calls it. - A biome modifier can be code, not only data. The stock
neoforge:add_spawnstakes a fixedHolderSet<Biome>; a modifier whose biome list is decided at runtime (every biome a loaded breed names) is a record implementingBiomeModifier-modify(Holder<Biome>, Phase, ModifiableBiomeInfo.BiomeInfo.Builder), act only inPhase.ADD, andbuilder.getMobSpawnSettings().addSpawn(MobCategory, weight, new MobSpawnSettings.SpawnerData(type, min, max))(the weight is separate fromSpawnerDatain 26.1.2) - whoseMapCodecis registered in aDeferredRegisteronNeoForgeRegistries.Keys.BIOME_MODIFIER_SERIALIZERS, then named by"type"in adata/<mod>/neoforge/biome_modifier/*.json.Biome.LIST_CODECreads a biome list field. Modifiers run as a server starts, so anything loaded in the mod constructor is available. Seeworld/BreedHerdsBiomeModifier. - Opening a folder in the OS file browser is
net.minecraft.util.Util.getPlatform().openPath(Path)- what the resource-pack screen's “Open pack folder” button calls.FMLPaths.GAMEDIRis.minecraft(or the instance folder), and the same on client and integrated server. The Breeds tab's button uses both. - Day or night is
Level.isBrightOutside()/isDarkOutside(). - A structure template can carry entities - it is how vanilla puts villagers in
village/plains/villagers/*.nbt- andSinglePoolElementexplicitly setssetIgnoreEntities(false), so jigsaw pieces place them. The entity NBT needs only{pos, blockPos, nbt:{id}}; nothing callsfinalizeSpawn, so anything rolled has to be rolled by the mod on the entity's first tick. That is how the cowboy is generated. - Jigsaw block NBT keys (for hand-baking a structure):
name,target,pool,joint(aligned/rollable),final_state,selection_priority,placement_priority. In a plains village the road surface is a piece'sy=0and itsstreetconnectors sit aty=1, so a building that wants its floor level with the street puts its connector aty=1too. Entity#moveTois nowEntity#snapTo(all overloads, includingsnapTo(BlockPos, yRot, xRot), which uses the block's bottom centre).Player#displayClientMessageis gone. The action bar isServerPlayer#sendSystemMessage(Component, boolean overlay)- server-side only, so branch oninstanceof ServerPlayer.AbstractVillagerhas nocreateAttributes()- that is onVillager. A custom merchant builds fromMob.createMobAttributes().AbstractVillager.getOffers()assignsthis.offersbefore callingupdateTrades, so anupdateTradesthat callsgetOffers()does not recurse - that is the vanilla pattern, not a trick.AttachmentType.Builder#sync(StreamCodec<? super RegistryFriendlyByteBuf, T>)syncs an attachment to tracking clients. Worth it whenever the client predicts something the attachment would forbid: right-clicking a tameable horse is client-predicted, so a server-only "this horse is not yours" check makes the player climb on and get snapped back off.- A goal that holds
Flag.MOVEat priority 0 starves every other movement goal, which is how a mob that is ridden by another mob gets steered - vanilla only hands control to a saddled mount's player passenger, so a mounted mob is otherwise dead weight. Do not also holdFlag.JUMP: that is the horse's ownFloatGoal, i.e. the thing that stops it drowning. DoorBlock#setOpen(Entity, Level, BlockState, BlockPos, boolean)updates both halves, so only ever call it on theDoubleBlockHalf.LOWERstate. There is still no "the other leaf of this double door" lookup in the SDK, but there is a reliable rule for writing one, andCowboyDoorGoal.partnerOfis it: the neighbour to either side along the wall that is a wooden door of the sameFACINGand the oppositeHINGE. Check both perpendicular sides rather than deriving the side from the hinge - the hinge says which way a leaf swings, not which side its partner is on, and getting that backwards fails silently as a door that opens half way. Sweeping every door in a box works too and is what the deletedCowboyDoorsdid, but it opens doors the mob was not going through.DoorInteractGoal#setOpenonly ever touches its owndoorPos, so a double door needs the partner leaf swung by hand. Note also thatDoorInteractGoal.canUsein this SDK does not consultcanOpenDoors- it gates onmob.horizontalCollisionplus the path - so the goal and the navigator flag are two independent switches that the caller has to keep in step.OpenDoorGoalis not usable as-is either: constructed withcloseDoorAfter=falseitscanContinueToUseis false immediately, so it stops on the next tick and its unconditionalstop()closes the door it just opened.PathNavigation#setCanOpenDoorsgates the pathfinder, not the mob. It forwards to theNodeEvaluator, which is what decides whether a shut wooden door scores as passable or as wall. So a mob with a door-opening goal and this left off never plans a route through the door in the first place, and a mob with this on and no goal walks into the door and shoves it for ever. Both halves or neither.DirtPathBlockwill not stay under a solid block -canSurviveis false when the block aboveisSolid()(a stair counts), andupdateShapethen schedules a tick that turns it to plain dirt. Structure placement runsBlock.updateFromNeighbourShapesover the blocks it wrote, so this fires on generation: a baked structure that laysdirt_pathunder its own steps is laying dirt. Nothing warns you, and the NBT still saysdirt_path.- A jigsaw block never reaches the world.
SinglePoolElement#getSettingsaddsJigsawReplacementProcessorunlesskeepJigsaws, and processors run before blocks are written - sofinal_state(defaulting tominecraft:air) is what gets placed, and the jigsaw is never briefly solid. That is what makes it safe to sit one directly above a block that cares what is over it. - The layer of a piece that carries its jigsaw is the layer that rests on the ground.
JigsawPlacementlands the child's jigsaw block at the parent connector's own world Y (targetBoxY = sourceBoxY + sourceJigsawLocalY - targetJigsawLocalYfor a rigid pair, orgetFirstFreeHeight - targetJigsawLocalYagainst aterrain_matchingstreet). A plains street's connector sits one above its road block, so a piece whose jigsaw is in its foundation course sits one proud of the road, which is the vanilla house shape. The useful consequence: to put a block at road level, add a layer below the jigsaw's layer and lift everything else - the buildings do not move, because the jigsaw moved up with them, and the new bottom layer lands in the road's own course. The terrain beard is unaffected: it referencesbox.minY() + groundLevelDelta, and the lift changes both by one in opposite directions. - Loot functions register as the
MapCodecitself onRegistries.LOOT_FUNCTION_TYPE. ExtendLootItemConditionalFunction, build the codec withcommonFields(i).and(...), and note thatcodec()must returnMapCodec<? extends LootItemConditionalFunction>, not...LootItemFunction. - A
ModConfig.Type.SERVERfile lives in the instance'sconfig/, not in the world. It writesconfig/<modid>-server.tomllike a common config would, and each world'sserverconfig/folder is an optional override of that path (its ownreadme.txtsays so) rather than the place the file is created. So "edit the server config" means the instance file unless someone has deliberately put a per-world copy beside it. What still makesSERVERthe right type is who wins: the server's copy is the authority and clients on it follow, which is what a setting that moves a hitbox needs (body.size). - A config's file name is resolved with a plain
Path.resolve, so it can leaveconfig/.ConfigTracker.openConfig(FML 11.0.15) resolves the name against the config directory (or the world'sserverconfig/for a SERVER override) andsetupConfigFilecreates missing parents, so a name of../phc/server.tomlputs the file in.minecraft/phc/. All three of this mod's files use it (ModBreedSpecs.configFile). Seen working on a dedicated server, the file watcher included: an edit torun/phc/breed-spawning.tomlwas re-read live. Two consequences: a SERVER config's per-world override is then<world>/phc/server.toml, and the name must be a constant, because a SERVER config is synced to clients by file name. defineInListwith an immutable list crashes at startup. Writing a fresh file, NeoForge probes a missing value withacceptableValues.contains(null), andList.of(...).contains(null)throws aNullPointerException- a mod-loading failure, seen. Pass a mutable list.