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)
Use
Runnable/Supplier; do not pass Kotlin lambdas directly. The parameter types of QCL's scheduling APIs (TaskScheduler,ThrottledExecutor, etc.) are Java'sRunnableandSupplier<T>. If you pass a bare lambda via SAM conversion directly from a Kotlin sub-plugin, aLinkageErrormay be triggered during cross-plugin class loading. Please wrap it explicitly asRunnable { ... }/Supplier { ... }, or pass lambdas normally in Java.Folia compatibility.
TaskSchedulerwraps the Bukkit scheduler and is Folia-compatible. Always schedule throughTaskSchedulerrather than callingBukkit.getScheduler()yourself.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 toComponent.Multi-name fallback.
ServerCompat'ssound(...)/particle(...)/material(...)/resolveSound/resolvePotionEffectTypeaccept 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.
| Method | Description |
|---|---|
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
// 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.
| Method | Description |
|---|---|
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 |
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.
| Method | Description |
|---|---|
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 |
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.
| Method | Description |
|---|---|
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)
// 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.
| Method | Description |
|---|---|
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 |
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.
| Method | Description |
|---|---|
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 |
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 / member | Description |
|---|---|
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) |
platform | Current platform |
supportsPluginLibraries | Whether plugin libraries are supported |
supportsAsyncChatEvent | Whether 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
// 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.11TextUtil (text, singular)
Conversion of text to Component and colored output. toComponent automatically detects MiniMessage / legacy & codes / § codes / plain text.
| Method | Description |
|---|---|
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
| Extension | Description |
|---|---|
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 |
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.
| Method | Description |
|---|---|
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 |
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
| Method | Description |
|---|---|
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
| Method | Description |
|---|---|
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 |
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
DisplayLayerinterface: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> }
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
- API Overview — the overall map of the public API
- Module System — Module / ModuleManager / diagnostic model
- Glossary — terminology
- Documentation Home · Table of Contents