Welcome to the Hangar Open Beta. Please report any issue you encounter on GitHub!
Avatar for TinyYana

Buried ores are decoys until first exposed. Beats X-Ray, freecam & seed maps. No packets, no scans.

Report Kyokalith?

R

1.5.0

Fixed

  • Explosions (TNT, beds, end crystals) stalled the ticking thread for hundreds of milliseconds. Players reported that blast mining — the standard way to look for ancient debris — dropped TPS in whatever region it happened in. Reproduced on the test server: a 512-block-capped blast in solid netherrack at Y15 pushed the worst tick from a 2 ms idle baseline to 291–500 ms. With the plugin unloaded the same blast cost 3–18 ms, so all of it was Kyokalith. Three independent causes, each measured, none guessed:

    1. A fresh JDBC connection per block in the blast list. OreLifecycleListener.onEntityExplode / onBlockExplode called EligiblePlacedOreStore.remove() once per block, and every call opened a connection and ran a SELECT — unconditionally, against a table holding 4 rows on the live server. Measured 1.8–2.1 ms per block: 172 ms for an ~80-block bed blast, 919 ms at the 512-block cap. The same path also ran on every player ore break via OreEligibilityService.find, violating the "no DB I/O on the hot path" red line in KYOKALITH_SPEC.md §15.1. The table is now held entirely in memory (loaded once in onEnable, write-through afterwards), so lookups and no-op removes never reach SQLite.

    2. A separate transaction per vein hit. MaterializationService.resolveAndLock opened its own connection and committed on every hit. All locks produced by one event are now buffered in MaterializationLockBuffer and committed once at the end of it. The buffer reports not-yet-flushed locks to later lookups within the same event, so resolution results are identical to the old per-hit writes; the difference is that the batch is now all-or-nothing instead of leaving a half-committed prefix behind when a later write fails.

    3. Every connect().use { } closed the last connection, forcing SQLite to checkpoint the whole WAL. This was the one left after (1) and (2), and it was invisible in the first round of profiling because it does not scale with rows: a 10-row commit cost the same as a 4-row one. Kyokalith now holds one connection open for the plugin's lifetime purely so that a close is never the last one, and sets synchronous=NORMAL on each connection. Measured on a copy of the live database, on the drive the server actually runs on:

      10-row commit
      before 55–90 ms
      keep-alive that never ran a statement 55–87 ms
      keep-alive + one statement 23–25 ms
      synchronous=NORMAL alone 65 ms
      keep-alive + one statement + synchronous=NORMAL 6.5–11.6 ms

      Two things worth keeping: DriverManager.getConnection is lazy — a keep-alive connection that never executes a statement has not opened the database file and blocks nothing, which is why the first attempt at this fix measured as a complete no-op. And the two changes only work together: synchronous=NORMAL alone is pointless while every close still checkpoints.

    Result (same scenario, same server): the worst tick after a blast went from 291–500 ms to 13.9–48 ms in steady state. The first blast after a restart still costs about 110 ms while chunk caches and the WAL warm up. For reference, the same blast with Kyokalith unloaded costs 3–18 ms.

    Durability note. synchronous=NORMAL in WAL mode means a commit is no longer fsynced. It stays durable against a plugin or server crash — the case §9.4 is about — and only a power loss or OS crash can drop the last few seconds. What is at risk is materialized_positions, a derived cache of a pure function: a lost collectShape lock re-resolves to the same answer at that coordinate's own first exposure. Only a worldgen-continuation shape could come out differently, and never in a way that reveals a buried block early.

    No anti-x-ray behaviour changed. Unexposed positions still only ever get a database row, never a setBlock; the 512-block and 4096-row per-event caps are untouched; the "resolved but not persisted must never change a block" ordering of §9.4 is now stricter, not weaker, because the single flush happens before any setType in the event.

Changed

  • ancient_debris recalibrated against vanilla. Players reported netherite hunting felt nothing like vanilla, and the numbers agree. The shipped curve (peak 1.0 at Y15, background 0.65) had a peak-to-background ratio of 1.54, against roughly 21 in vanilla. Total yield was 2.07x vanilla, but smeared across 112 Y levels: the band players actually mine (Y824) held only 0.55x vanilla, and a single Y16 layer only 0.34x. The total was never the problem — the shape was. The curve now reproduces vanilla's two batches, a triangle over Y824 peaking at Y16 plus a much thinner uniform Y8~119 background:

    • preferred_y 15 → 16, cell_chance 0.045 → 0.125, y_weight_points background 0.65 → 0.05. density stays 1.0 (this config keeps density as the global abundance knob and expresses per-ore rarity through cell_chance).
    • Background weight 0.05 is vanilla's background layer density divided by its peak layer density (0.0054 / 0.1234); cell_chance then solves total = 56 * cell_chance * mean(weight) * E[vein_size] for vanilla's ~1.65 blocks per chunk. Source: https://minecraft.wiki/w/Ancient_Debris.
    • Measured over 1024 chunks x 4 salts: 1.65 blocks/chunk total (vanilla 1.65), 1.03 in Y824 (vanilla ~1.05), 0.118 at Y16 (vanilla ~0.123) — all within 5%. Mean tunnel spacing at Y15 improves from 1057 m to 426 m.
    • Deliberate tradeoff: matching vanilla makes Y25+ genuinely dry (mean tunnel spacing at Y60 goes from ~1.5 km to 6 km). That is what vanilla does — netherite is found at Y824 or not at all. The ancient_debris Y60 threshold in OreDistributionMetricsTest was relaxed accordingly, and the Y9/Y15/Y60 density band in NetherOreDensityMonteCarloTest split into three vanilla-referenced bands, because a single threshold across three Y levels that differ by an order of magnitude in vanilla cannot express the intent.
    • Existing locked positions are not re-resolved. materialized_positions rows decided under the old curve keep their answer; vein_algorithm_version is unchanged because the geometry did not change, only the config parameters. Re-deciding an already-decided coordinate would break the §9.4 invariant.
    • Servers upgrading must take the shipped config.yml — Bukkit's copyDefaults never overwrites keys that already exist on disk, so a stale config.yml keeps the flat curve.

    The two reports are the same activity seen from two sides: blast mining at Y15 is how players look for netherite, and it was both laggy and unrewarding.

Added

  • KyokalithDatabase.connectionsOpened, surfaced in /kyo stats as "DB connections opened". A per-coordinate connection on a hot path has now caused two separate incidents (1.4.2's dirty-position flush, and this one), so it is both observable in ops and pinned by a regression test: a 512-block explosion-sized block list with nothing placed must open zero connections.
  • A warning when resolving one explosion takes longer than 25 ms, naming the block count and location. Without it, "blast mining lags" is only ever a player report that has to be re-measured from scratch.

Information

Published
August 29, 2026
Author
1Downloads

Platforms

Paper
Paper
26.1–26.2