Replace is a high-performance, type-safe Kotlin library designed for Minecraft plugins to handle dynamic placeholders with built-in caching and smart state management.
- Smart Updating: Automatically avoids redundant updates to save CPU and network bandwidth (crucial for packet-based systems).
- Type-Safe Contexts: Link placeholders to specific types (e.g.,
Player,Entity, or custom objects). - Context Transformations: Easily map data types (e.g., provide a
Gameobject and automatically inheritPlayerplaceholders). - Update Intervals: Built-in throttling to control how often values are re-calculated.
- Multi-Format: Support for
Stringliterals and KyoriComponents out of the box.
Add the repository and dependency to your build.gradle.kts:
repositories {
maven("https://repo.nekroplex.com/releases")
}
dependencies {
implementation("gg.aquatic.replace:Replace:26.0.2")
}You can define placeholders that return simple strings or complex Kyori components.
// A constant placeholder (only calculated once per session)
val playerName = Placeholder.Literal<Player>("name", isConst = true) { player, _ ->
player.name
}
// A dynamic placeholder with internal arguments (e.g., %stat_kills%)
val statPlaceholder = Placeholder.Literal<Player>("stat", isConst = false) { player, arg ->
when (arg.lowercase()) {
"kills" -> getKills(player).toString()
"deaths" -> getDeaths(player).toString()
else -> "0"
}
}Register placeholders globally so they are automatically included when creating new contexts for that type.
Placeholders.register(playerName, statPlaceholder)A PlaceholderContext manages the lifecycle of your placeholders. You can transform contexts to reuse existing logic.
class MyGameSession(val player: Player, val score: Int)
// Create a context for MyGameSession that INHERITS all Player placeholders
val gameContext = Placeholders.resolverFor<MyGameSession>(
maxUpdateInterval = 20, // 20 ticks
transforms = arrayOf(
Placeholders.Transform { it.player } // Tell the context how to get a Player from a MyGameSession
)
)The library uses a "State" system. You can check if a value actually changed before sending updates to a player.
val component = Component.text("Welcome %name%! Kills: %stat_kills%")
val contextItem = gameContext.createItem(mySession, component)
// Attempt to update (respects maxUpdateInterval)
val updateResult = contextItem.tryUpdate(mySession)
if (updateResult.wasUpdated) {
val newComponent = updateResult.value
player.sendMessage(newComponent)
}For complex placeholders with multiple branches and arguments (PAPI-style), you can use the built-in DSL. It handles underscored tokens and quoted arguments automatically.
Placeholders.registerDSL<Player>("rank") {
// %rank_name%
"name" {
handle { getRankName(binder) }
}
// %rank_info_<rank>%
"info" {
stringArgument("target_rank") {
handle {
val rank = string("target_rank")
"Details for $rank..."
}
}
}
// Optional arguments logic: %rank_status% or %rank_status_detailed%
"status" {
handle { "Simple Status" }
"detailed" {
handle { "Very Detailed Status" }
}
}
}Example with shared handler
Placeholders.registerDSL<Player>("stat") {
// Shared handler for both %stat_wins% and %stat_wins_<player>%
"wins" {
handle {
val targetName = string("player") ?: binder.name
getWins(targetName).toString()
}
stringArgument("player") {
// No handler needed here; it automatically falls back to the parent
}
}
}If PlaceholderAPI is present on the server, Replace can automatically wrap PAPI placeholders into its type-safe system using the papi identifier:
%papi_player_name% → Automatically resolved via PAPI.
Contributions are welcome! Please feel free to submit a Pull Request.
Got questions, need help, or want to showcase what you've built with Replace? Join our community!
- Discord: Join the Aquatic Development Discord
- Issues: Open a ticket on GitHub for bugs or feature requests.
Built with ❤️ by Larkyy