Skip to content

Toolkit (util package + services)

Navigation: Documentation Home · Table of Contents · API Overview · Module System · Glossary

QinhCoreLib (QCL) provides a full set of general-purpose utilities in the util package and its accompanying services. They cover high-frequency scenarios such as scheduling, effects, items, coordinates, server-side compatibility, text, holograms, and dynamic assembly. Sub-plugins can use them directly without reinventing the wheel.

This page is organized by utility class into sections, each providing a method table and typical-usage code examples. All code examples are copy-paste ready; identifiers are case-sensitive.

Global conventions (read first)

  1. Use Runnable / Supplier; do not pass Kotlin lambdas directly. The parameter types of QCL's scheduling APIs (TaskScheduler, ThrottledExecutor, etc.) are Java's Runnable and Supplier<T>. If you pass a bare lambda via SAM conversion directly from a Kotlin sub-plugin, a LinkageError may be triggered during cross-plugin class loading. Please wrap it explicitly as Runnable { ... } / Supplier { ... }, or pass lambdas normally in Java.

  2. Folia compatibility.TaskScheduler wraps the Bukkit scheduler and is Folia-compatible. Always schedule through TaskScheduler rather than calling Bukkit.getScheduler() yourself.

  3. MiniMessage support. The text classes (TextUtil, HologramManager, AssemblySystem, etc.) all support MiniMessage tags, legacy & color codes, § color codes, and plain text, automatically detecting and converting them to Component.

  4. Multi-name fallback.ServerCompat's sound(...) / particle(...) / material(...) / resolveSound / resolvePotionEffectType accept multiple candidate names, attempting to resolve them in order and matching the first valid one, which is more stable across versions.


TaskScheduler (scheduling)

Wraps the Bukkit scheduler and is Folia-compatible. All methods take Runnable / Supplier as parameters to avoid the LinkageError from Kotlin lambdas.

MethodDescription
runSync(Runnable)Execute immediately on the main thread
runSyncLater(delay, Runnable)Execute on the main thread after a delay of delay ticks
runSyncRepeating(delay, period, Runnable)Execute repeatedly on a timer on the main thread
runAsync(Runnable)Execute immediately on an async thread
runAsyncLater(delay, Runnable)Execute with a delay on an async thread
runAsyncRepeating(delay, period, Runnable)Execute repeatedly on a timer on an async thread
supplySync<T>(Supplier<T>): CompletableFuture<T>Fetch a value on the main thread, returning a CompletableFuture
supplyAsync<T>(Supplier<T>): CompletableFuture<T>Fetch a value on an async thread, returning a CompletableFuture

Typical usage

kotlin
// Execute immediately on the main thread
TaskScheduler.runSync(Runnable {
    player.sendMessage("Executed on the main thread")
})

// Execute after a delay of 20 ticks (1 second)
TaskScheduler.runSyncLater(20L, Runnable {
    player.health = player.maxHealthValue()
})

// Repeat every 5 ticks
val taskRef = TaskScheduler.runSyncRepeating(0L, 5L, Runnable {
    EffectUtils.spawnParticle(loc, Particle.HEART, 1)
})

// Compute asynchronously, then return to the main thread to use the result
TaskScheduler.supplyAsync(Supplier {
    heavyDatabaseQuery()  // async, time-consuming operation
}).thenAccept { result ->
    TaskScheduler.runSync(Runnable {
        player.sendMessage("Query result: $result")
    })
}

ThrottledExecutor (throttled executor)

Per-key throttling: the same key executes only once within throttleMs.

MethodDescription
ThrottledExecutor(throttleMs)Constructor; specifies the throttle interval (milliseconds)
execute(key, Runnable)Execute if that key is not within the throttle window
clear()Clear all throttle records
kotlin
val throttle = ThrottledExecutor(200L) // 200ms throttle

// Prevent spam in high-frequency events (such as movement)
throttle.execute(player.uniqueId.toString(), Runnable {
    player.sendActionBar("Moving…")
})

CooldownManager (cooldown management)

Per-key cooldown, commonly used for skill and interaction cooldowns.

MethodDescription
hasCooldown(key)Whether it is on cooldown
getRemaining(key)Remaining cooldown time
setCooldown(key, duration, unit)Set a cooldown
removeCooldown(key)Remove the cooldown for a key
clear()Clear all cooldowns
kotlin
val cd = CooldownManager()

val key = player.uniqueId.toString()
if (cd.hasCooldown(key)) {
    player.sendMessage("Still need to wait ${cd.getRemaining(key)}")
    return
}
cd.setCooldown(key, 3, TimeUnit.SECONDS)
// Cast the skill…

EffectUtils (effects)

A unified entry point for sounds and particles, with common presets (Presets) included.

MethodDescription
playSound(loc, ...)Play a sound at a coordinate
playSoundForPlayer(player, ...)Play a sound for that player only
playSoundForAll(...)Play a sound for the whole server
spawnParticle(loc, particle, count)Spawn particles at a coordinate
spawnParticleForPlayer(player, ...)Show particles for that player only
spawnColoredDust(loc, color, ...)Spawn colored dust particles
spawnCircle(...)Spawn particles in a circle
spawnHelix(...)Spawn particles in a helix
spawnLine(...)Spawn particles in a line

