Kyokalith ⛏ Anti-X-Ray — cheaters tunnel to ore, mine stone
1.5.0Buried ores are decoys until first exposed. Beats X-Ray, freecam & seed maps. No packets, no scans.
1.5.0latestFix explosion mining tps dropAug 29, 2026 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: · 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. · 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. · 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.1.4.2Dirty position write-back moved off the ticking thread and batched into one transactionAug 14, 2026 Changed · Dirty position write-back moved off the ticking thread and batched into one transaction. A spark profile of a live 12-player server showed DirtyPositionStore.persist accounting for roughly 1% of server-thread wall time: every flush cycle opened a fresh JDBC connection per pending chunk and committed each one separately, so a cycle with a few hundred dirty chunks paid a few hundred file opens and a few hundred fsyncs — on the thread that ticks the world. The cycle now runs on the async scheduler (Schedulers.asyncTimer: runTaskTimerAsynchronously on Spigot/Paper, AsyncScheduler.runAtFixedRate on Folia) and writes the whole batch through a single connection and a single transaction. · Nothing about persistence guarantees was relaxed. A dirty flag that fails to land re-opens the cover-and-dig exploit, so the same data is still written on the same cadence — only the thread and the batching changed. A failed batch (e.g. SQLITE_BUSY) falls back to the original per-chunk path, preserving the "one bad chunk must not skip the rest of the cycle" guarantee that has held since 1.x; anything still failing is requeued for the next cycle. · Folia/threading safety. The flush task touches no Bukkit API. DirtyPositionStore's shared state was already built on concurrent collections; the write paths (flush, flushAll, clearEpoch) are now additionally serialized by an internal lock. clearEpoch matters specifically here: it and the flush used to be implicitly mutually exclusive because both ran on the main thread, and without the lock an in-flight batch could write back an epoch that NatureRevive had just cleared. The final flushAll in onDisable waits for an in-flight cycle, keeping PlugMan hot-swaps safe. · docs/CONFIG.md / docs/CONFIG.zh-TW.md: the dirty_flush_interval_ticks red-line note was rewritten to describe the new behaviour.1.4.1Another fix for notificationAug 7, 2026 Fixed · /kyo notify off (and editing config.yml directly) could fail to actually stop broadcasts. OreFindNotifyListener and /kyo notify <on|off> both went through plugin.config (Bukkit YamlConfiguration) independently. Consolidated into a single volatile var notifyOnOreFind on KyokalithPlugin: /kyo notify writes it (and still persists to config.yml), the listener reads it directly instead of touching the config map on every mining event. This closes a real class of latent bug — OreVeinResolver's cache already needed Collections.synchronizedMap for the same non-thread-safe-config-object reason — but an earlier draft of this note incorrectly asserted a Folia cross-region-thread race as the confirmed production root cause. That assertion was wrong and unverified: production runs plain Paper, not Folia, so that specific race cannot be what's happening there. The true root cause of the reported production failure is still being investigated; see docs/HANDOFF.md in the main Lycohinya repo for the current state of that investigation. This 1.4.1 change stands on its own as a correctness improvement regardless of that outcome.1.4.0Add optional admin notification on real ore findsAug 6, 2026 · When a player mines a real (non-decoy) ore block, Kyokalith can now broadcast it to every online player holding kyokalith.admin. Off by default — toggle at runtime with /kyo notify <on|off>, which writes notify_admins_on_ore_find to config.yml and takes effect immediately (the only config key in this plugin that doesn't need a restart). Implemented as a plugin-internal listener on its own OreCheckTriggerEvent at MONITOR priority with ignoreCancelled = true, so it observes only and never affects decoy resolution.1.3.5Silk Touch'd eligible ore no longer refuses to stackJul 25, 2026 EligibleOrePdc.tag() stamped a fresh random UUID into the ItemStack's PDC on every call, so any two otherwise-identical eligible ore items always had different meta and could never merge — each Silk Touch break sat in the inventory as its own 1-count stack. The per-item token_id is gone from the ItemStack; eligible ore now stacks normally whenever oreType/origin_world/origin_epoch match, matching the spec's "eligibility follows the ItemStack" model. token_id still exists as a debug-only column on the eligible_placed_ores DB row, generated fresh at placement time instead of carried from the item.1.3.4Mining Experience Triple Fix: Bounded Veins, Exposed-Ore Continuations, and TNT Drop ValidationJul 24, 2026 This update includes versions 1.3.2, 1.3.3, and 1.3.4. Kyokalith's anti-X-ray decoy model remains unchanged. These releases address three mining issues that legitimate players could encounter during normal gameplay. 1.3.2: Bounded Ore Veins vein_size now represents the actual number of blocks in a generated vein, from 1 to 32. Previous versions converted vein sizes into sphere radii. Several configured sizes produced the same large sphere, and neighboring candidates could connect into extremely long ore belts. This created a mining experience where veins were encountered infrequently, but could continue for hundreds of blocks once found. Veins are now generated as deterministic, six-direction connected shapes with a strict block-count limit. The same server salt, world, epoch, ore type, and coordinates always produce the same result. Additional generation fixes include: · Overlapping ore types are resolved as complete veins, preventing lower-priority veins from being reduced into one-block fragments. · Touching veins of the same ore type are suppressed deterministically, preventing multiple candidates from merging into long chains. · Ore height distributions can now use y_weight_points for piecewise-linear curves. · Existing configurations continue using the previous preferred_y triangular distribution unless the new curve is explicitly configured. Configuration loading has also been updated to safely distinguish older custom configurations from the new schema. Existing custom height ranges will not be silently mixed with new defaults. This version also fixes ores disappearing after non-solid blocks such as rails were removed. Exposure detection now uses Minecraft's actual block-occlusion rules instead of a small hardcoded list of transparent materials. 1.3.3: Natural Ores Continue Behind Cave Walls Version 1.3.2 could still make naturally visible ore appear as an isolated single block. When a player mined an ore already exposed on a cave wall, buried vanilla ore directly behind it could be treated as an unrelated decoy and replaced with stone. Visible world-generated ore now acts as a validated entrance. When the entrance is mined, Kyokalith uses the server's private salt to generate a small, connected continuation within that ore's configured vein-size range. This preserves the expected experience of following ore into a wall without relying on vanilla's original hidden vein shape, which can be predicted using the world seed. Continuations are strictly bounded: · Each continuation has a configured maximum size. · Previously locked blocks cannot open another continuation. · A vein cannot repeatedly extend itself as the player mines forward. · All affected loaded chunks are committed together. · If the result cannot be safely saved, the block break is cancelled instead of leaving an unresolved ore state. Synthetic Kyokalith veins are now locked as one complete shape. The previous moving 5×5×5 locking window has been removed. Generation cells were reduced from 16³ to 8³, and all 11 bundled ores were recalibrated around smaller veins with more frequent encounters. 1.3.4: TNT Drops Are Now Validated Explosions previously validated the newly exposed crater surface, but could miss buried decoys destroyed directly inside the explosion. This allowed a player to locate an ore using X-ray and destroy it with TNT before Kyokalith had a chance to determine whether that ore should exist. Kyokalith now validates the complete explosion before Bukkit creates item drops. The explosion process now: · Registers naturally exposed ore entrances affected by the event. · Resolves buried blocks destroyed by the explosion. · Resolves the newly exposed crater surface. · Converts failed decoys back into their base material before drops are generated. Valid ores continue dropping normally. All decisions from the same explosion are committed as one bounded operation. If validation fails, exceeds the safety limit, or cannot be persisted completely, the explosion is cancelled without leaving partial world changes. Explosions affecting more than 512 blocks are cancelled immediately. Kyokalith no longer truncates an oversized explosion and leaves an unpredictable subset of blocks behind. Mining Distribution After Recalibration Average distance before encountering a new vein in a 2×1 tunnel: · Common overworld ores: approximately 125 to 255 blocks · Diamond: approximately 434 blocks · Emerald: approximately 1,260 blocks · Nether quartz: approximately 69 blocks · Ancient debris near Y15: approximately 1,057 blocks Fixed 64³ samples remained within the configured density limits: · Overworld: 1,060 ore blocks, approximately 0.404% · Nether: 1,660 ore blocks, approximately 0.633% The intended result is smaller veins, clearer endpoints, and more consistent distances between discoveries without flooding the world with additional ore. Upgrade Notes The algorithm version has been increased. During upgrade, Kyokalith invalidates only previously stored materialized-position decisions that were generated by the older vein algorithm. The following data is preserved: · Server salt · Chunk epochs · Dirty positions · Eligible ore records · Suspended chunks Do not delete or reset your Kyokalith database. Full implementation details, benchmarks, automated test coverage, and runtime verification notes are available in the GitHub repository.1.2.0Folia SupportJul 17, 2026 Added · Folia support. folia-supported: true is declared, and every scheduled task now runs on the thread that owns the data it touches: block work on the owning region, /kyo giveeligible on the recipient's entity scheduler, and the dirty-position flush (which only writes SQLite) on the global region. Spigot and Paper behaviour is unchanged — the same code path resolves to Bukkit.getScheduler() there. The startup line reports which mode is active (scheduler: Folia regionized / Bukkit main thread). · Admin commands that read or write blocks (inspect, preview, sample, markeligible, resolve) now run on the thread that owns the target coordinate — inline when the current thread already owns it, scheduled onto the owning region otherwise. On Folia a console command runs on the global thread, which owns no blocks at all, so these would otherwise have thrown. Spigot and Paper always dispatch commands on the main thread, so the inline path is always taken there and replies — including over RCON — behave exactly as in 1.1.0. Known limit: on Folia, an RCON command that has to hop regions gets an empty RCON response (the response buffer is flushed when dispatch returns and cannot wait for another thread). Changed · Folia's scheduler API is compiled in a separate folia source set, not added to main. folia-api transitively exposes the whole Paper API, so putting it on main's compile classpath would have silently retired the [1.0.0] guarantee that Spigot compatibility is a compile-time fact. main still sees only spigot-api; the one file that calls Folia (FoliaSchedulers) is loaded only when running on Folia, and SchedulersSpigotSafetyTest fails if a Folia reference ever reaches the class Spigot does load. · database.dirty_flush_interval_ticks below 1 is now clamped to 1. Bukkit silently did this already; Folia rejects it outright, so the same config would have failed startup there. · api-version lowered from 26.2 to 26.1. Folia's newest release is 26.1.2 — no 26.2 build exists — and a server refuses to load a plugin whose api-version is above its own version, so this is the price of one jar running on all three platforms. The compile baseline is unchanged (spigot-api 26.2); on Spigot/Paper 26.2 the plugin now merely declares the older API level. Fixed · Dirty positions could be lost on Folia, which is an exploit problem rather than mere data loss — a dropped dirty flag makes a block a player covered up "first-exposure resolvable" again, reopening the cover-and-dig hole. Each chunk's position set was a plain HashSet, safe on Spigot only because markDirty and the flush task shared the main thread. Under regionized scheduling the flush runs on the global thread and encodes the set while a region thread is still adding to it, throwing ConcurrentModificationException mid-write. The per-chunk sets are now concurrent; DirtyPositionConcurrencyTest reproduces the original failure. Spigot and Paper were never affected. · A failed dirty-position flush no longer drops the chunk from the flush queue: the chunk is requeued for the next cycle and the failure is logged, and one chunk's failure no longer aborts the rest of the cycle. Relevant on Folia, where region threads and the global flush task can hit SQLite concurrently and collide with SQLITE_BUSY; previously the pending flag was consumed before the write, so a failed write meant those dirty positions were never retried — the same exploit-reopening loss the flush-interval note in CONFIG.md warns about. Folia support was contributed by RiceChen_ (RICE0707) in #1 — including the source-set isolation that keepsfolia-apioff the main compile classpath. Thank you!1.1.0Eligibility now includes already-exposed vanilla oreJul 15, 2026 · Previously, only ore Kyokalith itself materialized on first exposure (NATURAL_BLOCK) or moved through the placed-block token flow (PLACED_BLOCK) could fire OreCheckTriggerEvent. Ore that was already exposed at world generation — cave walls, ravine faces, i.e. most of what a player actually mines while exploring — almost never coincidentally matched the deterministic vein function, so it silently never fired the event. That exclusion caught zero cheaters (X-Ray gives no informational edge on ore that's already visible) while starving downstream reward plugins of legitimate check opportunities: a server measured zero triggers across ~200 ores mined in ordinary cave exploration. A new EligibilitySource.WORLDGEN_EXPOSED now covers this case — any real, currently-standing ore of an enabled type that isn't in a dirty position is eligible. Anti-X-Ray guarantees are unaffected: this only changes reward-check eligibility, not the decoy/materialization logic that decides what's real. See docs/API.md.1.0.0Jul 14, 2026
Similar projects
Plugin →Let Me Despawn
Improves performance by tweaking mob despawn rules. Say bye to pesky unintentional persistent mobs.
25MSueta
JourneyMap
Real-time map used for mapping in-game or your browser as you explore.
14.7MSueta
Emotecraft
Create your own emotes in Minecraft.
7.5MSueta
Simple Tpa
A simple tpa plugin which sends requests
5.4MSueta
Common Network
Forge and Fabric Unified Networking Solution
3.9MSueta
Set Home
A lightweight plugin for players to set their homes.
3.8MSueta