Every release, in order.
Conduit follows SemVer with a +mcXX.X.X suffix marking the Minecraft build each release compiled against. While in 0.x, the minor number is the breaking-change signal.
- 0.26.0 +mc26.2 2026-09-03
- Fixed
The entity freeze never froze a mob.
RewindEntityTickMixincancelledEntity.tick(), whichMob.tick()overrides and moves after the super call. It cancelsServerLevel.tickNonPassengernow, the one call per entity per tick. - Fixed
Fast-forward looked broken. The server ran at 4× (measured 79.9 ticks/s on a dedicated server) while the vanilla client keeps ticking at 20/s, so only the player did not speed up. See Added for both halves of the fix.
- Fixed
A speed picked on the panel never reached play. The panel freezes the game before its speed row exists and every resume path reset the speed to 1×.
- Fixed
Inventories were up to ten seconds stale on
commitandpreviewAt. Keyframes are written the tick an inventory changes (InventorySnapshot.matches), sharing unchanged snapshots by reference. - Fixed
Every click on the panel's timeline teleported the host away from the panel.
- Fixed
A frozen player jittered in mid-air, falling and being anchored back every tick.
- Fixed
A rewound chest came back empty, a sign blank. The journal held block states only.
- Fixed
A game's speed buff survived every rewind. Attribute modifiers were not in the pose.
- Added
GameHost.rewind(RewindConfig). The host arms after the arena is built and stops before it comes down, on every match;r.recording()exists byonRoundStart; the match's phase, clock, roster and arena size rewind with the world; a player rewound to before their elimination leaves spectating. That isPAIRED-RULESrow 42, enforced. - Added
The freeze is a freeze. Entities, scheduled block and fluid ticks, random ticks, weather, block entities, every player interaction and the held-item drop are held in a paused level; bodies are anchored with gravity off. Everyone is frozen except the operator: a director (spectating) walks freely and is never restored; an operator who is in the round is frozen with everyone and the panel follows them.
- Added
Speed is a choice per recording (
RewindController.setSpeedreturnsnullor a refusal reason), applied while live and on resume, refused while a person is outside the recorded level. Above 1× a vanilla client's walkers are scaled by aMOVEMENT_SPEEDmodifier; the Conduit client now lifts the 20/s cap (FastForwardTickMixin) and runs at the server's rate, with no modifier. - Added
Block-entity data in the journal and the tape (
BlockEntityNbt,RewindBlockEntityChangeMixin): chests, signs and furnaces rewind with their contents, seeded every capture tick. - Added
Attribute modifiers in the pose (
ModifierSnapshot): a game's own modifiers (not theminecraftnamespace) rewind and persist in tapes. - Added
The memory cap compares against a count (
Recording.memoryBytes), entity NBT measured, shared inventory snapshots counted once; entity state keyframes are staggered per track;/conduit rewind statusshows memory and capture cost. - Added
Opt-in tape retention:
rewind_tape_max_countandrewind_tape_max_mb, both0(never prune) by default. - Added
Module master docs for core, render, rewind and fx; a rewind roadmap; 63 new game tests.
- Changed
RewindController.setSpeedreturnsString(the refusal reason) instead ofvoid. - Changed
RewindController.holdOperator(Recording, ServerPlayer)returns whether the operator is in the scene; previews never move a director. - Changed
BlockJournal.EditVisitorandBlockEditcarry block-entity NBT before and after. - Changed
PlayerPosecarriesmodifiers;EntityLedger.captureFrametakes the keyframe interval and staggers per track. - Changed
rewind_auto_recordis still unread (roadmap Q1, pending).
- Fixed
- 0.25.0 +mc26.2 2026-08-29
- Fixed
A player given back on the death screen lost their entire inventory (D1).
onInstanceReleasedforce-converts aDEFERREDrecord andreleaseapplies the restores; vanilla builds a freshServerPlayeron respawn andrestoreFromdoes not carry the inventory across unless the player is a spectator. Four subjects measured: SURVIVAL lost, ADVENTURE lost, SPECTATOR kept, control kept.DEFERREDexists precisely so a restore never lands on a corpse, and the release path defeated it. Only the stash waits now; gamemode and respawn point survive vanilla's respawn and still go back immediately. - Fixed
Every reconnect overwrote the captured way home (D2).
onReconnectre-captured throughhomeFor, four lines under a javadoc saying it never reads the returning body. The JOIN hook fires from insideplaceNewPlayerbefore the player is in the list (inPlayerList=false, 7/7 joins), sohomeOfread a null body and fell through tofallbackHome()every time. The archived home now stands, and the re-capture is the planted defect it should always have been. - Fixed
The custody diagnostic reported the opposite of the truth (D3).
statusprinted the live body under a heading that reads as the engine's record, showinggamemode=ADVENTURE respawn=conduit:rt-1 forced=truewhile custody held SURVIVAL and an overworld point. Filed independently by both archetypes' auditors. - Added
Custody.identityOf/destinationOf, andPlayerCustody.heldIdentity/heldDestination. Without them no diagnostic could read the record at all, which is why D3 could not be fixed at the command layer.statusnow prints HELD, HOME and BODY as three labelled lines. - Added
Game tests for all three, each proven against a known-bad control:
CustodyDeathScreenStashGameTest(dying vs live at release),CustodyConsoleCommandGameTest(held vs body). 17 game tests per line now. - Changed
CustodyInterleavingTest's world gives each player their own hub spot. Every player shared one string, which made "kept the captured home" and "re-captured, then fell back to the generic hub spawn" the same value, soREOPEN_DESTINATIONhad no witness. It is now caught by[TAKE, TAKE, TAKE, DISCONNECT, RECONNECT, GIVE]— the D2 scenario exactly.
- Fixed
- 0.24.0 +mc26.2 2026-08-29
- Fixed
The engine-wide leave discard destroyed a stash custody was holding, and every mid-round disconnect saved the round's inventory over the player's own.
ConduitCoreModregistersPlayerInventoryStash::discardon the leave fan-out;PlayerCustodyrestores the stash from a separate listener on the same FabricDISCONNECTevent. Whichever registered first won, and module init order shuffles between boots of the same jar, so the outcome was undefined rather than merely wrong. When the discard won,restoreStash'sisStashedguard read false and did nothing. Round 28 SINGLETON measured the loss on 8 of 8 subjects, with players still online at teardown as the control.The fix is not a reordering, because designing around a registration position is what PAIRED-RULES 27 and 28 exist to forbid.
discardnow asks a question instead of racing:PlayerInventoryStash.addRetentionPolicylets an owner claim a player's stash,discardskips a claimed player in either order, and the owner releases it throughdiscardRetained.PlayerCustodyclaims for the life of every custody record and releases fromCustody.end, the one point every record passes through.This is the 2026-08-26 round 1 finding, which said to fix the engine before the doc or the doc would bless a data-loss bug. It was never fixed, and custody was then built on top of it. Three warning blocks that told readers to "register your leave handler early enough to run before the discard" are corrected: that advice was never sound, for the same reason the bug was undefined.
- Fixed
Custody had no diagnostic surface a person could reach.
/conduit admin custodyopened every one of its six executors withServerPlayer p = src.getPlayer(); if (p == null) return 0;and the file contained zerosendFailurecalls, so from the console or RCON it did nothing and said nothing.TESTING.mdnamed it as the instrument for driving custody by hand, and round 28 reported Phase 2 blocked on both archetypes because a headless agent has no client. At the same timePlayerCustodyandCustodycontained no logger and no log call, while that same page said "conduit-arena logs every custody transition".Every move now takes an optional player, so
/conduit admin custody status <player>works from a playerless source, and the bare form says what to type instead of returning 0 into the void. Custody logs underconduit.Custody:take,giveBack,giveBackAll, disconnect and rejoin at INFO, per-effect detail at DEBUG.TESTING.mdnow states the levels rather than overclaiming. - Added
CustodyConsoleCommandGameTest, two arms: the targeted form must answer a playerless source with an actual status, and the bare form must refuse out loud. Content-asserted, not presence-asserted, because a refusal is also output and a test that accepted any line would pass on it. Proven by restoring the silentreturn 0and watching only the bare arm fail. - Added
Custody is documented where builders read. Round 28's convergent finding was that
grep -ci custodyreturned 0 on both archetype guides,MAKING-MODS.md,README.mdandwiki/Home.md, so every page a builder is sent to still taught the hand-rolled teardown. Section 8 of each guide now carries the two calls and the migration table, section 10 carries the checklist lines, and the three entry docs point at them.New content, not just relocation: how custody composes with
GatherRoom, which no document stated. Settled against source.takebeforegather; leave the stash on; do not also callroom.disperse, because custody queues its own return anddispersequeues a second one toPrior, so two returns drain for one player; keeproom.close(), which is the force-load ticket and not the return. That retires the dead-playerPENDING_HOMEbranch entirely.Section 2's reference mod on each page is bannered, not rewritten. It stays compiled and current for everything except teardown, and the rewrite waits for a round to verify it.
- Added
Doc pair
custody-consumer-apisotakeandgiveBackcannot change shape without failing the build until those pages are re-read. - Added
Doc pair
custody-diagnostics, 16 pairs now.checkEngineDocPairsTargetingrejected the first attempt becauseTESTING.mdnever namedCustodyCommand, which was correct: re-reading a page that does not mention the class cannot reveal a change to it. - Added
CustodyStashRetentionGameTest, two arms on a real server, differing in one variable: custody holds the player or it does not. Held survives the discard, unheld is still discarded. Proven against a known-bad control by restoring the original unconditionaldiscardand watching the held arm fail by name while the control arm kept passing. - Added
Doc pair
stash-retention, so the two halves of this behaviour can no longer drift apart silently. 14 pairs now. - Changed
CustodyEffects.dropStash, a default no-op, called fromCustody.end. Deliberately not a plantedDefect: it was one, andeveryPlantedDefectIsCaughtreported it undetected, because no ordering in the enumeration ends a record with a retention outstanding. Shipping it as a control would have shipped a check that cannot fail. The call stays for the one path the model has no event for,NOT-SOLVED.md#2. - Changed
CustodyInterleavingTest's world now modelsrestoreStashfaithfully: the binding no-ops when the body is gone, and modelling it as always succeeding hid that case. No existing ordering changed behaviour.
- Fixed
- 0.23.0 +mc26.2 2026-08-29
- Fixed
A queued teleport into a released level reported success. Every document in this repo said that case fired nothing, which is a leak: silent and survivable. A game test written to stage it measured the opposite —
afterTeleportfired, so the caller was told the player reached home while the body sat in a level that had been closed and whose region files were deleted. Every consumer ends ownership on that callback. Both outcomes were reachable because the case was unspecified:executeTeleportasked four questions about the player and none about the destination, andDESTINATION_GONEwas raised only inside the chunk gate thatNO_PRELOADskips. It now checks destination identity against the server's level map for every teleport, gated or not. Identity rather than null, because a key can be re-registered and arriving in a stranger's level is the same defect wearing a live object. - Fixed
The 1.21.11 artifacts could never have loaded on 1.21.11. All eleven
fabric.mod.jsonfiles hardcoded"minecraft": "~26.2"and"java": ">=25", and five mixin configs hardcodedcompatibilityLevel: JAVA_25, which Mixin cannot set on a Java 21 JRE. Both are expanded per target now.assemblehad been green throughout, because nothing ever ran that jar. - Added
Player custody.
me.zlex.conduit.custody.Custody(core, no Minecraft types, the state machine) andPlayerCustody(arena, the calls). A game writestake(player, instance)andgiveBack(player)and nothing else: no leave handler, no in-flight set, no eviction on disconnect, instance release or shutdown, and no ordering rule for the spectator release. Supersedes theSendHomereference class inPAIRED-RULES.md, which is kept deliberately becausecheckReferenceModsCompileandcheckReferenceModBehaviourrun it. Migration table at the top of that section; the honest gap list is indocs/ownership-lifecycle/NOT-SOLVED.md. - Added
/conduit admin custody—status,take,wreck,give,release,drop. Drives every exit path by hand on a dev server, printing gamemode, respawn and dimension before and after.runServeris re-enabled onconduit-arenaas the host. Seedocs/ownership-lifecycle/TESTING.md. - Added
Two Minecraft targets from one source tree. Stonecutter merged: 26.2 and 1.21.11, each with its own published coordinate and its own Java toolchain.
- Added
Game tests. The engine had none. Eight now, on each target, against a real headless server, plus an exhaustive interleaving suite over the custody state machine: 11,390,625 orderings in about four seconds, with eighteen historical defects planted as controls that it must catch.
- Changed
CI runs
check. It previously ranassembleand published; the unit tests and the documentation checks were enforced by whoever remembered to run them. - Changed
Publishing no longer fails on an unchanged version. GitHub Packages answers a PUT over an existing Maven version with a 409, which had made every push to main red since 2026-08-24. It now probes first and skips with a notice.
- Changed
VERSIONS.mddescribes both game lines, deriving the target table from themc_<line>_fabric_apiproperties so adding a line is one property edit. - Fixed
Documented
SafeTeleport.whenChunksReady. Three public overloads, previously named in no document but this changelog — including the 6-arg one made public in round 25. Also records the two behaviours that differ fromteleport:onReadyfires anyway on gate timeout (so it means "we waited", not "the chunks are here"), and either callback can fire inline. - Fixed
CONDUIT.mdsaidSafeTeleporthad fourteleportoverloads; it has five. The missing one is the 8-arg form carryingonDropped— the API limit 4 collapses into. - Fixed
SafeTeleport: a teleport still waiting on chunks when the server stops now reportsSERVER_STOPPING, notDESTINATION_GONE. The two cases were silent before round 23, inverted by round 23's fix, and inverted the other way by round 24's: a gate parked onminecraft:the_nether— a level that is never released — reportedDESTINATION_GONEat shutdown with the nether demonstrably alive. EachChunkGatenow carries its own shutdown handler. The public 5-argwhenChunksReadystill routes both into one callback because it is given only one; use the new public 6-arg overload to tell them apart. - Fixed
Two
SafeTeleportjavadoc sentences that the round-24 fix had falsified. The 8-argteleportdoc claimed a gate-held teleport fires nothing at shutdown; it fires. - Added
DropReason.DESTINATION_GONEis now documented. It shipped in round 24 and appeared in no document at all — all three enumerations listed five of six members and one described them as "all four". - Added
conduit-combat — new module. A custom, animation-agnostic melee engine (the "Better Combat" replacement).
MeleeWeapondeclares reach, sweep arc, attack cooldown, combo window, and aMeleeSwingsequence;MeleeCombat.install(WeaponResolver)interceptsAttackEntityCallback, returnsFAILfor resolved weapons (owning combat), runs a forward cone-sweep, deals each combo swing's damage viahurtServer(i-frames cleared so a fast string lands), applies player-safe knockback, plays feedback, and fires aMeleeHitper target viaonHit(the seam animations attach to). AnyLivingEntitycan fight:MeleeCombat.performSwing(level, attacker, weapon)runs the identical pipeline for AI opponents / scripted bosses / training bots (whiffs are audible, cooldown-gated), andcomboState(uuid)exposes the combo cursor for AI observations and HUDs. Depends on conduit-core; server-side only. First consumer: the Blade Clash katana; first non-player consumer: the AI Fighters gym bridge. - Added
conduit-world ·
GameplayRules.weatherCycle. A sixth toggle (default true); when false the enforcer freezesADVANCE_WEATHERand forces clear weather, and whendaylightCycleis false it now also pins the world to day — driving the vanillatime/weathercommands (26.2's time/weather setters are reworked, so the command path is version-stable). Record gains a field (positional-ctor break for the two internal consumers; Codec back-compatible). - Added
conduit-world ·
GameplayRules.daylightCycle. A fifth toggle (default true); when false the enforcer freezes theADVANCE_TIMEgamerule so a filming world stays at a fixed time of day. NewapplyWorldRulesapplies spawning + daylight together on server start. (Record gains a field — a positional constructor break for the two internal consumers;Codecstays back-compatible.) - Added
conduit-instance ·
RuntimeDimensionSpec.superflatWorld. A one-call factory for a superflat runtime dimension (overworld dimension type, classic flat ground) besidevoidWorld, wired toVoidWorldHelper.createSuperflatGenerator. A mod spins one up + teleports players in, so "everything is flat" doesn't depend on how the host generated their main world (which can't be reflattened after creation). Persist it for a single reusable arena across restarts. - Added
conduit-world · superflat generator.
VoidWorldHelper.createSuperflatGeneratorbuilds a classic bedrock/dirt/grass-over-PLAINSFlatLevelSource(plus an overload taking a caller-chosen surface biome +FlatLayerInfostack) — the flat, open surface prop rigs and Modplex creation worlds want, sitting right beside the existing void generator. Structures off — the structure overrides are an explicit empty set (notOptional.empty(), which would fall back to all default structures), so no villages/strongholds/etc. interrupt the flat surface. - Added
conduit-world ·
Regions.railedLane. Stamps a single-file lane flanked by a rail on each side (left rail anchored, right rail pushed out bywallGap) and returns each lane slot's centreBlockPosso the caller spawns whatever it likes down the middle. The reusable geometry behind mob gauntlets / runways; stays block-agnostic like the rest ofRegions. - Added
conduit-world ·
GameplayRules+GameplayRulesEnforcer. An enforced server-wide toggle bag —naturalSpawns(SPAWN_MOBS gamerule),invincible(players take no damage),noHunger(food kept full),instakill(a player's hit one-shots any mob) — with aCodecand a one-callinstall(Supplier<…>)that wires the Fabric hooks once and reads live rules per event. Deliberately mirrors the Modplex runtime rule catalog (player.invincible,difficulty.hunger_rate,spawns.disable, player-instakill) so a mod built on this maps 1:1 onto a Modplex pack. - Changed
Ported to Minecraft 26.2 (
+mc26.2). Artifacts now compile against MC26.2, Fabric Loader0.19.3, and Fabric API0.154.2+26.2; every module'sfabric.mod.jsonnow dependsminecraft ~26.2. Mapping deltas fixed during the port (kept here + inCONTRIBUTING.mdfor the next MC bump):- Per-colour block constants collapsed into
ColorCollectionaccessors —Blocks.LIME_GLAZED_TERRACOTTA→Blocks.GLAZED_TERRACOTTA.lime(),Blocks.GRAY_CONCRETE→Blocks.CONCRETE.gray(),Blocks.LIGHT_BLUE_STAINED_GLASS→Blocks.STAINED_GLASS.lightBlue(). - Copper collapsed into
WeatheringCopperCollection—Blocks.COPPER_BLOCK→Blocks.COPPER_BLOCK.weathering().unaffected(). - Entity-type constants moved
EntityType.*→EntityTypes.*(BLOCK_DISPLAY,ITEM_DISPLAY,TEXT_DISPLAY). GameRenderer.getMainCamera()→mainCamera().EntityType.create(ValueInput, Level, EntitySpawnReason)→create(ValueInput, Level, EntitySpawnRequest)(wrap:new EntitySpawnRequest(reason, false)).
- Per-colour block constants collapsed into
- Changed
A rewind restore no longer loads chunks on the tick thread — it waits for them.
BlockJournal.restoreToSeqreplays the block diff throughLevel.setBlock, and asetBlockinto a non-resident chunk is the samemanagedBlockpark as a coldgetBlockState— per diffed chunk. The rewind panel's seek bar reaches it on every drag notch, and rewind lives in instance dimensions whose chunks are prime unload candidates once players are back in the hub. Every restore now derives the diff's chunk set up front (BlockJournal.pendingDiffChunks); when everything is resident it applies on the calling tick exactly as before, otherwise the chunks are requested through the non-blocking chunk entry point and the restore fires the tick they arrive. Slider targets that pile up while waiting coalesce to the latest — never interleave — and commit/cancel/resume hold their un-freeze andcommitAttruncation behind the same gate, so play never resumes into un-restored geometry and no diff is truncated before it lands. A restore is must-happen work: after 15 s the apply proceeds with blocking loads and a warning rather than ever dropping writes.Consumer-visible: a scrub against a cold instance can now lag the slider by the chunk-arrival time instead of freezing the whole server for it;
RewindController.isRestorePending(r)reports a restore still in flight. - Changed
ArenaMarkers.floodFloorgains a non-blocking overload; the synchronous one is documented as blocking-when-cold. The fill probes every cell withgetBlockState, so crossing into a non-resident chunk parked the server thread per cold chunk, up tomaxCellstimes. The newfloodFloor(level, marker, maxCells, done)parks frontier cells at cold chunk borders, requests those chunks without blocking, and resumes as they arrive — completing on the calling tick when the region is warm, and delivering the identical set either way: truncating a floor at a cold border would change arena bounds with chunk-cache weather, which is worse than any stall. A chunk still absent after 20 passes is read blocking with a warning (exactness outranks the stall). The synchronous overload is kept for provably-warm callers — arena wiring immediately after the paste that loaded the region — and its javadoc now says exactly that. - Changed
MobSpawns.spawnAroundno longer loads chunks, and can place fewer mobs than asked. Resolving a spawn column meant reading blocks in it, andLevel.getBlockStateon a chunk that isn't resident isChunkSource.getChunk(…, FULL, create = true)underneath — the server thread parked inmanagedBlockuntil the chunk is read off disk or generated, on the order of 140 ms a column on virgin terrain. Every caller is a tick handler:MobWaveruns from theMobWavesEND_SERVER_TICKsweep, and Cataclysm's Blood Moon callsspawnAroundstraight from its event tick, four picks per player every two seconds.spawnYnow checkshasChunkAtbefore it reads anything and returns a newNOT_LOADEDverdict;spawnAroundresponds by re-picking elsewhere in the annulus, up to 8 fresh draws per mob, and skipping that mob if every draw lands on unloaded terrain. One check covers the whole column — the vertical foothold search and the heightmap fallback never leave the same chunk.This is deliberately not the
SafeTeleport.whenChunksReadytreatment. A teleport's destination is not optional, so waiting for it is right. A scatter spawn's destination is one of infinitely many random picks, and a mob placed where nothing is loaded is a mob nobody can see in a chunk that unloads again immediately — waiting would buy nothing a player experiences and would cost every caller its synchronous return value.Consumer-visible: the returned list has always been the honest count of what was placed, and there is now one more reason for it to be short. Callers that assumed
spawnAround(…, n, …).size() == nnever had that guarantee (the cramped-tunnel case predates this) but are now much more likely to notice.spawnAt,executeandexecuteAllread no blocks and are unchanged.New:
/conduit mob spawnprobe <count> <minRadius> <maxRadius>(op-gated,conduit.instance.debug) reports what a scatter spawn costs the tick it runs on, alongside placed-vs-requested. Aim the annulus past any loaded region —spawnprobe 8 400 700— to measure the cold case the everyday one hides. - Changed
Debris.rainno longer loads chunks either, and can drop fewer blocks than asked.FallingBlockEntity.fallis a hidden block write — it clears the source position withLevel.setBlockbefore adding the entity, andLevel.setBlockgoes throughgetChunkAt.rainschedules every block throughScheduler.runLater, so that write lands on a tick task, at a polar offset up toMAX_RADIUS(128 blocks, 8 chunks) and an absolutespawnHeightunrelated to any player.spawnOnenow skips a block whose column isn't loaded. The check is at spawn time rather than at pick time deliberately: theSPREAD_TICKSstagger puts up to two seconds between the two, and a chunk can unload inside that window.
- Fixed
- 0.22.0 +mc26.1.2 2026-08-19
- Changed
Structure finding became arithmetic instead of search, and there is now one mechanism instead of two.
VillageSearch's heuristic is deleted;StructureScan/StructureLocatoranswer the same question withStructure.findValidGenerationPoint, which is whatStructureCheck.canCreateStructureitself calls, so the verdict is exact by construction rather than probabilistic. 0.21.0's three-height biome pre-filter (y=64/128/192, accept on any) was strictly a heuristic: biomes are 3D, so a column whose surface sits at y=70 need not have its real biome among those three samples, and a valid candidate could in principle be dropped. The locator also walks cells in true distance order rather than vanilla's ring perimeters, which returns a nearer village on many seeds. - Changed
BREAKING:
me.zlex.conduit.lobby.VillageSearchis deleted. Useme.zlex.conduit.world.structure.StructureLocator, which is generic over a structureTagKey.WorldSetup.findVillage/cancelVillageSearch/VILLAGE_SEARCH_RINGSare unchanged and now delegate to it, so consumers need only the version bump. - Changed
/conduit admin instance village <key> [cells] [vanilla|blind]keeps the A/B lever, now switching cell order rather than the deleted pre-filter. - Added
StructureScan+StructureLocator(me.zlex.conduit.world.structure) — structure finding as arithmetic instead of as search, generic over a structureTagKeyrather than hardcoded to villages.The village search was slow because it was searching. It walked a grid asking
StructureManager.checkStructurePresenceper cell — which scans the region files from disk on every probe — and then generated a chunk for every probe that survived. On a fresh instance: 810 probes, 31 chunk requests, 6,468 ms of server-thread time, worst tick 153.9 ms, and no village after 30 seconds. Vanilla/locatewas worse: one tick, past 60 seconds, watchdog kill.None of that work was necessary. Structure placement is a pure function of the world seed, in three stages of strictly increasing cost:
1.
RandomSpreadStructurePlacement.getPotentialStructureChunkis integer maths over aLegacyRandomSource. One candidate perspacing-wide grid cell, and the mapping is a bijection — the candidate derived from a cell always floor-divides back to that cell, because the spread offset is bounded byspacing - separation— so enumerating cells enumerates every candidate in a region with none missed and none repeated. 2. The frequency reducer and the exclusion zone are also pure arithmetic. 3.Structure.findValidGenerationPointis noise maths: one terrain column and oneMultiNoiseBiomeSourcesample. No chunk, no disk, no chunk system.Stage 3 is the whole answer, not a shortlist to confirm later.
StructureCheck.canCreateStructure— the predicate behindcheckStructurePresence'sCHUNK_LOAD_NEEDED— is literallyfindValidGenerationPoint(ctx).isPresent(), andChunkGenerator.createStructuresgates on the same call when a chunk really generates. Vanilla's chunk load ingetStructureGeneratingAtexists only to re-read the start and bump its explorer-map reference count; the start'sChunkPosis the candidate chunk stage 1 already computed.StructureScanalso walks cells in true distance order rather than vanilla's square ring perimeters, so the first hit is the nearest hit and the walk stops on the next cell whose distance lower bound already exceeds it. Ring order is not distance order — a corner of ring 1 is further than an edge of ring 2 — so vanilla can return a village that is not the nearest. On 16 test seeds the two orders disagreed on 9, and the distance-ordered walk was nearer every time.Measured over 12 seeds at radius 6 cells: 4-16 candidates, 86 ms of CPU on average (23-167 ms), 0 chunks generated, 0 region-file reads, and a village found on every one — against 6,468 ms and a failure. On the specific seed the old search lost on it visits 4 cells, spends 34 ms and returns a village 452 blocks out. Sliced into 2 ms tick budgets, 86 ms is about 43 ticks of barely-measurable work. Per-candidate the split is
getNoiseBiome8 us andgetFirstFreeHeight2,077 us — the biome test is free, the terrain column that decides which biome to test is the entire cost, which is why the early-out matters more than any micro-optimisation of the check.Verified headlessly against two independent ground truths on every candidate:
ChunkGenerator.createStructures(weighted variant draw included, run to an assembledStructureStart— what the game actually places) andStructureCheck.checkStartagainst a storage reporting every chunk absent (what/locateasks per probe on a fresh instance). 784 candidates over 16 seeds, 0 mismatches against either.minecraft:ruined_portal— a different structure set with different placement behaviour — agrees exactly on 200/200, so the genericity is tested rather than asserted.StructureScanhas no Conduit imports, no Fabric imports and noServerLevel; itsContextrecord is the guarantee that nothing reachable from it can load a chunk. That is also what lets thestructure-findertool (~/yt/structure-finder) compile the identical file and render the algorithm over a real biome map for any seed, without the engine and the picture being able to drift apart.StructureLocatoris the server-side driver: budgeted, resumable, cached per(seed, tag, radius), never blocking, never completing exceptionally. The per-tick budget is kept even though it barely matters now — cheap in the common case is not a reason to let a large radius on a rare structure spike a tick.StructureScanlanded additive, beside a still-workingVillageSearch. The swap and the deletion are theChangedentries above, in this same release, so the two mechanisms never coexisted in a shipped version.
- Changed
- 0.21.0 +mc26.1.2 2026-08-19
- Fixed
Village start never found a village.
VillageSearch0.19.0 fixed the 60-second tick hang by slicing the walk, but the slices were the wrong unit: each one was a fullStructureManager.checkStructurePresence, and for a village — aJigsawStructure— that assembles the entire village piece layout before the biome test is applied (findValidGenerationPointisfindGenerationPoint(ctx).filter(Structure::isValidBiome)). At ~8 ms a probe the 2 ms per-tick budget collapsed into "one probe per tick", so 30 seconds of searching bought about five rings and timed out with nothing. Measured on a live server:601 tick(s), 810 step(s), 31 chunk request(s), 6468ms of server-thread time, worst tick 153895us: none.The search now runs the biome test first, from
BiomeSource.getNoiseBiome— climate-noise maths, no chunk, no jigsaw — and only pays the structure check for cells that pass it. It cannot lose a village: every cell it drops is one the expensive call would have filtered out at the end anyway. It also stops generating chunks entirely:CHUNK_LOAD_NEEDEDis returned on exactly one path inStructureCheck.checkStart—canCreateStructurecame back true — which is the same verdictChunkGenerator.createStructureswill reach on the same seed and chunk, so loading the chunk to STRUCTURE_STARTS only re-derives an answer already in hand. Default range widened from 6 to 8 rings (±4,352 blocks) now that a rejected cell costs a noise sample.WorldSetup.findVillageis unchanged. - Added
/conduit admin instance village <key> [rings] [blind], the hit-rate harness. "It finds a village" is a claim about a rate, not about one world, and nobody is going to sit through a dozen seeds in a lobby GUI. This runs the engine's real search headlessly, bypasses the seed cache so a repeat is a real repeat, and prints the instrumented line: probes split into biome-filtered and structure-checked, where the milliseconds went, worst tick, worst probe, wall clock, ticks elapsed.blinddisables the biome pre-filter, and it is how the filter gets verified rather than trusted: filtered and blind must reach the same answer, and over 16 seeds they do. The same escape hatch exists in code asVillageSearch.find(level, rings, biomePrefilter, useCache).
- Fixed
- 0.20.0 +mc26.1.2 2026-08-19
- Fixed
The village walk stopped loading chunks on the tick thread. Slicing it across ticks in 0.19.0 was not enough. Measured on 26.1.2 with a tick-by-tick harness:
ServerChunkCache.getChunkFuturemanagedBlocks the server thread despite its name, and one coldSTRUCTURE_STARTSrequest cost 1,183 ms inside a single tick; oneStructureManager.checkStructurePresencecosts 3-13 ms, and the village tag holds five variants, so a whole cell was a 25-40 ms lump.The walk is now built out of vanilla's own pieces rather than driven through
ChunkGenerator.findNearestMapStructure.checkStructurePresenceruns per structure as placement and biome maths with no chunk:START_NOT_PRESENTis free, andSTART_PRESENT(already-generated terrain) is a hit with no chunk load at all. OnlyCHUNK_LOAD_NEEDEDneeds a chunk, and it goes through a newServerChunkCacheAccessor@InvokerontogetChunkFutureMainThread, the private method underneathgetChunkFuturewithout themanagedBlock, with up to 16 requests concurrent on the worldgen workers. A request made in the same tick its ticket is added answers "Unloaded chunk", so a chunk is re-asked up to 20 times before being written off. One unit of work is now onecheckStructurePresencerather than one cell, so the 2 ms budget can stop anywhere.It mirrors
getStructureGeneratingAtexactly, including reading the locate position fromStructureStart.getChunkPoson the chunk path, and was verified against vanilla: both agree on village(-144, -832)for seed12345.Measured on the Pad Party dev world, a structure-poor world with no village in range, which is the worst case and the one that crashed:
result before, radius 25, blocking 14,067 ms in ONE tick; RCON dead for 14.3 s before, radius 100, as shipped 60 s, watchdog kill (the crash report) after, 6 rings / 169 cells / 507 steps / 169 chunk requests 50 ticks, 127 ms of server-thread time total, worst tick 13.5 ms, 20 TPS throughout On a normal overworld the village is found at ring 1 in ~2.3 s of wall clock (worst tick 18 ms), and a repeat on the same seed answers from cache in 2 ms without touching a tick.
- Fixed
A runtime dimension now generates on the seed it was asked for.
WorldTemplate.overworld(seed)had been dropping the seed for as long as runtime dimensions have existed: askingInstanceManagerfor seed999produced a level reportinglevelSeed=-567870618070133791, the host world's, and so did asking for424242. The lobby World tab's Fixed/Random seed control was inert in every game that uses it.The seed was being passed, just to the wrong thing. The long argument of
new ServerLevel(...)is the biome zoom seed, handed straight toBiomeManager; the seed worldgen actually runs on is read back out of the level byChunkMap's constructor vialevel.getSeed(), which is hard-wired to the server's own world.RuntimeLevelSeedstherefore records the seed under the dimension key before the level is constructed (ChunkMapasks during construction, and theLevelsuperclass has stored the key by then), andServerLevelSeedMixinanswersgetSeed()from it. A level Conduit did not create is not in the map and keeps vanilla behaviour untouched. The biome-zoom argument is nowBiomeManager.obfuscateSeed(seed)as vanilla passes, so an instance on seed S matches a vanilla world on seed S instead of being one hash off it. - Fixed
An overworld instance is an overworld, not a copy of the host world.
WorldTemplate.Overworldbuilt its generator fromoverworld.getChunkSource().getGenerator(), whatever the server booted from. That is only an overworld on a server whose own world happens to be one. Pad Party's dev world is a superflat with no layers, which is the normal way to host a minigame server, so every "overworld" instance came out as empty void: no terrain, no biomes past the fixed one, and nothing for the World tab's village-start option to find. The seed control looked broken partly because the world it seeded had nothing in it to vary.OverworldTerrainbuilds the vanilla definition instead (multi-noise biome source over the overworld noise settings). A game that genuinely wants the host's terrain rules still hasRuntimeDimensionSpec.copyOf. - Fixed
SafeTeleportnever blocks a tick to load its destination.preloadChunkswas a loop ofgetChunk(..., FULL, true), which parks the server thread inmanagedBlockuntil each chunk is generated, and every game calls it at every round start over a 3x3 block of virgin chunks in a brand-new instance world. Measured on a fresh overworld instance: 1,239 ms inside one tick, worst tick gap 1,243 ms, with the whole server stopping dead at the exact moment players are dropped into the arena.whenChunksReadyrequests the same chunks through the non-blocking chunk entry point and re-checks once a tick, then runs its callback on the server thread;teleport()holds the pending teleport behind that gate rather than preloading at enqueue time. The arrival guarantee is stronger than it was: chunks used to be loaded at enqueue and merely assumed still resident a tick later. - Added
/conduit admin instancegives the instance system a headless hand. Everything the instance system does, it did from a GUI: the host picks a seed in the lobby's World tab and presses Start. That made the two things most worth checking untestable from a console, namely whether the seed asked for is the seed you got, and how long the tick stops for at round start./conduit admin instance list /conduit admin instance new <void|flat|overworld> [seed] /conduit admin instance probe <key> seeds + a terrain fingerprint /conduit admin instance spawnprep <key> [r] the round-start step, timed /conduit admin instance release <key>probeis the seed check, and it takes a terrain fingerprint rather than trusting the reported seed, because a world can echo a seed it never generated with. Drivable over RCON, so instances can be exercised without a client. - Added
ChunkSweep, and a hub builder allowed to finish on a later tick. Pad Party's hub build opens by sweeping 625 chunks for marker blocks withlevel.getChunk, one blocking disk read each: 356 ms in a single tick on an idle boot, and 3,252 ms on a boot where the runtime-dimension restore ran first and took the chunk workers with it.ChunkSweepis that scan shape done properly: keep up to 32 requests in flight through the non-blocking chunk entry point, visit whatever has arrived, stop when the tick's 2 ms budget is spent. Loading happens on the chunk workers in parallel and the tick thread only pays for visiting. Both the visitor and the completion callback run on the server thread, so a sweep may write blocks, which is the point for a marker scan that erases what it finds.A sweep spans ticks, so a hub builder that starts with one cannot have finished when it returns.
HubManager.CustomHubBuildergains an explicit done signal and holds back the post-build work (roomBuilt, the pad-overlap self-check,GameRegistry.markHubBuilt) until it fires. The oldBiConsumeroverload still works and completes immediately.
- Fixed
- 0.19.0 +mc26.1.2 2026-08-18
- Fixed
The village search stopped blocking the tick. 0.18.0's
WorldSetup.findVillageranChunkGenerator.findNearestMapStructureinline on the server thread at radius 100. Radius there is not chunks: reading 26.1.2's bytecode, it walks rings of structure-spacing cells (34 chunks for villages), so 100 meant up to 40,401 probes across ±54,400 blocks, each able to generate aSTRUCTURE_STARTSchunk throughmanagedBlock. In a brand-new instance that measured 60 seconds inside a single tick: the dedicated-server watchdog declared the server crashed and force-killed it, and singleplayer, with no watchdog, simply froze.VillageSearchwalks the same cell grid in vanilla's own ring order, but sliced across ticks at ~2 ms of probing per tick (always at least one cell, so progress is certain), one cell at a time probed with a radius-0findNearestMapStructurewhose origin is that cell, which is the finest granularity the public API allows. Worst-case tick cost is therefore one chunk'sSTRUCTURE_STARTSgeneration. Bounded to 6 rings (about ±3,264 blocks) instead of 100, cached per(levelSeed, maxRings)since village placement is a pure function of the seed, and timed out (empty on unload or exception) so the caller falls back to world spawn rather than ever waiting.The slicing stays on the server thread on purpose. The chunk system is not safe for arbitrary worker access and
StructureCheck's caches are plain fastutil maps that worldgen mutates, so moving the walk to a background thread trades a hang for a race.skipExistingChunks=trueis not the cheap first pass it sounds like. It is the explorer-map flag and does the opposite: ingetStructureGeneratingAta true value skips the early return onSTART_PRESENTand forceslevel.getChunk(STRUCTURE_STARTS)even for chunks already on disk.false, used here, is the cheap path. - Fixed
BREAKING:
WorldSetup.findVillagenow returnsCompletableFuture<Optional<BlockPos>>, andVILLAGE_SEARCH_RADIUSis nowVILLAGE_SEARCH_RINGS.
- Fixed
- 0.18.0 +mc26.1.2 2026-08-18
- Added
The lobby World tab.
LobbyMenu.Tabs.WORLD+me.zlex.conduit.lobby.WorldSetup.WorldSettings, the data, has lived inconduit-worldsince the modular split, andWorldSettingsScreenrenders it on the legacy flat theme. The half every consumer re-implemented was the application logic: whichWorldTemplatea settings bag asks for, how a typed seed becomes a long, which gamerules to push, and how to find the nearest village.WorldSetuppackages that, plus theVanillaConfigMenurows for a World tab so the surface matches the rest of the setup screen instead of being a separate flat-theme panel. Purely additive.The engine still never regenerates a world for anyone.
templateFor()names the template; acquiring and swapping the instance stays the game's call.
- Added
- 0.17.0 +mc26.1.2 2026-08-18
- Added
Solo-testable games (
SoloMode,LobbyConfig.minPlayers,BaseLobby.beginRound/roundOver/underStrength). Every mini-game hard-coded the sameif (players.size() < 2) refusegate, so nobody could exercise a game without finding a second human. The minimum is now declared once, onLobbyConfig, and enforced throughSoloMode.canStart. One server-side switch —solo_testinconduit.toml, theSolo test roundstoggle in/conduit settings, or/conduit solo on— lowers it to one player for every registered game at once. Nothing about it is silent:SoloMode.announcepaints a red banner for every participant,sidebarNote()gives the HUD a persistent marker,GameRegistry.updateStatustags the hub pad, andLobbyMenutags the setup screen's title. It defaults to off, so shipped servers keep their real minimums.The gate was only half the problem. Most party games end when "one player remains", which with one player is true at tick zero — so an unlocked game would have started and ended in the same instant.
SoloMode.roundOver(alive, startingPlayers)(surfaced asBaseLobby.roundOver) is the corrected test: normally "at most one contender left", but for a round that began with one player, "the lone player is out". Games callBaseLobby.beginRound(min)at round start to record the round's size. - Added
LobbyMenu— the one lobby-setup surface. Six games each hand-assembled the sameVanillaConfigMenu.openTabbedcall with their own footer labels and their own "waiting for host" title.LobbyMenufixes the shape:Setup/Tuning/Arenatab names (LobbyMenu.Tabs), a footer that is always host-gated Start Game then always-available Leave Game, a sharedshowWaitingcard, and the solo tag on the title. - Added
Difficulty— the sharedEasy / Normal / Hard / Insane / Customvocabulary, plusindexOf/isCustom/isBundle. Five games declared byte-identical copies of this list and their own index helpers. - Added
LobbyConfig.zoneId— a game may pin theHubZoneid its pad claims instead of taking the derived"<game-id>:main". This is what lets a shipped game move ontoGameRegistry.registerwithout changing the id its hub, marker pipeline and saved state key off.GameRegistryalso gainedzoneIdOf,byZoneId, and a duplicate-zone-id guard.
- Added
- 0.16.0 +mc26.1.2 2026-08-18
- Added
Hub pads have a configurable size and shape (
PadFootprint,ZoneShape). Pad geometry was hard-coded atPAD_PLATFORM_HALF = 4— a 9×9 axis-aligned trigger box, in eleven places inHubManager. That is right for the pads the engine lays out on its own arc and wrong for a hand-built hub: a build whose pads are 5×5 gets a trigger sticking two blocks past every edge, and pads placed as close together as a human places them get flagged as overlapping when the pads themselves do not.PadFootprint(shape, size, height)—sizeis the full width / diameter in blocks, sosquare(5)andcircle(5)are both 5 across.boundsAt(centre)produces the trigger box; atHubManager.DEFAULT_FOOTPRINT(9×9, 7 tall) it is character-for-character the box the engine built before.registerCustomPad(server, zone, centre, normal, footprint)— new overload. The four-argument form is unchanged and now delegates withDEFAULT_FOOTPRINT, so every existing call site keeps its exact geometry.HubManager.getZoneFootprint(zoneId)andzoneContains(zoneId, point)— shape-aware companions togetZoneAabb, which still returns the bounding box (for a circle, the circle's bounding box).- The ambient particle ring now scales with the pad: its radius is the pad's own half-width less a 0.1 inset and the dot count scales with that radius, so a 5-wide pad gets a 5-wide ring. At the default footprint both work out to the previous constants (4.4 and 18), so engine pads emit the ring they always have.
- Added
ZoneShape(conduit-prefab) — sealedSQUARE/CIRCLE, the horizontal cross-section of aZoneDef.SQUARE.containsis literallyAABB.contains;CIRCLEis radial about the bounds' horizontal centre with the same half-open vertical rule. Neither variant carries state: a zone's position and size are always itsaabb(), and a circle's is its bounding box — one source of geometric truth, so shape and bounds cannot drift apart.ZoneShape.overlaps(shapeA, a, shapeB, b)is exact for all three pairings. Square/square is box intersection (equivalently a Chebyshev test — Euclidean chord distance under-reports for axis-aligned boxes). Circle/circle is Euclidean (Chebyshev would over-report). The mixed case is neither: it clamps the circle's centre into the box on each axis and compares that distance against the radius.
- Added
HubManager.checkPadOverlaps()— boot-time self-check, run at the end ofSERVER_STARTEDafter whichever build path ran, so it covers the engine's arc and a hand-built hub alike. Logs each overlapping pair with both centres, both footprints, the measured separation and the required one. Reports only; whether to refuse a bad layout is the hub owner's call. - Added
The hologram read-back path is exposed (
worldLabel+HubManager.getZoneLabelId).ServerLabelManageralready stores every world label server-side so late joiners can be re-sent it, so a published hologram was readable all along; it just had no accessor.getZoneLabelIdis the companion togetZoneAabbthat turns a zone id into that widget id. Together they let a hub self-check assert thatupdateZoneStatusreally repainted a pad, rather than assume the call landed. - Changed
ZoneDefgained ashapecomponent (third of four). The previous three-argument constructor is retained and defaults toZoneShape.SQUARE, so existing call sites compile and behave identically.ZoneManager.zoneAtnow asksZoneDef.contains, which for a square zone is the sameaabb().contains(pos)call it made before.
- Added
- 0.15.0 +mc26.1.2 2026-08-18
- Fixed
A custom hub's pads had no runtime API.
setCustomBuilderskipped the engine's own pad-placement path, which is the only thing that fillsHubManager's privatebuiltmap, soupdateZoneStatus,updateZoneLabelandgetZoneAabball silently no-opped and holograms froze on their initial status while matches ran.HubManager.registerCustomPadlets a builder hand each placed pad back to the engine, running the sameregisterPadcode the arc build uses, so the hologram, hovering icon, trigger AABB and retainedZoneDefare built identically and torn down identically byclearPads/SERVER_STOPPING. - Fixed
registerPad's AABB is anchored to the pad's own Y instead ofROOM_Y_FLOOR: identical for the engine's arc, correct for a custom builder at another height.clearPads()is now exposed so a builder can drop a previous build's zones and widgets before laying out a new one.
- Fixed
- 0.14.0 +mc26.1.2 2026-07-16
- Fixed
Underground spawns were delivered to the roof of the world.
MobSpawns.spawnAroundresolved the motion-blocking heightmap unconditionally, socenter.yonly ever picked the x/z column and never the height. Above ground that is invisible, because the surface is the answer. Underground it is not: a player strip-mining at y=12 got their ambush arranged on the grass a hundred blocks overhead, where it spawned, milled about and never arrived.BloodMoonEventuses the same call, so the horde did it in caves too.spawnYnow hunts outward fromcenterYfor a foothold (solid floor, two blocks of air); open ground is unchanged. The surface stays a fallback only when the caller is already near it. From underground that is not a near-miss but a different place, so the call returnsNO_SPOTand the caller skips it. A cramped tunnel therefore yields fewer mobs rather than a pile of them somewhere the player will never see, and the returned list stays honest about how many actually arrived.
- Fixed
- 0.13.1 +mc26.1.2 2026-07-16
- Fixed
A buffed mob lost its original team for good.
joinGlowTeamcalledaddPlayerToTeamunconditionally, which silently reassigns an entity that already belongs to a team, andremove()can only take it off the glow team, never put the original back, so a buffed-then-unbuffed mob permanently lost its team colour, friendly-fire and nametag rules. If the entity is already on someone else's team it is now left there; that team's colour is what the glow renders anyway.
- Fixed
- 0.13.0 +mc26.1.2 2026-07-16
- Added
MobBuff.glowing(ChatFormatting): coloured glow outlines. An un-teamed glowing entity always renders a white outline: vanilla takes the colour from the entity's scoreboard team, so a colour was simply unreachable throughMobBuffand downstream mods had to hand-roll scoreboard teams around the engine.apply()now joins aconduit_glow_<colour>team that carries nothing but the colour andremove()leaves it again. One team per colour, created on demand and shared by every buff and event, so the cost is a bounded handful of scoreboard entries rather than one per event.leaveGlowTeamonly unteams a glow team, so it will not strip an entity off a team someone else put it on. Non-colour formattings (BOLDand friends) fall back to plain white rather than making a junk team.glowing()with no argument keeps vanilla white. - Changed
MobBuff.elite()andboss()read as elite at a glance.elite()goes +15% → +35% speed and gains +30% scale, taking a RED outline to match its "⚔ Elite" badge;boss()takes DARK_RED to match "☠ Boss". ⚠️SCALEmoves the hitbox too, so a +30% zombie no longer fits a 2-block corridor.
- Added
- 0.12.0 +mc26.1.2 2026-06-18
- Added
conduit-mobEntityReg— custom-EntityTyperegistration glue.register(namespace, path, EntityType.Builder)builds against theResourceKeyand registers intoBuiltInRegistries.ENTITY_TYPE;attributes(EntityType<? extends LivingEntity>, AttributeSupplier.Builder)wrapsFabricDefaultAttributeRegistry.register. Server-safe (no client imports) — the glue Cataclysm's GeckoLib mobs (alien, ghost, ninja, running creeper, cave monster, lava golem, shark) register through. - Added
conduit-mobForces— stateless, capped, per-tick field forces over nearby entities (caller owns the loop).pullToward(level, point, radius, strength)(black hole),vortex(level, axisBase, radius, swirl, lift)(tornado),sweep(level, lineCenter, dir, halfWidth, push)(tsunami wall). Radius clamped toMAX_RADIUS(96), entities capped atMAX_ENTITIES(512); velocities flagged dirty so player knockback resyncs. - Added
conduit-mobProjectiles— observe/clone projectiles without a mixin.onProjectileSpawn(BiConsumer<ServerLevel, Projectile>)registers aServerEntityEvents.ENTITY_LOADlistener filtered toProjectile;cloneWithSpread(Projectile, double spreadDegrees)spawns a same-type copy with yaw-rotated velocity (triple-shot fan-out). - Added
conduit-mobStatusEffects— custom-MobEffectregistration + contagion.register(namespace, path, MobEffect)→Holder<MobEffect>intoBuiltInRegistries.MOB_EFFECT;spreadOnContact(level, carrier, effect, radius, durationTicks, amplifier)applies the effect to nearby living entities (Ebola spread). Caller ticks it; capped atMAX_SPREAD_TARGETS(256). - Added
conduit-mobDebris—rain(level, center, radius, count, palette, spawnHeight, explodeOnImpact)dropsScheduler-staggeredFallingBlockEntitys of random palette blocks over a disc, optional anvil-style impact damage. Count capped atMAX_COUNT(512). Meteor / volcano / falling-trees reuse this. - Added
conduit-fxAtmosphere— environmental mood effects, self-restoring viaScheduler.bloodSky(players, ticks)envelops each player in a red dust haze (server-safe approximation of a fog shader — documented limitation);weather(level, storming, thundering, ticks)forces then restores weather;shake(players, intensity, ticks)best-effort camera rumble via relative- rotation position packets.
- Added
- 0.11.0 +mc26.1.2 2026-06-17
- Added
Flight(conduit-mob): hand-driven flight stepping for custom craft. The reusable primitive for flying entities that move by direct repositioning rather than pathfinding (UFOs, drones, flying bosses). It caps each step, turns the craft to face its travel, and zeroes velocity to avoid the double-step aLivingEntity's owntravel()would otherwise add by integratingdeltaMovementon top of thesetPos. Pair it withnoPhysics+noGravity+updateInterval(1)for smooth, client-interpolated flight. - Fixed
The
Ufoabduction beam resized as the ship departed. Once the captive is aboard the beam now locks to a short fixed length and simply follows the ship, instead of rescaling from the captive's jittery position.
- Added
- 0.10.22 +mc26.1.2 2026-06-17
- Fixed
Ufoship didn't move / captive got stuck — moving aDisplayvia theInterpolationHandler.interpolateTodidn't reliably advance the entity, so the saucer hovered in place and never flew off. Movement now usessetPosdirectly (the entity tracker syncs it; the client smooths via a set interpolation window), updates every tick, lifts the captive straight to the ship, and times the hover out so the sequence always completes.
- Fixed
- 0.10.21 +mc26.1.2 2026-06-17
- Fixed
Ufobody re-grew from 1×1 every tick — static saucer parts had their transform re-applied each update. Re-sending the transform's start-interpolation marker for an unchanged transform makes the client re-interpolate the scale from the default (1×1) up to full, so the body looked like it re-inflated every tick. Static parts are now posed once at spawn and never re-touched; only the orbiting lights (whose transform actually changes) re-apply — also fewer transform packets.
- Fixed
- 0.10.20 +mc26.1.2 2026-06-17
- Fixed
Ufomotion was jittery — the saucer hard-teleported each update (blocks visibly vanished + reappeared as it rose) becauseDisplayentities don't interpolate position by default. Movement now routes through the 26.xInterpolationHandler(getInterpolation().setInterpolationLength+interpolateTo) so the client smoothly slews the ship and beam between updates; the captive moves viasetPos(not a hardsnapTo) for the same reason. Descend/ascend sped up a little and beam particles trimmed for lighter clients.
- Fixed
- 0.10.19 +mc26.1.2 2026-06-16
- Added
Ufo— animated flying-saucer abduction prop (conduit-mob). A self-contained "custom model + animation" built from vanillaDisplayentities — no model library needed.Ufo.abduct(level, ground, target)spawns a saucer rig (disc body, glowing belly, glass dome, four orbiting rim lights) high above a point; a singleSchedulertask drives a descend → beam → abduct → ascend → depart state machine. The saucer spins (each part bakes its offset/scale/spin into aTransformationthe client tweens between updates), drops a glowing beam +END_RODparticle column, lifts the target up into the ship (non-players are taken/discarded, players released), then climbs away and cleans every entity up. The first set-piece for the Cataclysm "alien invasion" event.
- Added
- 0.10.18 +mc26.1.2 2026-06-16
- Added
MobBadgestate accessors —displayInWorld()andcurrentText()to introspect a live nameplate. Added while runtime-verifying the enhanced-mob system end-to-end on a dedicated server: buffs apply (a boss zombie goes 20→100 HP, scale ×1.4, glowing), the nameplate is added to the world and follows, its health bar drains with the mob (100→80→60→40→20), and it auto-discards on death.
- Added
- 0.10.17 +mc26.1.2 2026-06-16
- Added
Enhanced "elite/boss" mobs (
conduit-mob).MobBuff— a named, declarative, removable stat-buff bundle (the improved successor toMobBuffs' permanentsetBaseValuescaling): stable-idAttributeModifiers applied viaaddOrUpdateTransientModifier(idempotent — re-apply updates in place, never stacks) and stripped cleanly byremove. Fluent builder over health / speed / damage / scale (visible size) / armor / knockback-resist / attack-knockback / follow-range, as percent or flat, plus a glow flag and ability-display lines;MobBuff.elite()/MobBuff.boss()presets.MobBadge— a floating nameplate that follows a mob (the engine's first entity-follow display): a vanilla billboardTextDisplayre-positioned each tick, showing the name, a live unicode health bar (green→yellow→red by %) withcur/max, and the buff's ability lines, auto-discarded when the mob dies.MobAnimations— generalDisplay-transform animation helpers (scale-in pop / interpolated scale) over vanilla interpolation, the engine's first entity-attached animation support.Mobs.enhance(mob, buff, name)ties buff + badge together in one call.
- Added
- 0.10.16 +mc26.1.2 2026-06-16
- Changed
Mob-feature hardening pass.
Scene.instantiatenow isolates per-element failures — a single bad id (a typo'dmob-spawnentity, an unknown block in ablock-list/volume) is skipped-and-logged instead of aborting the whole prefab and leaving a half-built scene.MobSpawns.executeclamps authored count (≤256) and radius (≤128) and preserves the authored Y (an elevated-platform spawn spawns on the platform, not the terrain below).MobWavegained an absolute lifetime backstop (30 min) so a wave left fully unbounded can't leak a forever-spawning loop, andconduit-mobnow drops all active waves onSERVER_STOPPEDso the static registry can't carry stale waves into the next world in a persistent JVM.
- Changed
- 0.10.15 +mc26.1.2 2026-06-16
- Added
MobWavecontroller (conduit-mob) — capped, interval-paced spawn waves, the runtime primitive the big scheduled events (a Blood Moon's "hundreds of mobs", an alien invasion) are built on. A fluent builder (MobWave.builder(level, type).follow(player).around(8, 24).perWave(8) .every(40).maxAlive(60).total(300).duration(...).onSpawn(...).start()) spawns batches around a re-evaluated anchor (so it can follow a player), counts only still-alive mobs against the concurrent cap, and defaults to persistent spawns so event mobs don't despawn mid-event.cancel(discardLiving)ends a wave and can sweep its survivors. A singleMobWavesdriver (one server-tick listener,CopyOnWriteArrayList-backed, wrapped inTickBudget) advances every active wave and drops finished ones, so waves don't each register their own hook.
- Added
- 0.10.14 +mc26.1.2 2026-06-16
- Added
conduit-mobmodule — runtime mob spawning + buffing helpers, the layer chaos / event mods build their spawns on (no per-mod vanilla boilerplate).MobSpawns.spawnAround(level, center, type, count, minR, maxR[, reason, afterSpawn])scatters entities on the surface in an annulus (heightmap ground-snap, random yaw) and returns what it added;MobSpawns.spawnAt(...)places at an exact Y for arena / authored points.MobBuffsscales attributes (scaleHealthtops up to the new max,scaleSpeed,scaleDamage,empower) — every scale is null-guarded so attribute-less mobs (e.g. a creeper has no attack-damage attribute) pass through safely — plusequip(mob, slot, stack)with drop chance pinned to zero. Spawns useEntitySpawnReason.COMMANDby default (deliberate, not environmental). A capped/interval-pacedMobWavecontroller and GeckoLib-aware custom-entity registration glue are planned follow-ups (seedocs/MOB-FEATURES-PLAN.md). - Added
mob-spawnprefab element — a named, authorable spawn point. LikeZoneit is a descriptor, not an action: instantiating the prefab spawns nothing,Scene.mobSpawns()exposes the resolvedMobSpawnDef(entity type, count, world centre, scatter radius) and the consuming game — orconduit-mob'sMobSpawns.execute/executeAll(scene)— fires it on demand. This keeps the prefab purely declarative (no entity duplication on/reload) and is what lets Conduit Studio author mob spawns, closing its geometry-only gap.
- Added
- 0.10.13 +mc26.1.2 2026-06-08
- Added
conduit-rewindmodule — live full-fidelity rewind / time machine for a running match. A host arms a recording, then can pause, scrub to any earlier moment (players, blocks, entities, and scores all snap back), and resume play from there — a real do-over. Restores state, never replays behaviour (MC is non-deterministic), so the recording is only ever read backwards. Capture: per-tick player poses + inventory keyframes; block-write journal (LevelChunk.setBlockStatemixin) restored by minimal diff; entity ledger (spawn/remove + spawn NBT + transform samples + state keyframes) via the modernValueInput/ValueOutputNBT system. Pause freezes entity ticking (mixin) + locks players + shows a PAUSED overlay; a boss-bar scrub timeline tracks the preview head.SnapshotSliceSPI rewinds game state — arena shipsMatchRewind.bindsoMatchscores rewind too (Match.restoreState). Full command tree (/conduit rewind arm|stop|pause|resume|back|step|to|commit| cancel|mark|status|save|discard|list|load, gated byconduit.rewind.control). Coverage is full up torewind_memory_cap_mb, with logged eviction beyond it. At match end the host is prompted to save a durable match record (summary + event-marker timeline) under<world>/conduit_rewind/. - Added
MatchCompletedEvent(arena) — fired on the bus fromMatch.finish; the completion signal a tournament/Season runner (and stats/announcers) consume. - Added
WorldNpc(render) — frozen, vanilla-rendered character NPC figures (armor-stand-backed with equipment) for hub guards / doll / front-man. Fluent builder: name (Text markup), head/chest/legs/feet/hands, glow, slow spin; visible on every client incl. vanilla. Server-owned lifecycle (character(...).show()/worldHide). - Added
Hologram(render) — fluent, handle-based floating label overServerLabelManager(auto-allocated id;show()/lines()/label()/remove()). - Added
Hud.sidebar(fx) — framed MCC-style scoreboard preset. - Fixed
Duplicate / static hub pad displays. The arc-lobby pad icons are real
Displayentities; a mid-session autosave persisted them, so on the next server start the saved copies reloaded as static leftovers beside the freshly-spawned (animated) set, and on a cold first start they didn't tick until a relog.HubManager.buildArcLobbynow force-loads the room chunks (so the displays tick and animate from the first join) and sweeps any pre-existingBlockDisplay/ItemDisplayin the room before spawning fresh ones.
- Added
- 0.10.10 +mc26.1.2 2026-06-05
- Added
DebugOverlay(fx) +/conduit debug— toggleable action-bar dev HUD (players/mem/dimension + custom lines); first in-engine consumer of@Command. - Fixed
CommandReflectorpermission gating — gated only the root literal, so a subcommand merged into an existing root (e.g.conduit debug) leaked ungated; now every node carries the permission predicate.
- Added
- 0.10.9 +mc26.1.2 2026-06-05
- Added
Zones(core) — named AABB trigger regions firingZoneEnterEvent/ZoneLeaveEventon the bus; kills hand-rolled "is the player on the pad" polling. - Added
ItemBuilder(core) — fluentItemStack(Text-markup name/lore, glow, count). - Added
FxPresets(fx) — confetti / winBurst / ringPulse particle choreography.
- Added
- 0.10.8 +mc26.1.2 2026-06-05
- Added
GameActivity+GameRule(arena) — Plasmid-style scoped, auto-unregistering event node + deny/allow rules (ALL_DAMAGE/FALL_DAMAGE/PVP/HUNGER/BLOCK_BREAK), enforced once viaGameRuleEnforcer. - Added
Sequence(core) — TaskChain-stylerun/delay/waitUntil/loopover the scheduler. - Added
Leaderboard(fx) — top-N holographic board overProfileStorecounters (offline-safe names; the engine now records each player's name on join).
- Added
- 0.10.7 +mc26.1.2 2026-06-05
- Added
Event bus (core,
me.zlex.conduit.event) — Minestom-styleEventNodetree, cancellable + prioritized events,@Subscribeauto-listeners (explicitListeners.register+conduit:listenersentrypoint), fault-isolated dispatch. Bridges Fabric server events (damage/join/leave/tick). - Added
Declarative commands (core,
me.zlex.conduit.command) —@Command/@Arg/@Suggestsreflector over Brigadier, permission-gated. - Added
Scheduler+Cooldowns(core) — Bukkit-style task scheduling on the bus; per-owner millisecond cooldowns. - Added
Text(core) — MiniMessage-style markup + gradients/rainbow → vanillaComponent(no Adventure dependency). - Added
Services(core) — typed service locator / DI-lite. - Added
Ease+Tween(fx) — Penner easing curves + tick value tweener. - Added
SoundKit(fx) — semantic per-player UI/gameplay sound vocabulary. - Added
Menu(fx) — fluent themed menu over in-world screens (scale-in pop + audio). - Added
Hud(fx) — countdown bossbar (drains/recolors/ticks) + cinematic role reveal. - Added
Theme.premium()/Theme.lerp()(render) — navy+gold preset + crossfade.See [
wiki/framework.md](wiki/framework.md) for the full guide.
- Added
- 0.10.6 +mc26.1.2 2026-06-05
- Added
HubManager.setCustomBuilder(arena) — optional hook letting a mod replace the engine's default spawn-box / pad-arc hub build with its own (e.g. paste a custom hub). Registered zones stay available via the newHubManager.zones(); custom builders set the spawn through the newHubManager.setSpawn(pos, yaw). - Added
Podium.celebrate(..., Collection<ServerPlayer> audience)overload — scopes the winner title card + fanfare to a supplied audience (e.g. the finishing lobby) instead of every player on the server. - Fixed
Cross-lobby celebration spam.
Podium.celebratepreviously broadcast the winner title + fanfare to the whole server, so on multi-lobby servers one lobby finishing interrupted everyone. The no-audience overload still broadcasts server-wide for backward compatibility; callers wanting lobby-scoped celebrations use the new overload.
- Added
- 0.10.2 +mc26.1.2 2026-06-03
- Added
Metrics(core) — a process-lifetime counter registry. Engine hotspots bump named counters (player.leaves,phase.stuck,callback.faults) so operators can read health without grepping logs. - Added
/conduit admin instances— lists every live game instance (dimension, template, player count, callback-fault count). Op-gated. - Added
/conduit admin metrics— dumps theMetricsregistry. Op-gated. - Added
/conduit leaderboard <stat> [count]— prints the top-N players by anyProfileStorecounter (wins,games_played,points_total, …) in chat, with online-name resolution and stat-name suggestions. (The in-world board render remains future work — this is the data/command half.)
- Added
- 0.10.1 +mc26.1.2 2026-06-03
- Added
Quality & feature pass (engine-discovery roadmap). A survey of all eight modules drove a batch of additive, non-breaking features:
PlayerLifecycle(conduit-core) — one engine-wide disconnect fan-out. Subsystems registeronLeave(UUID)handlers; on disconnect every transient per-player map is pruned (state, inventory stash, fx sidebar, fx bossbars, spectator gamemode), so long-running servers no longer leak as players churn. NewConduitFxModentrypoint (fx previously had none).ProfileStore+PlayerProfile(conduit-core) — durable cross-game player profiles (schema-free counters + attributes) persisted via SavedData on the overworld.get/increment/set/counter/setAttr/attr/top. The keystone for leaderboards, achievements, and match history.Achievement+Achievements(conduit-core) — declarative unlock triggers on profile counters ("first_win"whenwins >= 1), persisted asach.<id>attributes so they never re-fire. Default gold-toast presentation viaAchievementToast(conduit-fx), wired through a callback handoff.MatchResult+MatchHistorySavedData(conduit-arena) — snapshot a finishedMatch(winner/standings/points),commitTo()folds wins/games_played/points_total intoProfileStore, and a capped ring buffer keeps the last 50 results queryable.Match.finish(gameId).PhaseFlowtimeouts +PhaseWatchdog+TimedPhaseFlow(conduit-arena) — per-phase deadlines that auto-advance a stuck phase (AFK host, missedtransitionTo) and log/alert on recovery.PhaseMachine.ticksInCurrentPhase().GuardedDispatch+FaultPolicy(conduit-arena) — exception-isolation barrier around game-supplied callbacks so one throwingonTick/phase callback can't kill the server tick or wedge a half-applied phase.Game.faultPolicy()(defaultLOG_ONLY, backward-compatible 8-arg ctor).- fx choreography layer (conduit-fx) —
Particles(burst/ring/puff + elimination/safe macros),FxTimeline(self-tickingat/after/repeatsequencer),EffectThrottle(per-key cooldown gate),ScoreBoardBinding(generic standings →Sidebar) + arena-sideMatchScoreboardadapter. SpectatorManager.SpectatorListener(conduit-spectator) — observer seam fired on make/release spectator (spectator-HUD prerequisite).
Fixes flagged in the survey: thread-safety on
PlayerStateManager,PlayerInventoryStash,Sidebar,PlayerBossbar,SpectatorManagermaps (→ConcurrentHashMap); theSidebar.showsubListaliasing bug. - Added
Jar-bundled arena presets (conduit-world).
ArenaStore.loadnow falls back to a classpath resource at/conduit/arenas/<gameId>/<preset>.jsonwhen no game-dir file exists, so a published mod can ship its authored arena inside its own jar (undersrc/main/resources/conduit/arenas/<gameId>/default.json) and it loads out of the box on a fresh install. An operator still overrides it by dropping a file of the same name into<gameDir>/conduit/arenas/…(disk wins).list()surfaces the bundleddefaultso the host preset picker shows it too. - Added
Multi-spawn points in the arena editor (conduit-world + conduit-arena). Operators can mark any number of player spawn points inside an edit session and bake them into the saved preset:
/spawn add— record a spawn where you stand (position + facing yaw)./spawn list//spawn remove <n>//spawn clear— manage the set. Spawns persist in the preset JSON as offsets relative to the snapshot's low corner (a new optionalspawnsfield onArenaSnapshot, so older preset files still load), andArenaSnapshot.worldSpawns(origin)resolves them for a paste. When a started game's preset carries spawns, players are distributed across them (cycling) instead of the procedural spawn ring; with no spawns the game falls back to its existing auto-spawn. Editing the default preset pre-loads its existing spawns so a re-save preserves them.
- Added
WorldEdit-style
.schemschematic import/export (conduit-world). NewSchematicIoreads [Sponge Schematic](https://github.com/SpongePowered/Schematic-Specification) files (gzip NBT) from<gameDir>/conduit/schematics/and bridges them toArenaSnapshot, plus a/schemsubtree (gated onCAP_EDITOR):/schem list— list available.schemfiles./schem paste <name>— paste a schematic at the player'spos1(else their feet), pushed through the/undostack. Reports block count, any unknown blocks (pasted as air), and whether block-entity data was dropped./schem save <name>— write the current selection out as a Sponge v3.schem. Read side covers Sponge v2 (flat root) and v3 (nestedSchematic.Blocks); the varint block-index stream (YZX order) is decoded in-house and palette strings resolve viaBlockStateParser. Blocks only — block entities, entities and biomes are ignored; air overwrites the destination so a paste matches the source exactly.
- Changed
Hub pads: the floating block display now sits above the name hologram (conduit-arena). The spinning game icon reads on top with the label just beneath it, instead of the icon below the text.
- Changed
Editor commands are now top-level (conduit-world). The
/cedit <sub>tree was flattened into standalone commands —/wand,/pos1,/pos2,/sel,/set,/walls,/replace,/copy,/paste,/undo, and/schem …— matching the WorldEdit muscle memory operators expect. All remain gated onCAP_EDITOR. - Fixed
WorldBlockDisplayblock/item displays rendered pure white (correct cube shape, untextured) for every consumer (hub pads, the Red Light doll, Hot Potato's bomb + power-up markers). Root cause: the custom client-overlay path (ClientBlockDisplay→ItemStackRenderState.submit(...)inside theLevelRenderEvents.COLLECT_SUBMITShook) cached the resolvedItemStackRenderStateonce on the first frame; that snapshot could be taken before the item-model atlas finished baking (themc.level != nullguard does not guarantee models are baked after a resource reload), leaving the layers pointing at a blank/missing sprite — the cube shape with no texture. - Changed
WorldBlockDisplayis now entity-backed (conduit-render). Instead of a client-only overlay driven byShowBlockDisplayPayload, displays now spawn real vanillaDisplay.BlockDisplay(block form) /Display.ItemDisplay(item form) entities server-side: non-interactive (no gravity/collision, invulnerable, full-brightness), centred on the anchor, with theBlockDisplaySpecmapped onto the entity transformation (uniform scale) plus a per-tick Y-spin / vertical-bob animation interpolated client-side. Because the model is a real vanilla render-state it is fully textured on every client, including vanilla clients without the engine installed.- Real rotation.
rotationDegreesPerSecondis now a true entity rotation, so a fixed-yaw orientation is achievable — the Red Light doll can now actually turn to face the lane (capability exposed; the game mods are unchanged). - Per-player scope changed.
show/showWithId/hidenow spawn a single shared world entity (visible to everyone in the dimension), not a per-player overlay; theServerPlayerargument is used only to resolve the dimension. The public signatures are unchanged and the per-player id-tracking map is retained for source compatibility, but a marker can no longer be shown to just one player through this API — drive single-viewer visibility from game logic instead. WorldBlockDisplay.registerPayloads()is now a no-op (kept for source compatibility); the oldShowBlockDisplayPayload/HideBlockDisplayPayloadand the clientClientBlockDisplay*overlay are no longer on the active path.
- Real rotation.
- Added
VanillaConfigMenu— customizable footer. Newopen/openTabbedoverloads take aFooter(or adoneLabel+onDoneshorthand): a primary button whose label/action you choose (e.g."Start Game"), an optional secondary button beside it (e.g."Leave Game"), and alargemode that renders both bigger + lower (for results screens). The no-arg form still defaults to a single"Done"button that hides (fully backward compatible). - Added
Universal
/lobby leave(conduit-arena) — leaves whatever game lobby the caller is in, routed through a newBaseLobby.leaveHandlerthe game sets so the leave runs the game's own teardown (eliminate / hub / release instance). - Added
In-game arena editor + custom WorldEdit —
me.zlex.conduit.editor.WorldEdit(conduit-world): a golden-axe selection wand (left-click → pos1, right-click → pos2, op-gated via the newConduitPermissions.CAP_EDITOR) + a/ceditcommand tree (set/walls/replace/copy/paste/undo/pos1/pos2/sel/wand) over the selection, built onRegions, with a per-player clipboard, undo stack, and a particle selection outline.ArenaSnapshot(capture/paste a region;BlockState.CODECJSON) +ArenaStore— named arena presets per game id, persisted under<gameDir>/conduit/arenas/<gameId>/<preset>.json(survives restart), reserveddefaultpreset.ArenaEditSession(conduit-arena) — op editor sessions: spin up a void edit world, paint the current arena, Creative + wand, capture-and-save a preset, return to hub. _(All of the above will be cut as 0.10.1.)_
- Added
- 0.10.0 +mc26.1.2
- Added
VanillaConfigMenu(conduit-render) — declare a title + ordered list ofConfigEntry, pass anonChangecallback, callopen(); the framework compiles a vanilla-themed screen, routes every control click and text submit, applies the standard mutation, fires the callback, and re-issues the screen. Tabs (openTabbed+ConfigTab), automatic pagination (PAGE_SIZE = 6,[< Prev] Page x/y [Next >]), and a per-(player, screenId)open registry. - Added
ConfigEntrysealed types —Toggle,Cycle,Stepper,Slider,Action,Text, each immutable with typedwith*helpers the framework calls for you. - Added
Real sliders —
ConfigEntry.Slider+VanillaSliderElement+Widgets.vanillaSlider: a recessed track with a raised handle. Click routing goes through the new positionalServerScreenManager.onButtonAthandler, which carries the click's screen-UV (ButtonClickPayloadgainedclickU/clickV) so the server resolves the track fraction. Click-to-position (an in-world screen emits a click, not mouse-move), snapped tostep. - Added
Stateful vanilla controls —
Widgets.vanillaToggle/vanillaCycle/vanillaStepper, theVanilla*Widgetrecords, and theScreenLayoutCompilerbranches that drive them. - Changed
Stateless button chrome is now textured. Buttons whose look never varies with state —
Cycle, theStepper[−]/[+],Action,Done,Prev/Next— render the 9-slice pixel-art vanilla button surface instead of the procedural bevel. Controls whose chrome encodes state stay procedural: the toggle (bevel direction = on/off), tabs (pressed-in = selected), and the slider (recessed track + moving handle). The textured button corner is sized to vanilla's 3px/20px border ratio (VANILLA_BUTTON_CORNER_WORLD) so the border reads thin and elegant rather than chunky. - Changed
Pressed/ON buttons darken the whole face uniformly (~18%,
PRESSED_DARKEN) on top of the inverted bevel, so an active toggle reads as genuinely pushed into shadow rather than just re-lit. - Fixed
Button z-layering. The procedural and textured buttons were pinned at a low z (
0.012) that collides with the per-indexRectElementz band on a rect-heavy screen (e.g. the scroll list's row backgrounds vs the arrow buttons), causing z-fighting. Buttons now composite above the rect band ceiling and below the text plane — the same fix the text-input field already had — while keeping their internal layers a tight0.0005apart so a rear layer (the black outline) no longer parallax-leaks past the fill on an off-axis screen.
- Added
- 0.9.0 +mc26.1.2
- Added
Developer-velocity pass — four reusable primitives so new mini-games stop re-implementing the same scaffolding:
SafeTeleportnow preloads the destination chunks (toChunkStatus.FULL) before the deferred teleport fires, fixing the "land in darkness / fall through unloaded terrain" stutter that every cross-area teleport hit. New overloadteleport(player, level, pos, yaw, pitch, preloadRadius, after)and a publicSafeTeleport.preloadChunks(level, pos, radius). Default radius is 1 (3×3 chunks); passSafeTeleport.NO_PRELOADto skip.conduit-fxTitles—show/actionBar/clearfor single players or groups, dedup'ing the three-packet animation→subtitle→title dance every mod hand-rolled. Ticks for timing; sensible defaults.conduit-worldRegions— axis-aligned boxfill/fillHollow/fillWalls/replace. The geometry every prefab-built arena re-wrote; caller still picks theBlockStateso the engine stays game-agnostic.conduit-coreConduitTest— a solo-testing harness: an op-gatedtest list/test run <scenario>brigadier subtree (buildCommand), a named-scenario registry (register), and a deterministic fake roster (fakeRoster(n)→ "Bot1"…"BotN" with stable, tagged UUIDs) so a whole match can be driven from one client without a full lobby.conduit-coreGatherRoom— the "assemble everyone, then send them home" lifecycle (meetings, voting rooms, finales): force-loads the room chunks while open, snapshots each player's prior dimension/position, optionally stashes their inventory, and reverses all of it ondisperse. Both directions route throughSafeTeleport. Geometry stays the caller's job.
- Added
- 0.8.0 +mc26.1.2
- Added
RuntimeDimensionSpecdata-driven factories:fromStem(stem, seed, persist),fromStemJson(server, json, seed, persist)(decode aLevelStemfrom JSON viaLevelStem.CODEC+ registry-awareRegistryOps), andfromDatapackStem(server, stemKey, seed, persist)(reference a datapack-registered dimension). - Changed
conduit-arena'sInstanceManageris now on-demand + handle-based, built onconduit-instance'sRuntimeDimensionsinstead of a fixed 8-slot pool:- No
POOL_SIZEcap —acquirecreates a freshconduit:rt-<n>dimension each time; instances are addressed by dimension key, not slot index. GameInstance.slot()→handle()(aRuntimeDimensionHandle);dimension()+template()+level()+release()unchanged.InstanceManager.get(int)→get(ResourceKey<Level>)/get(String).- Instances persist across restart (RuntimeDimensions
persist=true); arena keeps a small key→template state file to reattachGameInstancewrappers on boot.
- No
- Changed
Session routing is now String-keyed.
SessionRecord.instanceSlot(int) →instanceKey(String dimension key).PlayerSessionManager:recordDisconnect(..., int, ...)→(..., String instanceKey, ...),holdSlot(int)→holdInstance(String),isSlotHeldFor(int)→isInstanceHeldFor(String),setHoldExpiryHandler(BiConsumer<Integer,UUID>)→BiConsumer<String,UUID>. - Removed
ServerLevelSeedMixin+InstanceManager.getSeedOverride—RuntimeDimensionspasses the seed to theServerLevelconstructor directly, so the seed-override mixin is no longer needed. - Removed
The 8
data/conduit/dimension/instance-*.jsondatapack dimensions — instances are created at runtime now, not pre-declared. - Migration
Consumers calling
recordDisconnectwith anintslot pass the instance's dimension-key string instead (""for no instance). ReadingGameInstance.slot()becomeshandle()/dimension(). The common consumer path —InstanceManager.acquire(server, WorldTemplate.VOID)→level()→release()— is unchanged.### Still out of scope
- Migration
Per-instance custom dimension types authored from JSON beyond the spec factories.
- Added
- 0.7.0 +mc26.1.2
- Added
Persistent runtime worlds in
conduit-instance. Apersist=trueRuntimeDimensionSpecis now recorded to a manifest (<world>/conduit_runtime_dimensions.json, itsLevelStemserialized viaLevelStem.CODEC+ registry-awareRegistryOps) and recreated on the next server start, loading its existing region data. v0.6 silently deleted these on boot. - Changed
persistnow means restart survival, not release survival.releasealways fully destroys (evacuate → unload → delete files → drop manifest entry), regardless ofpersist. To keep a world across a restart, don't release it — the manifest carries it over. - Changed
The boot orphan sweep is manifest-aware: it deletes only
conduit:rt-*dimension folders whose key isn't active after restore (crashed temp worlds), instead of nuking everyrt-*dir. - Changed
Clean shutdown saves + keeps persistent worlds (so they restore next boot) and force-deletes only non-persistent temp worlds.
### Still out of scope
- Changed
Per-instance custom dimension types authored from JSON.
- Changed
Migrating
conduit-arena's fixed pool onto on-demand.
- Added
- 0.6.0 +mc26.1.2
- Added
conduit-instancemodule — on-demand runtime dimensions. Create a fresh world per match and tear it down when the match ends, instead of claiming fromconduit-arena's fixed 8-slot pool.RuntimeDimensions.create(server, spec)/.release(server, handle).RuntimeDimensionSpecwithvoidWorld(server)+copyOf(template)builders and apersistflag (default discard-on-release).RuntimeDimensionHandle— live handle to a created dimension.- One accessor mixin (
MinecraftServerLevelsAccessor) to reach the server's level map; everything else is public MC API + Fabric events. - Crash-orphan sweep on
SERVER_STARTED+ force-release of all active runtime dimensions onSERVER_STOPPING.
- Added
v1 supports discard-on-release temp worlds only; persistent runtime worlds (restart recovery) are not yet implemented. Arena's fixed pool is unchanged — migrating it onto this is future work.
- Added
- 0.5.0 +mc26.1.2
- Changed
SpectatorManagernow uses vanillaGameType.SPECTATORinstead of the old ADVENTURE + flight + infinite-invisibility + scoreboard-team model. Breaking:makeSpectator/releasekeep the same signatures but the resulting player state is different (true spectator, not adventure). Vanilla SPECTATOR natively blocks pickup / drop / use / attack and cancels damage, so consumer mods no longer need to layer their own interaction restrictions on top. - Removed
SpectatorManager.TEAM_NAMEconstant. - Removed
Internal
applyAbilities/ensureTeamhelpers + theseeFriendlyInvisibles/collisionRulescoreboard team. - Removed
The
ServerLivingEntityEvents.ALLOW_DAMAGE+JOINrejoin hooks (vanilla SPECTATOR handles both). - Migration
Consumers that relied on dead players being ADVENTURE (e.g. to keep them visible as ghost-bodies, or to layer custom restrictions) must adapt. Sabocraft, for example, now flips spectators to ADVENTURE + invisibility itself for the duration of a meeting, then back to SPECTATOR — that policy moved out of the engine and into the mod.
- Changed
- 0.4.0 +mc26.1.2
- Added
Modular split (Phase B.1): the monolithic engine became seven separately-published modules —
conduit-core,conduit-render,conduit-world,conduit-spectator,conduit-fx,conduit-prefab,conduit-arena— plus aconduitaggregator BOM. - Added
Per-module
ModInitializers (Phase B.2). - Added
PlayerSessionManager(Phase C) — engine-level disconnect snapshots + reconnect routing with a configurable hold window, persisted across restart viaSavedData.
- Added
- 0.3.0 +mc26.1.2
- Added
Initial stable Minecraft 26.1.2 port of the pre-modular monolithic engine.
- Added