Presets

EffectUtils.Presets provides one-call scene effects, taking a coordinate loc as the parameter:

success(loc) / error(loc) / warning(loc) / info(loc) / click(loc) / plant(loc) / harvest(loc) / breakBlock(loc)

kotlin
// Success effect
EffectUtils.Presets.success(player.location)

// Circular particle ring
EffectUtils.spawnCircle(player.location, Particle.FLAME, /* radius, point count, etc. */)

// Play a sound for a single player only
EffectUtils.playSoundForPlayer(player, Sound.ENTITY_PLAYER_LEVELUP)

ItemUtils (item)

A convenient wrapper for item creation and editing.

MethodDescription
isEmpty(item) / isNotEmpty(item)Emptiness check (including AIR and null)
createItem(material, amount, name, lore, cmd)Create an item with name/lore/CustomModelData in one call
setDisplayName(item, name) / getDisplayName(item)Read/write the display name
setLore(item, lore) / getLore(item) / addLoreLine(item, line)Read/write and append lore
setCustomModelData(item, cmd) / getCustomModelData(item) / hasCustomModelData(item)Read/write and check CustomModelData
isSimilar(a, b) / compareWithNbt(a, b)Similarity comparison / comparison with NBT
clone(item, amount?)Clone an item (amount can be specified)
takeItem(item, amount)Decrement the amount
kotlin
val sword = ItemUtils.createItem(
    Material.DIAMOND_SWORD,
    1,
    "<gold>Blade of Qinhuai",   // MiniMessage supported
    listOf("<gray>A fine sword"),
    100100                      // CustomModelData
)

ItemUtils.addLoreLine(sword, "<yellow>Enhanced")

if (ItemUtils.isNotEmpty(sword) && ItemUtils.hasCustomModelData(sword)) {
    val cloned = ItemUtils.clone(sword, 2)
}

LocationUtils (location)

Coordinate serialization, computation, and range queries.

MethodDescription
serialize(loc) / deserialize(world, x, y, z, yaw, pitch)Coordinate serialization/deserialization
serializeList(...) / deserializeList(...)List serialization (semicolon-separated)
getBlockLocation(loc) / getCenterLocation(loc)Block coordinate / block center coordinate
distance(a, b) / distanceSquared(a, b)Distance / squared distance
isSameBlock(a, b)Whether it is the same block
getDirection(from, to) / lookAt(loc, target)Direction vector / face a target
getNearbyLocations(...) / getNearbyBlocks(...)Nearby coordinates / nearby blocks
isInRange(a, b, range)Whether it is within range
getChunkKey(loc)Chunk key
kotlin
val serialized = LocationUtils.serialize(player.location)
// Lists are semicolon-separated
val listStr = LocationUtils.serializeList(spawnPoints)

val center = LocationUtils.getCenterLocation(block.location)
if (LocationUtils.isInRange(player.location, target, 5.0)) {
    val dir = LocationUtils.getDirection(player.location, target)
}

ServerCompat (server-side compatibility)

A cross-platform, cross-version compatibility layer. Provides platform detection, version validation, and multi-name fallback resolution.

Method / memberDescription
detectPlatform() / platformLabel()Detect platform / platform label
bukkitVersionLabel() / parseBukkitVersion()Bukkit version label / parse
pluginVersion(plugin)Plugin version
validateServer(logger)Validate the server environment
validateJava(...)Validate Java (requires ≥ 25)
validateMinecraftVersion(...)Validate Minecraft (requires ≥ 1.21.11)
platformCurrent platform
supportsPluginLibrariesWhether plugin libraries are supported
supportsAsyncChatEventWhether async chat events are supported
resolveSound(...) / resolvePotionEffectType(...)Resolve sound / potion effect (multi-name fallback)
sound(vararg) / particle(vararg) / material(vararg)Multi-name fallback resolution of sound/particle/material
ATTR_MAX_HEALTH etc.Attribute constants
applyMaxStackSize(...)Apply the maximum stack size
playBlockStepEffect(...)Play a block step effect

Multi-name fallback

kotlin
// Material names may differ across versions; try in order and match the first valid one
val mat = ServerCompat.material("GRASS_BLOCK", "GRASS")
val snd = ServerCompat.sound("BLOCK_NOTE_BLOCK_PLING", "NOTE_PLING")
val particle = ServerCompat.particle("DUST", "REDSTONE")

// Startup-time environment validation
ServerCompat.validateServer(logger)   // Java ≥ 25, MC ≥ 1.21.11

TextUtil (text, singular)

Conversion of text to Component and colored output. toComponent automatically detects MiniMessage / legacy & codes / § codes / plain text.

MethodDescription
toComponent(text)Text → Component (auto format detection)
colored(text)Apply color
sendColored(target, text)Send a colored message
logColored(text)Colored logging
broadcastColored(text)Colored broadcast
applyItemDisplay(meta, name, lore)Apply name and lore to an ItemMeta
showColoredTitle(player, text, fadeIn, stay, fadeOut)Show a colored title

