Changelog

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.

  1. 0.26.0 +mc26.2 2026-09-03
    • Fixed

      The entity freeze never froze a mob. RewindEntityTickMixin cancelled Entity.tick(), which Mob.tick() overrides and moves after the super call. It cancels ServerLevel.tickNonPassenger now, 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 commit and previewAt. 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 by onRoundStart; the match's phase, clock, roster and arena size rewind with the world; a player rewound to before their elimination leaves spectating. That is PAIRED-RULES row 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.setSpeed returns null or 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 a MOVEMENT_SPEED modifier; 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 the minecraft namespace) 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 status shows memory and capture cost.

    • Added

      Opt-in tape retention: rewind_tape_max_count and rewind_tape_max_mb, both 0 (never prune) by default.

    • Added

      Module master docs for core, render, rewind and fx; a rewind roadmap; 63 new game tests.

    • Changed

      RewindController.setSpeed returns String (the refusal reason) instead of void.

    • Changed

      RewindController.holdOperator(Recording, ServerPlayer) returns whether the operator is in the scene; previews never move a director.

    • Changed

      BlockJournal.EditVisitor and BlockEdit carry block-entity NBT before and after.

    • Changed

      PlayerPose carries modifiers; EntityLedger.captureFrame takes the keyframe interval and staggers per track.

    • Changed

      rewind_auto_record is still unread (roadmap Q1, pending).

  2. 0.25.0 +mc26.2 2026-08-29
    • Fixed

      A player given back on the death screen lost their entire inventory (D1). onInstanceReleased force-converts a DEFERRED record and release applies the restores; vanilla builds a fresh ServerPlayer on respawn and restoreFrom does not carry the inventory across unless the player is a spectator. Four subjects measured: SURVIVAL lost, ADVENTURE lost, SPECTATOR kept, control kept. DEFERRED exists 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). onReconnect re-captured through homeFor, four lines under a javadoc saying it never reads the returning body. The JOIN hook fires from inside placeNewPlayer before the player is in the list (inPlayerList=false, 7/7 joins), so homeOf read a null body and fell through to fallbackHome() 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). status printed the live body under a heading that reads as the engine's record, showing gamemode=ADVENTURE respawn=conduit:rt-1 forced=true while custody held SURVIVAL and an overworld point. Filed independently by both archetypes' auditors.

    • Added

      Custody.identityOf / destinationOf, and PlayerCustody.heldIdentity / heldDestination. Without them no diagnostic could read the record at all, which is why D3 could not be fixed at the command layer. status now 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, so REOPEN_DESTINATION had no witness. It is now caught by [TAKE, TAKE, TAKE, DISCONNECT, RECONNECT, GIVE] — the D2 scenario exactly.

  3. 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. ConduitCoreMod registers PlayerInventoryStash::discard on the leave fan-out; PlayerCustody restores the stash from a separate listener on the same Fabric DISCONNECT event. 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's isStashed guard 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. discard now asks a question instead of racing: PlayerInventoryStash.addRetentionPolicy lets an owner claim a player's stash, discard skips a claimed player in either order, and the owner releases it through discardRetained. PlayerCustody claims for the life of every custody record and releases from Custody.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 custody opened every one of its six executors with ServerPlayer p = src.getPlayer(); if (p == null) return 0; and the file contained zero sendFailure calls, so from the console or RCON it did nothing and said nothing. TESTING.md named 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 time PlayerCustody and Custody contained 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 under conduit.Custody: take, giveBack, giveBackAll, disconnect and rejoin at INFO, per-effect detail at DEBUG. TESTING.md now 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 silent return 0 and watching only the bare arm fail.

    • Added

      Custody is documented where builders read. Round 28's convergent finding was that grep -ci custody returned 0 on both archetype guides, MAKING-MODS.md, README.md and wiki/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. take before gather; leave the stash on; do not also call room.disperse, because custody queues its own return and disperse queues a second one to Prior, so two returns drain for one player; keep room.close(), which is the force-load ticket and not the return. That retires the dead-player PENDING_HOME branch 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-api so take and giveBack cannot change shape without failing the build until those pages are re-read.

    • Added

      Doc pair custody-diagnostics, 16 pairs now. checkEngineDocPairsTargeting rejected the first attempt because TESTING.md never named CustodyCommand, 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 unconditional discard and 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 from Custody.end. Deliberately not a planted Defect: it was one, and everyPlantedDefectIsCaught reported 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 models restoreStash faithfully: the binding no-ops when the body is gone, and modelling it as always succeeding hid that case. No existing ordering changed behaviour.

  4. 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 — afterTeleport fired, 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: executeTeleport asked four questions about the player and none about the destination, and DESTINATION_GONE was raised only inside the chunk gate that NO_PRELOAD skips. 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.json files hardcoded "minecraft": "~26.2" and "java": ">=25", and five mixin configs hardcoded compatibilityLevel: JAVA_25, which Mixin cannot set on a Java 21 JRE. Both are expanded per target now. assemble had been green throughout, because nothing ever ran that jar.

    • Added

      Player custody. me.zlex.conduit.custody.Custody (core, no Minecraft types, the state machine) and PlayerCustody (arena, the calls). A game writes take(player, instance) and giveBack(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 the SendHome reference class in PAIRED-RULES.md, which is kept deliberately because checkReferenceModsCompile and checkReferenceModBehaviour run it. Migration table at the top of that section; the honest gap list is in docs/ownership-lifecycle/NOT-SOLVED.md.

    • Added

      /conduit admin custodystatus, take, wreck, give, release, drop. Drives every exit path by hand on a dev server, printing gamemode, respawn and dimension before and after. runServer is re-enabled on conduit-arena as the host. See docs/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 ran assemble and 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.md describes both game lines, deriving the target table from the mc_<line>_fabric_api properties 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 from teleport: onReady fires anyway on gate timeout (so it means "we waited", not "the chunks are here"), and either callback can fire inline.

    • Fixed

      CONDUIT.md said SafeTeleport had four teleport overloads; it has five. The missing one is the 8-arg form carrying onDropped — the API limit 4 collapses into.

    • Fixed

      SafeTeleport: a teleport still waiting on chunks when the server stops now reports SERVER_STOPPING, not DESTINATION_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 on minecraft:the_nether — a level that is never released — reported DESTINATION_GONE at shutdown with the nether demonstrably alive. Each ChunkGate now carries its own shutdown handler. The public 5-arg whenChunksReady still routes both into one callback because it is given only one; use the new public 6-arg overload to tell them apart.

    • Fixed

      Two SafeTeleport javadoc sentences that the round-24 fix had falsified. The 8-arg teleport doc claimed a gate-held teleport fires nothing at shutdown; it fires.

    • Added

      DropReason.DESTINATION_GONE is 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). MeleeWeapon declares reach, sweep arc, attack cooldown, combo window, and a MeleeSwing sequence; MeleeCombat.install(WeaponResolver) intercepts AttackEntityCallback, returns FAIL for resolved weapons (owning combat), runs a forward cone-sweep, deals each combo swing's damage via hurtServer (i-frames cleared so a fast string lands), applies player-safe knockback, plays feedback, and fires a MeleeHit per target via onHit (the seam animations attach to). Any LivingEntity can fight: MeleeCombat.performSwing(level, attacker, weapon) runs the identical pipeline for AI opponents / scripted bosses / training bots (whiffs are audible, cooldown-gated), and comboState(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 freezes ADVANCE_WEATHER and forces clear weather, and when daylightCycle is false it now also pins the world to day — driving the vanilla time/weather commands (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 the ADVANCE_TIME gamerule so a filming world stays at a fixed time of day. New applyWorldRules applies spawning + daylight together on server start. (Record gains a field — a positional constructor break for the two internal consumers; Codec stays back-compatible.)

    • Added

      conduit-instance · RuntimeDimensionSpec.superflatWorld. A one-call factory for a superflat runtime dimension (overworld dimension type, classic flat ground) beside voidWorld, wired to VoidWorldHelper.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.createSuperflatGenerator builds a classic bedrock/dirt/grass-over-PLAINS FlatLevelSource (plus an overload taking a caller-chosen surface biome + FlatLayerInfo stack) — 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 (not Optional.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 by wallGap) and returns each lane slot's centre BlockPos so the caller spawns whatever it likes down the middle. The reusable geometry behind mob gauntlets / runways; stays block-agnostic like the rest of Regions.

    • 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 a Codec and a one-call install(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 MC 26.2, Fabric Loader 0.19.3, and Fabric API 0.154.2+26.2; every module's fabric.mod.json now depends minecraft ~26.2. Mapping deltas fixed during the port (kept here + in CONTRIBUTING.md for the next MC bump):

      • Per-colour block constants collapsed into ColorCollection accessors — Blocks.LIME_GLAZED_TERRACOTTABlocks.GLAZED_TERRACOTTA.lime(), Blocks.GRAY_CONCRETEBlocks.CONCRETE.gray(), Blocks.LIGHT_BLUE_STAINED_GLASSBlocks.STAINED_GLASS.lightBlue().
      • Copper collapsed into WeatheringCopperCollectionBlocks.COPPER_BLOCKBlocks.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)).
    • Changed

      A rewind restore no longer loads chunks on the tick thread — it waits for them. BlockJournal.restoreToSeq replays the block diff through Level.setBlock, and a setBlock into a non-resident chunk is the same managedBlock park as a cold getBlockState — 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 and commitAt truncation 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.floodFloor gains a non-blocking overload; the synchronous one is documented as blocking-when-cold. The fill probes every cell with getBlockState, so crossing into a non-resident chunk parked the server thread per cold chunk, up to maxCells times. The new floodFloor(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.spawnAround no longer loads chunks, and can place fewer mobs than asked. Resolving a spawn column meant reading blocks in it, and Level.getBlockState on a chunk that isn't resident is ChunkSource.getChunk(…, FULL, create = true) underneath — the server thread parked in managedBlock until 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: MobWave runs from the MobWaves END_SERVER_TICK sweep, and Cataclysm's Blood Moon calls spawnAround straight from its event tick, four picks per player every two seconds.

      spawnY now checks hasChunkAt before it reads anything and returns a new NOT_LOADED verdict; spawnAround responds 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.whenChunksReady treatment. 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() == n never had that guarantee (the cramped-tunnel case predates this) but are now much more likely to notice. spawnAt, execute and executeAll read 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.rain no longer loads chunks either, and can drop fewer blocks than asked. FallingBlockEntity.fall is a hidden block write — it clears the source position with Level.setBlock before adding the entity, and Level.setBlock goes through getChunkAt. rain schedules every block through Scheduler.runLater, so that write lands on a tick task, at a polar offset up to MAX_RADIUS (128 blocks, 8 chunks) and an absolute spawnHeight unrelated to any player. spawnOne now skips a block whose column isn't loaded. The check is at spawn time rather than at pick time deliberately: the SPREAD_TICKS stagger puts up to two seconds between the two, and a chunk can unload inside that window.

  5. 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 / StructureLocator answer the same question with Structure.findValidGenerationPoint, which is what StructureCheck.canCreateStructure itself 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.VillageSearch is deleted. Use me.zlex.conduit.world.structure.StructureLocator, which is generic over a structure TagKey. WorldSetup.findVillage / cancelVillageSearch / VILLAGE_SEARCH_RINGS are 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 structure TagKey rather than hardcoded to villages.

      The village search was slow because it was searching. It walked a grid asking StructureManager.checkStructurePresence per 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 /locate was 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.getPotentialStructureChunk is integer maths over a LegacyRandomSource. One candidate per spacing-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 by spacing - 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.findValidGenerationPoint is noise maths: one terrain column and one MultiNoiseBiomeSource sample. No chunk, no disk, no chunk system.

      Stage 3 is the whole answer, not a shortlist to confirm later. StructureCheck.canCreateStructure — the predicate behind checkStructurePresence's CHUNK_LOAD_NEEDED — is literally findValidGenerationPoint(ctx).isPresent(), and ChunkGenerator.createStructures gates on the same call when a chunk really generates. Vanilla's chunk load in getStructureGeneratingAt exists only to re-read the start and bump its explorer-map reference count; the start's ChunkPos is the candidate chunk stage 1 already computed.

      StructureScan also 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 getNoiseBiome 8 us and getFirstFreeHeight 2,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 assembled StructureStart — what the game actually places) and StructureCheck.checkStart against a storage reporting every chunk absent (what /locate asks 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.

      StructureScan has no Conduit imports, no Fabric imports and no ServerLevel; its Context record is the guarantee that nothing reachable from it can load a chunk. That is also what lets the structure-finder tool (~/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.

      StructureLocator is 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.

      StructureScan landed additive, beside a still-working VillageSearch. The swap and the deletion are the Changed entries above, in this same release, so the two mechanisms never coexisted in a shipped version.

  6. 0.21.0 +mc26.1.2 2026-08-19
    • Fixed

      Village start never found a village. VillageSearch 0.19.0 fixed the 60-second tick hang by slicing the walk, but the slices were the wrong unit: each one was a full StructureManager.checkStructurePresence, and for a village — a JigsawStructure — that assembles the entire village piece layout before the biome test is applied (findValidGenerationPoint is findGenerationPoint(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_NEEDED is returned on exactly one path in StructureCheck.checkStartcanCreateStructure came back true — which is the same verdict ChunkGenerator.createStructures will 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.findVillage is 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.

      blind disables 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 as VillageSearch.find(level, rings, biomePrefilter, useCache).

  7. 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.getChunkFuture managedBlocks the server thread despite its name, and one cold STRUCTURE_STARTS request cost 1,183 ms inside a single tick; one StructureManager.checkStructurePresence costs 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. checkStructurePresence runs per structure as placement and biome maths with no chunk: START_NOT_PRESENT is free, and START_PRESENT (already-generated terrain) is a hit with no chunk load at all. Only CHUNK_LOAD_NEEDED needs a chunk, and it goes through a new ServerChunkCacheAccessor @Invoker onto getChunkFutureMainThread, the private method underneath getChunkFuture without the managedBlock, 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 one checkStructurePresence rather than one cell, so the 2 ms budget can stop anywhere.

      It mirrors getStructureGeneratingAt exactly, including reading the locate position from StructureStart.getChunkPos on the chunk path, and was verified against vanilla: both agree on village (-144, -832) for seed 12345.

      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, blocking14,067 ms in ONE tick; RCON dead for 14.3 s
      before, radius 100, as shipped60 s, watchdog kill (the crash report)
      after, 6 rings / 169 cells / 507 steps / 169 chunk requests50 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: asking InstanceManager for seed 999 produced a level reporting levelSeed=-567870618070133791, the host world's, and so did asking for 424242. 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 to BiomeManager; the seed worldgen actually runs on is read back out of the level by ChunkMap's constructor via level.getSeed(), which is hard-wired to the server's own world. RuntimeLevelSeeds therefore records the seed under the dimension key before the level is constructed (ChunkMap asks during construction, and the Level superclass has stored the key by then), and ServerLevelSeedMixin answers getSeed() from it. A level Conduit did not create is not in the map and keeps vanilla behaviour untouched. The biome-zoom argument is now BiomeManager.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.Overworld built its generator from overworld.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. OverworldTerrain builds the vanilla definition instead (multi-noise biome source over the overworld noise settings). A game that genuinely wants the host's terrain rules still has RuntimeDimensionSpec.copyOf.

    • Fixed

      SafeTeleport never blocks a tick to load its destination. preloadChunks was a loop of getChunk(..., FULL, true), which parks the server thread in managedBlock until 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. whenChunksReady requests 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 instance gives 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>

      probe is 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 with level.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. ChunkSweep is 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.CustomHubBuilder gains an explicit done signal and holds back the post-build work (roomBuilt, the pad-overlap self-check, GameRegistry.markHubBuilt) until it fires. The old BiConsumer overload still works and completes immediately.

  8. 0.19.0 +mc26.1.2 2026-08-18
    • Fixed

      The village search stopped blocking the tick. 0.18.0's WorldSetup.findVillage ran ChunkGenerator.findNearestMapStructure inline 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 a STRUCTURE_STARTS chunk through managedBlock. 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.

      VillageSearch walks 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-0 findNearestMapStructure whose origin is that cell, which is the finest granularity the public API allows. Worst-case tick cost is therefore one chunk's STRUCTURE_STARTS generation. 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=true is not the cheap first pass it sounds like. It is the explorer-map flag and does the opposite: in getStructureGeneratingAt a true value skips the early return on START_PRESENT and forces level.getChunk(STRUCTURE_STARTS) even for chunks already on disk. false, used here, is the cheap path.

    • Fixed

      BREAKING: WorldSetup.findVillage now returns CompletableFuture<Optional<BlockPos>>, and VILLAGE_SEARCH_RADIUS is now VILLAGE_SEARCH_RINGS.

  9. 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 in conduit-world since the modular split, and WorldSettingsScreen renders it on the legacy flat theme. The half every consumer re-implemented was the application logic: which WorldTemplate a settings bag asks for, how a typed seed becomes a long, which gamerules to push, and how to find the nearest village. WorldSetup packages that, plus the VanillaConfigMenu rows 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.

  10. 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 same if (players.size() < 2) refuse gate, so nobody could exercise a game without finding a second human. The minimum is now declared once, on LobbyConfig, and enforced through SoloMode.canStart. One server-side switch — solo_test in conduit.toml, the Solo test rounds toggle in /conduit settings, or /conduit solo on — lowers it to one player for every registered game at once. Nothing about it is silent: SoloMode.announce paints a red banner for every participant, sidebarNote() gives the HUD a persistent marker, GameRegistry.updateStatus tags the hub pad, and LobbyMenu tags 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 as BaseLobby.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 call BaseLobby.beginRound(min) at round start to record the round's size.

    • Added

      LobbyMenu — the one lobby-setup surface. Six games each hand-assembled the same VanillaConfigMenu.openTabbed call with their own footer labels and their own "waiting for host" title. LobbyMenu fixes the shape: Setup / Tuning / Arena tab names (LobbyMenu.Tabs), a footer that is always host-gated Start Game then always-available Leave Game, a shared showWaiting card, and the solo tag on the title.

    • Added

      Difficulty — the shared Easy / Normal / Hard / Insane / Custom vocabulary, plus indexOf / isCustom / isBundle. Five games declared byte-identical copies of this list and their own index helpers.

    • Added

      LobbyConfig.zoneId — a game may pin the HubZone id its pad claims instead of taking the derived "<game-id>:main". This is what lets a shipped game move onto GameRegistry.register without changing the id its hub, marker pipeline and saved state key off. GameRegistry also gained zoneIdOf, byZoneId, and a duplicate-zone-id guard.

  11. 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 at PAD_PLATFORM_HALF = 4 — a 9×9 axis-aligned trigger box, in eleven places in HubManager. 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)size is the full width / diameter in blocks, so square(5) and circle(5) are both 5 across. boundsAt(centre) produces the trigger box; at HubManager.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 with DEFAULT_FOOTPRINT, so every existing call site keeps its exact geometry.
      • HubManager.getZoneFootprint(zoneId) and zoneContains(zoneId, point) — shape-aware companions to getZoneAabb, 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) — sealed SQUARE / CIRCLE, the horizontal cross-section of a ZoneDef. SQUARE.contains is literally AABB.contains; CIRCLE is 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 its aabb(), 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 of SERVER_STARTED after 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). ServerLabelManager already 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. getZoneLabelId is the companion to getZoneAabb that turns a zone id into that widget id. Together they let a hub self-check assert that updateZoneStatus really repainted a pad, rather than assume the call landed.

    • Changed

      ZoneDef gained a shape component (third of four). The previous three-argument constructor is retained and defaults to ZoneShape.SQUARE, so existing call sites compile and behave identically. ZoneManager.zoneAt now asks ZoneDef.contains, which for a square zone is the same aabb().contains(pos) call it made before.

  12. 0.15.0 +mc26.1.2 2026-08-18
    • Fixed

      A custom hub's pads had no runtime API. setCustomBuilder skipped the engine's own pad-placement path, which is the only thing that fills HubManager's private built map, so updateZoneStatus, updateZoneLabel and getZoneAabb all silently no-opped and holograms froze on their initial status while matches ran. HubManager.registerCustomPad lets a builder hand each placed pad back to the engine, running the same registerPad code the arc build uses, so the hologram, hovering icon, trigger AABB and retained ZoneDef are built identically and torn down identically by clearPads / SERVER_STOPPING.

    • Fixed

      registerPad's AABB is anchored to the pad's own Y instead of ROOM_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.

  13. 0.14.0 +mc26.1.2 2026-07-16
    • Fixed

      Underground spawns were delivered to the roof of the world. MobSpawns.spawnAround resolved the motion-blocking heightmap unconditionally, so center.y only 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. BloodMoonEvent uses the same call, so the horde did it in caves too. spawnY now hunts outward from centerY for 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 returns NO_SPOT and 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.

  14. 0.13.1 +mc26.1.2 2026-07-16
    • Fixed

      A buffed mob lost its original team for good. joinGlowTeam called addPlayerToTeam unconditionally, which silently reassigns an entity that already belongs to a team, and remove() 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.

  15. 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 through MobBuff and downstream mods had to hand-roll scoreboard teams around the engine. apply() now joins a conduit_glow_<colour> team that carries nothing but the colour and remove() 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. leaveGlowTeam only unteams a glow team, so it will not strip an entity off a team someone else put it on. Non-colour formattings (BOLD and friends) fall back to plain white rather than making a junk team. glowing() with no argument keeps vanilla white.

    • Changed

      MobBuff.elite() and boss() 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". ⚠️ SCALE moves the hitbox too, so a +30% zombie no longer fits a 2-block corridor.

  16. 0.12.0 +mc26.1.2 2026-06-18
    • Added
      conduit-mob

      EntityReg — custom-EntityType registration glue. register(namespace, path, EntityType.Builder) builds against the ResourceKey and registers into BuiltInRegistries.ENTITY_TYPE; attributes(EntityType<? extends LivingEntity>, AttributeSupplier.Builder) wraps FabricDefaultAttributeRegistry.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-mob

      Forces — 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 to MAX_RADIUS (96), entities capped at MAX_ENTITIES (512); velocities flagged dirty so player knockback resyncs.

    • Added
      conduit-mob

      Projectiles — observe/clone projectiles without a mixin. onProjectileSpawn(BiConsumer<ServerLevel, Projectile>) registers a ServerEntityEvents.ENTITY_LOAD listener filtered to Projectile; cloneWithSpread(Projectile, double spreadDegrees) spawns a same-type copy with yaw-rotated velocity (triple-shot fan-out).

    • Added
      conduit-mob

      StatusEffects — custom-MobEffect registration + contagion. register(namespace, path, MobEffect)Holder<MobEffect> into BuiltInRegistries.MOB_EFFECT; spreadOnContact(level, carrier, effect, radius, durationTicks, amplifier) applies the effect to nearby living entities (Ebola spread). Caller ticks it; capped at MAX_SPREAD_TARGETS (256).

    • Added
      conduit-mob

      Debrisrain(level, center, radius, count, palette, spawnHeight, explodeOnImpact) drops Scheduler-staggered FallingBlockEntitys of random palette blocks over a disc, optional anvil-style impact damage. Count capped at MAX_COUNT (512). Meteor / volcano / falling-trees reuse this.

    • Added
      conduit-fx

      Atmosphere — environmental mood effects, self-restoring via Scheduler. 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.

  17. 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 a LivingEntity's own travel() would otherwise add by integrating deltaMovement on top of the setPos. Pair it with noPhysics + noGravity + updateInterval(1) for smooth, client-interpolated flight.

    • Fixed

      The Ufo abduction 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.

  18. 0.10.22 +mc26.1.2 2026-06-17
    • Fixed

      Ufo ship didn't move / captive got stuck — moving a Display via the InterpolationHandler.interpolateTo didn't reliably advance the entity, so the saucer hovered in place and never flew off. Movement now uses setPos directly (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.

  19. 0.10.21 +mc26.1.2 2026-06-17
    • Fixed

      Ufo body 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.

  20. 0.10.20 +mc26.1.2 2026-06-17
    • Fixed

      Ufo motion was jittery — the saucer hard-teleported each update (blocks visibly vanished + reappeared as it rose) because Display entities don't interpolate position by default. Movement now routes through the 26.x InterpolationHandler (getInterpolation().setInterpolationLength + interpolateTo) so the client smoothly slews the ship and beam between updates; the captive moves via setPos (not a hard snapTo) for the same reason. Descend/ascend sped up a little and beam particles trimmed for lighter clients.

  21. 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 vanilla Display entities — 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 single Scheduler task drives a descend → beam → abduct → ascend → depart state machine. The saucer spins (each part bakes its offset/scale/spin into a Transformation the client tweens between updates), drops a glowing beam + END_ROD particle 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.

  22. 0.10.18 +mc26.1.2 2026-06-16
    • Added

      MobBadge state accessorsdisplayInWorld() and currentText() 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.

  23. 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 to MobBuffs' permanent setBaseValue scaling): stable-id AttributeModifiers applied via addOrUpdateTransientModifier (idempotent — re-apply updates in place, never stacks) and stripped cleanly by remove. 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 billboard TextDisplay re-positioned each tick, showing the name, a live unicode health bar (green→yellow→red by %) with cur/max, and the buff's ability lines, auto-discarded when the mob dies. MobAnimations — general Display-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.

  24. 0.10.16 +mc26.1.2 2026-06-16
    • Changed

      Mob-feature hardening pass. Scene.instantiate now isolates per-element failures — a single bad id (a typo'd mob-spawn entity, an unknown block in a block-list/volume) is skipped-and-logged instead of aborting the whole prefab and leaving a half-built scene. MobSpawns.execute clamps authored count (≤256) and radius (≤128) and preserves the authored Y (an elevated-platform spawn spawns on the platform, not the terrain below). MobWave gained an absolute lifetime backstop (30 min) so a wave left fully unbounded can't leak a forever-spawning loop, and conduit-mob now drops all active waves on SERVER_STOPPED so the static registry can't carry stale waves into the next world in a persistent JVM.

  25. 0.10.15 +mc26.1.2 2026-06-16
    • Added

      MobWave controller (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 single MobWaves driver (one server-tick listener, CopyOnWriteArrayList-backed, wrapped in TickBudget) advances every active wave and drops finished ones, so waves don't each register their own hook.

  26. 0.10.14 +mc26.1.2 2026-06-16
    • Added

      conduit-mob module — 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. MobBuffs scales attributes (scaleHealth tops 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 — plus equip(mob, slot, stack) with drop chance pinned to zero. Spawns use EntitySpawnReason.COMMAND by default (deliberate, not environmental). A capped/interval-paced MobWave controller and GeckoLib-aware custom-entity registration glue are planned follow-ups (see docs/MOB-FEATURES-PLAN.md).

    • Added

      mob-spawn prefab element — a named, authorable spawn point. Like Zone it is a descriptor, not an action: instantiating the prefab spawns nothing, Scene.mobSpawns() exposes the resolved MobSpawnDef (entity type, count, world centre, scatter radius) and the consuming game — or conduit-mob's MobSpawns.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.

  27. 0.10.13 +mc26.1.2 2026-06-08
    • Added

      conduit-rewind module — 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.setBlockState mixin) restored by minimal diff; entity ledger (spawn/remove + spawn NBT + transform samples + state keyframes) via the modern ValueInput/ValueOutput NBT system. Pause freezes entity ticking (mixin) + locks players + shows a PAUSED overlay; a boss-bar scrub timeline tracks the preview head. SnapshotSlice SPI rewinds game state — arena ships MatchRewind.bind so Match scores 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 by conduit.rewind.control). Coverage is full up to rewind_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 from Match.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 over ServerLabelManager (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 Display entities; 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.buildArcLobby now force-loads the room chunks (so the displays tick and animate from the first join) and sweeps any pre-existing BlockDisplay/ItemDisplay in the room before spawning fresh ones.

  28. 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

      CommandReflector permission 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.

  29. 0.10.9 +mc26.1.2 2026-06-05
    • Added

      Zones (core) — named AABB trigger regions firing ZoneEnterEvent/ZoneLeaveEvent on the bus; kills hand-rolled "is the player on the pad" polling.

    • Added

      ItemBuilder (core) — fluent ItemStack (Text-markup name/lore, glow, count).

    • Added

      FxPresets (fx) — confetti / winBurst / ringPulse particle choreography.

  30. 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 via GameRuleEnforcer.

    • Added

      Sequence (core) — TaskChain-style run/delay/waitUntil/loop over the scheduler.

    • Added

      Leaderboard (fx) — top-N holographic board over ProfileStore counters (offline-safe names; the engine now records each player's name on join).

  31. 0.10.7 +mc26.1.2 2026-06-05
    • Added

      Event bus (core, me.zlex.conduit.event) — Minestom-style EventNode tree, cancellable + prioritized events, @Subscribe auto-listeners (explicit Listeners.register + conduit:listeners entrypoint), fault-isolated dispatch. Bridges Fabric server events (damage/join/leave/tick).

    • Added

      Declarative commands (core, me.zlex.conduit.command) — @Command/@Arg/ @Suggests reflector 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 → vanilla Component (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.

  32. 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 new HubManager.zones(); custom builders set the spawn through the new HubManager.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.celebrate previously 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.

  33. 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 the Metrics registry. Op-gated.

    • Added

      /conduit leaderboard <stat> [count] — prints the top-N players by any ProfileStore counter (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.)

  34. 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 register onLeave(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. New ConduitFxMod entrypoint (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" when wins >= 1), persisted as ach.<id> attributes so they never re-fire. Default gold-toast presentation via AchievementToast (conduit-fx), wired through a callback handoff.
      • MatchResult + MatchHistorySavedData (conduit-arena) — snapshot a finished Match (winner/standings/points), commitTo() folds wins/games_played/points_total into ProfileStore, and a capped ring buffer keeps the last 50 results queryable. Match.finish(gameId).
      • PhaseFlow timeouts + PhaseWatchdog + TimedPhaseFlow (conduit-arena) — per-phase deadlines that auto-advance a stuck phase (AFK host, missed transitionTo) and log/alert on recovery. PhaseMachine.ticksInCurrentPhase().
      • GuardedDispatch + FaultPolicy (conduit-arena) — exception-isolation barrier around game-supplied callbacks so one throwing onTick/phase callback can't kill the server tick or wedge a half-applied phase. Game.faultPolicy() (default LOG_ONLY, backward-compatible 8-arg ctor).
      • fx choreography layer (conduit-fx) — Particles (burst/ring/puff + elimination/safe macros), FxTimeline (self-ticking at/after/repeat sequencer), EffectThrottle (per-key cooldown gate), ScoreBoardBinding (generic standings → Sidebar) + arena-side MatchScoreboard adapter.
      • 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, SpectatorManager maps (→ ConcurrentHashMap); the Sidebar.show subList aliasing bug.

    • Added

      Jar-bundled arena presets (conduit-world). ArenaStore.load now falls back to a classpath resource at /conduit/arenas/<gameId>/<preset>.json when no game-dir file exists, so a published mod can ship its authored arena inside its own jar (under src/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 bundled default so 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 optional spawns field on ArenaSnapshot, so older preset files still load), and ArenaSnapshot.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 .schem schematic import/export (conduit-world). New SchematicIo reads [Sponge Schematic](https://github.com/SpongePowered/Schematic-Specification) files (gzip NBT) from <gameDir>/conduit/schematics/ and bridges them to ArenaSnapshot, plus a /schem subtree (gated on CAP_EDITOR):

      • /schem list — list available .schem files.
      • /schem paste <name> — paste a schematic at the player's pos1 (else their feet), pushed through the /undo stack. 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 (nested Schematic.Blocks); the varint block-index stream (YZX order) is decoded in-house and palette strings resolve via BlockStateParser. 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 on CAP_EDITOR.

    • Fixed

      WorldBlockDisplay block/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 (ClientBlockDisplayItemStackRenderState.submit(...) inside the LevelRenderEvents.COLLECT_SUBMITS hook) cached the resolved ItemStackRenderState once on the first frame; that snapshot could be taken before the item-model atlas finished baking (the mc.level != null guard 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

      WorldBlockDisplay is now entity-backed (conduit-render). Instead of a client-only overlay driven by ShowBlockDisplayPayload, displays now spawn real vanilla Display.BlockDisplay (block form) / Display.ItemDisplay (item form) entities server-side: non-interactive (no gravity/collision, invulnerable, full-brightness), centred on the anchor, with the BlockDisplaySpec mapped 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. rotationDegreesPerSecond is 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 / hide now spawn a single shared world entity (visible to everyone in the dimension), not a per-player overlay; the ServerPlayer argument 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 old ShowBlockDisplayPayload / HideBlockDisplayPayload and the client ClientBlockDisplay* overlay are no longer on the active path.
    • Added

      VanillaConfigMenu — customizable footer. New open / openTabbed overloads take a Footer (or a doneLabel + onDone shorthand): a primary button whose label/action you choose (e.g. "Start Game"), an optional secondary button beside it (e.g. "Leave Game"), and a large mode 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 new BaseLobby.leaveHandler the 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 new ConduitPermissions.CAP_EDITOR) + a /cedit command tree (set/walls/replace/copy/paste/undo/pos1/pos2/sel/wand) over the selection, built on Regions, with a per-player clipboard, undo stack, and a particle selection outline.
      • ArenaSnapshot (capture/paste a region; BlockState.CODEC JSON) + ArenaStore — named arena presets per game id, persisted under <gameDir>/conduit/arenas/<gameId>/<preset>.json (survives restart), reserved default preset.
      • 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.)_
  35. 0.10.0 +mc26.1.2
    • Added

      VanillaConfigMenu (conduit-render) — declare a title + ordered list of ConfigEntry, pass an onChange callback, call open(); 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

      ConfigEntry sealed types — Toggle, Cycle, Stepper, Slider, Action, Text, each immutable with typed with* helpers the framework calls for you.

    • Added

      Real slidersConfigEntry.Slider + VanillaSliderElement + Widgets.vanillaSlider: a recessed track with a raised handle. Click routing goes through the new positional ServerScreenManager.onButtonAt handler, which carries the click's screen-UV (ButtonClickPayload gained clickU/clickV) so the server resolves the track fraction. Click-to-position (an in-world screen emits a click, not mouse-move), snapped to step.

    • Added

      Stateful vanilla controlsWidgets.vanillaToggle / vanillaCycle / vanillaStepper, the Vanilla*Widget records, and the ScreenLayoutCompiler branches that drive them.

    • Changed

      Stateless button chrome is now textured. Buttons whose look never varies with state — Cycle, the Stepper [−]/[+], 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-index RectElement z 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 tight 0.0005 apart so a rear layer (the black outline) no longer parallax-leaks past the fill on an off-axis screen.

  36. 0.9.0 +mc26.1.2
    • Added

      Developer-velocity pass — four reusable primitives so new mini-games stop re-implementing the same scaffolding:

      • SafeTeleport now preloads the destination chunks (to ChunkStatus.FULL) before the deferred teleport fires, fixing the "land in darkness / fall through unloaded terrain" stutter that every cross-area teleport hit. New overload teleport(player, level, pos, yaw, pitch, preloadRadius, after) and a public SafeTeleport.preloadChunks(level, pos, radius). Default radius is 1 (3×3 chunks); pass SafeTeleport.NO_PRELOAD to skip.
      • conduit-fx Titlesshow / actionBar / clear for single players or groups, dedup'ing the three-packet animation→subtitle→title dance every mod hand-rolled. Ticks for timing; sensible defaults.
      • conduit-world Regions — axis-aligned box fill / fillHollow / fillWalls / replace. The geometry every prefab-built arena re-wrote; caller still picks the BlockState so the engine stays game-agnostic.
      • conduit-core ConduitTest — a solo-testing harness: an op-gated test 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-core GatherRoom — 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 on disperse. Both directions route through SafeTeleport. Geometry stays the caller's job.
  37. 0.8.0 +mc26.1.2
    • Added

      RuntimeDimensionSpec data-driven factories: fromStem(stem, seed, persist), fromStemJson(server, json, seed, persist) (decode a LevelStem from JSON via LevelStem.CODEC + registry-aware RegistryOps), and fromDatapackStem(server, stemKey, seed, persist) (reference a datapack-registered dimension).

    • Changed

      conduit-arena's InstanceManager is now on-demand + handle-based, built on conduit-instance's RuntimeDimensions instead of a fixed 8-slot pool:

      • No POOL_SIZE cap — acquire creates a fresh conduit:rt-<n> dimension each time; instances are addressed by dimension key, not slot index.
      • GameInstance.slot()handle() (a RuntimeDimensionHandle); 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 reattach GameInstance wrappers on boot.
    • 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.getSeedOverrideRuntimeDimensions passes the seed to the ServerLevel constructor directly, so the seed-override mixin is no longer needed.

    • Removed

      The 8 data/conduit/dimension/instance-*.json datapack dimensions — instances are created at runtime now, not pre-declared.

    • Migration

      Consumers calling recordDisconnect with an int slot pass the instance's dimension-key string instead ("" for no instance). Reading GameInstance.slot() becomes handle() / 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.

  38. 0.7.0 +mc26.1.2
    • Added

      Persistent runtime worlds in conduit-instance. A persist=true RuntimeDimensionSpec is now recorded to a manifest (<world>/conduit_runtime_dimensions.json, its LevelStem serialized via LevelStem.CODEC + registry-aware RegistryOps) and recreated on the next server start, loading its existing region data. v0.6 silently deleted these on boot.

    • Changed

      persist now means restart survival, not release survival. release always fully destroys (evacuate → unload → delete files → drop manifest entry), regardless of persist. 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 every rt-* 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.

  39. 0.6.0 +mc26.1.2
    • Added

      conduit-instance module — on-demand runtime dimensions. Create a fresh world per match and tear it down when the match ends, instead of claiming from conduit-arena's fixed 8-slot pool.

      • RuntimeDimensions.create(server, spec) / .release(server, handle).
      • RuntimeDimensionSpec with voidWorld(server) + copyOf(template) builders and a persist flag (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 on SERVER_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.

  40. 0.5.0 +mc26.1.2
    • Changed

      SpectatorManager now uses vanilla GameType.SPECTATOR instead of the old ADVENTURE + flight + infinite-invisibility + scoreboard-team model. Breaking: makeSpectator / release keep 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_NAME constant.

    • Removed

      Internal applyAbilities / ensureTeam helpers + the seeFriendlyInvisibles / collisionRule scoreboard team.

    • Removed

      The ServerLivingEntityEvents.ALLOW_DAMAGE + JOIN rejoin 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.

  41. 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 a conduit aggregator 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 via SavedData.

  42. 0.3.0 +mc26.1.2
    • Added

      Initial stable Minecraft 26.1.2 port of the pre-modular monolithic engine.