Extensions.kt extension functions

ExtensionDescription
String.toComponent()String → Component
String.colored()Apply color to a string
String.parseMiniMessage()Parse MiniMessage
Player.sendColoredMessage(text)Send a colored message to a player
String.replacePlaceholders(vararg pairs)Replace placeholders
Player.maxHealthValue()Get a player's maximum health
kotlin
import com.qinhuai.corelib.util.toComponent
import com.qinhuai.corelib.util.colored
import com.qinhuai.corelib.util.sendColoredMessage
import com.qinhuai.corelib.util.replacePlaceholders
import com.qinhuai.corelib.util.maxHealthValue

player.sendColoredMessage("<gold>Welcome back")

val title = "Hello %name%".replacePlaceholders("%name%" to player.name)
TextUtil.showColoredTitle(player, "<rainbow>Victory", 10, 40, 10)

val hp = player.maxHealthValue()

TextUtils (text utilities, plural)

Formatting and string-processing utilities.

MethodDescription
formatNumber(double, decimalPlaces) / formatNumber(int)Number formatting
formatTime(ms)Milliseconds → 1 hour 30 minutes 45 seconds
formatTimeCompact(ms)Milliseconds → compact time
joinList(list, sep, lastSep)Join a list (including a last-item separator)
capitalize(s) / toTitleCase(s)Capitalize first letter / title case
limitLength(s, n)Truncate length
stripColors(s)Strip color codes
countOccurrences(s, sub)Number of occurrences of a substring
levenshteinDistance(a, b)Edit distance
findSimilar(...)Fuzzy matching
kotlin
TextUtils.formatNumber(1234.5678, 2)  // "1234.57"
TextUtils.formatTime(5445000L)        // "1 hour 30 minutes 45 seconds"
TextUtils.joinList(listOf("A", "B", "C"), ", ", " and ")  // "A, B and C"

// Suggest a close match when a command is mistyped
val guess = TextUtils.findSimilar(input, validCommands)

HologramManager (hologram)

ArmorStand-based hologram text. Supports MiniMessage, with a line height of 0.25. When attached to an entity, it automatically rebinds and follows every second.

HologramManager methods

MethodDescription
create(id, location, vararg lines)Create a hologram at a coordinate
createAsPassenger(id, entity, offsetY, vararg lines)Create as an entity passenger (follows the entity)
get(id) / remove(id) / removeAll() / getAll()Get / remove / remove all / get all
showTemporary(location, text, durationTicks)Temporary hologram (auto-disappears when time is up)
showPlayerBubble(player, text, fadeIn, stay, fadeOut)Bubble above a player's head

Hologram instance methods

MethodDescription
setLine(index, text) / addLine(text) / removeLine(index)Line editing
getLines() / clearLines()Get all lines / clear
update()Refresh the display
setPassenger(entity, offsetY) / removePassenger()Bind/unbind an entity passenger
show() / hide()Show / hide
teleport(location)Teleport
delete()Delete
startRepairTask() / stopRepairTask()Start/stop the auto-rebind task
kotlin
val holo = HologramManager.create(
    "shop-1",
    shopLocation,
    "<gold>Qinhuai Shop",
    "<gray>Right-click to buy"
)
holo.addLine("<yellow>In stock")
holo.update()

// Bubble above the head
HologramManager.showPlayerBubble(player, "<aqua>Level up!", 5, 40, 10)

// Temporary prompt, disappears after 60 ticks
HologramManager.showTemporary(loc, "<green>+10 gold", 60)

// Follow an entity
val mobHolo = HologramManager.createAsPassenger("boss-hp", boss, 2.5, "<red>BOSS health")

AssemblySystem (dynamic assembly)

A layered dynamic item-display system: it splits the name, lore, etc. of the display into stackable DisplayLayers, applied in ascending priority order.

Core types

  • DisplayLayer interface: priority, apply(meta, AssemblyContext).
  • AssemblyContext(variables): carries the variable context.
  • ItemAssembly: addLayer(layer), apply(item, context): ItemStack.
  • Preset layers:
    • NameLayer(priority) { ctx -> String }
    • LoreLayer(priority) { ctx -> List<String> }
kotlin
val assembly = ItemAssembly()

assembly.addLayer(NameLayer(0) { ctx ->
    "<gold>${ctx.variables["itemName"]}"
})
assembly.addLayer(LoreLayer(10) { ctx ->
    listOf(
        "<gray>Level: ${ctx.variables["level"]}",
        "<yellow>Attack: ${ctx.variables["atk"]}"
    )
})

val context = AssemblyContext(mapOf(
    "itemName" to "Blade of Qinhuai",
    "level" to "5",
    "atk" to "120"
))

val finalItem: ItemStack = assembly.apply(baseItem, context)

Custom layer: implement the DisplayLayer interface, override priority and apply(meta, ctx), then addLayer it. All layers act on the same ItemMeta in ascending order of priority.


Continue reading