diff --git a/README.md b/README.md index 2d958b5..c252bbe 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,6 @@ ## Todo - [x] release publishing -- [ ] placeholders +- [x] placeholders - [ ] signs - [ ] npcs diff --git a/build-logic/src/main/kotlin/simplecloud.base-conventions.gradle.kts b/build-logic/src/main/kotlin/simplecloud.base-conventions.gradle.kts index 69af428..c7cb35f 100644 --- a/build-logic/src/main/kotlin/simplecloud.base-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/simplecloud.base-conventions.gradle.kts @@ -31,6 +31,7 @@ repositories { maven("https://repo.waterdog.dev/snapshots/") maven("https://repo.opencollab.dev/maven-releases/") maven("https://repo.opencollab.dev/maven-snapshots/") + maven("https://repo.extendedclip.com/releases/") } dependencies { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5bb6c31..bec13d7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,12 +14,15 @@ bungeecord = "1.21-R0.4" waterdog = "2.0.4-SNAPSHOT" paper = "26.3.build.+" minestom = "26_3-SNAPSHOT" +placeholderapi = "2.12.3" +miniplaceholders = "3.2.0" adventure = "5.2.0" adventure-platform-bungeecord = "4.4.1" configurate = "4.2.0" +caffeine = "3.3.1" slf4j = "2.0.20" cloud = "2.1.0" @@ -42,6 +45,8 @@ bungeecord-api = { module = "net.md-5:bungeecord-api", version.ref = "bungeecord waterdog-api = { module = "dev.waterdog.waterdogpe:waterdog", version.ref = "waterdog" } paper-api = { module = "io.papermc.paper:paper-api", version.ref = "paper" } minestom = { module = "net.minestom:minestom", version.ref = "minestom" } +placeholderapi = { module = "me.clip:placeholderapi", version.ref = "placeholderapi" } +miniplaceholders-api = { module = "io.github.miniplaceholders:miniplaceholders-api", version.ref = "miniplaceholders" } adventure = { module = "net.kyori:adventure-api", version.ref = "adventure" } adventure-text-minimessage = { module = "net.kyori:adventure-text-minimessage", version.ref = "adventure" } @@ -54,6 +59,8 @@ configurate-yaml = { module = "org.spongepowered:configurate-yaml", version.ref configurate-gson = { module = "org.spongepowered:configurate-gson", version.ref = "configurate" } configurate-extra-kotlin = { module = "org.spongepowered:configurate-extra-kotlin", version.ref = "configurate" } +caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version.ref = "caffeine" } + slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } cloud-core = { module = "org.incendo:cloud-core", version.ref = "cloud" } diff --git a/server/placeholders/placeholders-paper/build.gradle.kts b/server/placeholders/placeholders-paper/build.gradle.kts index fedbf37..4a37ab7 100644 --- a/server/placeholders/placeholders-paper/build.gradle.kts +++ b/server/placeholders/placeholders-paper/build.gradle.kts @@ -1,3 +1,22 @@ plugins { id("simplecloud.base-conventions") + alias(libs.plugins.blossom) +} + +dependencies { + implementation(projects.server.placeholders.placeholdersShared) + compileOnly(libs.simplecloud.api) + compileOnly(libs.paper.api) + compileOnly(libs.placeholderapi) + compileOnly(libs.miniplaceholders.api) +} + +sourceSets { + main { + blossom { + kotlinSources { + property("version", project.version.toString()) + } + } + } } diff --git a/server/placeholders/placeholders-paper/src/main/kotlin-templates/app/simplecloud/plugin/placeholders/BuildConstants.kt.peb b/server/placeholders/placeholders-paper/src/main/kotlin-templates/app/simplecloud/plugin/placeholders/BuildConstants.kt.peb new file mode 100644 index 0000000..f1be4f5 --- /dev/null +++ b/server/placeholders/placeholders-paper/src/main/kotlin-templates/app/simplecloud/plugin/placeholders/BuildConstants.kt.peb @@ -0,0 +1,5 @@ +package app.simplecloud.plugin.placeholders + +object BuildConstants { + const val VERSION = "{{ version }}" +} \ No newline at end of file diff --git a/server/placeholders/placeholders-paper/src/main/kotlin/.gitkeep b/server/placeholders/placeholders-paper/src/main/kotlin/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/server/placeholders/placeholders-paper/src/main/kotlin/app/simplecloud/plugin/placeholders/paper/PaperPlaceholdersPlugin.kt b/server/placeholders/placeholders-paper/src/main/kotlin/app/simplecloud/plugin/placeholders/paper/PaperPlaceholdersPlugin.kt new file mode 100644 index 0000000..758b16e --- /dev/null +++ b/server/placeholders/placeholders-paper/src/main/kotlin/app/simplecloud/plugin/placeholders/paper/PaperPlaceholdersPlugin.kt @@ -0,0 +1,39 @@ +package app.simplecloud.plugin.placeholders.paper + +import app.simplecloud.plugin.placeholders.paper.expansion.MiniPlaceholdersExpansion +import app.simplecloud.plugin.placeholders.paper.expansion.PlaceholderApiExpansion +import app.simplecloud.plugin.placeholders.shared.PlaceholdersPlugin +import org.bukkit.plugin.java.JavaPlugin + +class PaperPlaceholdersPlugin : JavaPlugin() { + + private val placeholders = PlaceholdersPlugin() + + private var placeholderApiExpansion: PlaceholderApiExpansion? = null + private var miniPlaceholdersExpansion: MiniPlaceholdersExpansion? = null + + override fun onEnable() { + placeholders.start() + + if (server.pluginManager.isPluginEnabled("PlaceholderAPI")) { + val expansion = PlaceholderApiExpansion(placeholders.resolver) + expansion.register() + placeholderApiExpansion = expansion + } + if (server.pluginManager.isPluginEnabled("MiniPlaceholders")) { + val expansion = MiniPlaceholdersExpansion(placeholders.resolver, placeholders.types) + expansion.register() + miniPlaceholdersExpansion = expansion + } + + if (placeholderApiExpansion == null && miniPlaceholdersExpansion == null) { + logger.warning("Neither PlaceholderAPI nor MiniPlaceholders is installed, no placeholders will be registered") + } + } + + override fun onDisable() { + placeholderApiExpansion?.unregister() + miniPlaceholdersExpansion?.unregister() + placeholders.shutdown() + } +} diff --git a/server/placeholders/placeholders-paper/src/main/kotlin/app/simplecloud/plugin/placeholders/paper/expansion/MiniPlaceholdersExpansion.kt b/server/placeholders/placeholders-paper/src/main/kotlin/app/simplecloud/plugin/placeholders/paper/expansion/MiniPlaceholdersExpansion.kt new file mode 100644 index 0000000..78a3ebe --- /dev/null +++ b/server/placeholders/placeholders-paper/src/main/kotlin/app/simplecloud/plugin/placeholders/paper/expansion/MiniPlaceholdersExpansion.kt @@ -0,0 +1,43 @@ +package app.simplecloud.plugin.placeholders.paper.expansion + +import app.simplecloud.plugin.placeholders.BuildConstants +import app.simplecloud.plugin.placeholders.shared.placeholder.PlaceholderType +import app.simplecloud.plugin.placeholders.shared.query.PlaceholderQuery +import app.simplecloud.plugin.placeholders.shared.resolve.PlaceholderResolver +import io.github.miniplaceholders.api.Expansion +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.minimessage.tag.Tag +import net.kyori.adventure.text.minimessage.tag.resolver.ArgumentQueue + +class MiniPlaceholdersExpansion( + private val resolver: PlaceholderResolver, + types: List>, +) { + + private val expansion: Expansion + + init { + val builder = Expansion.builder("simplecloud").author("SimpleCloud").version(BuildConstants.VERSION) + types.forEach { type -> + type.properties.forEach { property -> + builder.globalPlaceholder("${type.prefix}_$property") { queue, _ -> resolve(type, property, queue) } + } + } + expansion = builder.build() + } + + fun register() = expansion.register() + + fun unregister() { + if (expansion.registered()) expansion.unregister() + } + + private fun resolve(type: PlaceholderType<*>, property: String, queue: ArgumentQueue): Tag { + val arguments = mutableListOf() + while (queue.hasNext()) arguments += queue.pop().value() + + val target = arguments.joinToString(":").ifEmpty { null } + val value = resolver.resolve(PlaceholderQuery(type, target, property)) + return Tag.selfClosingInserting(Component.text(value)) + } +} diff --git a/server/placeholders/placeholders-paper/src/main/kotlin/app/simplecloud/plugin/placeholders/paper/expansion/PlaceholderApiExpansion.kt b/server/placeholders/placeholders-paper/src/main/kotlin/app/simplecloud/plugin/placeholders/paper/expansion/PlaceholderApiExpansion.kt new file mode 100644 index 0000000..5a2a35a --- /dev/null +++ b/server/placeholders/placeholders-paper/src/main/kotlin/app/simplecloud/plugin/placeholders/paper/expansion/PlaceholderApiExpansion.kt @@ -0,0 +1,21 @@ +package app.simplecloud.plugin.placeholders.paper.expansion + +import app.simplecloud.plugin.placeholders.BuildConstants +import app.simplecloud.plugin.placeholders.shared.resolve.PlaceholderResolver +import me.clip.placeholderapi.expansion.PlaceholderExpansion +import org.bukkit.OfflinePlayer + +class PlaceholderApiExpansion( + private val resolver: PlaceholderResolver, +) : PlaceholderExpansion() { + + override fun getIdentifier() = "simplecloud" + + override fun getAuthor() = "SimpleCloud" + + override fun getVersion() = BuildConstants.VERSION + + override fun persist() = true + + override fun onRequest(player: OfflinePlayer?, params: String): String? = resolver.resolve(params) +} diff --git a/server/placeholders/placeholders-paper/src/main/resources/paper-plugin.yml b/server/placeholders/placeholders-paper/src/main/resources/paper-plugin.yml new file mode 100644 index 0000000..630ff8d --- /dev/null +++ b/server/placeholders/placeholders-paper/src/main/resources/paper-plugin.yml @@ -0,0 +1,23 @@ +name: simplecloud-placeholder +version: ${version} +main: app.simplecloud.plugin.placeholders.paper.PaperPlaceholdersPlugin +api-version: "1.21.11" +author: SimpleCloud Maintainers +description: Provides SimpleCloud placeholders for PlaceholderAPI and MiniPlaceholders +website: https://github.com/simplecloudapp/plugins +folia-supported: true + +dependencies: + server: + PlaceholderAPI: + load: BEFORE + required: false + join-classpath: true + MiniPlaceholders: + load: BEFORE + required: false + join-classpath: true + simplecloud-api: + load: BEFORE + required: true + join-classpath: true diff --git a/server/placeholders/placeholders-shared/build.gradle.kts b/server/placeholders/placeholders-shared/build.gradle.kts index fedbf37..b83b0c1 100644 --- a/server/placeholders/placeholders-shared/build.gradle.kts +++ b/server/placeholders/placeholders-shared/build.gradle.kts @@ -1,3 +1,9 @@ plugins { id("simplecloud.base-conventions") } + +dependencies { + compileOnly(libs.simplecloud.api) + compileOnly(libs.slf4j.api) + implementation(libs.caffeine) +} diff --git a/server/placeholders/placeholders-shared/src/main/kotlin/.gitkeep b/server/placeholders/placeholders-shared/src/main/kotlin/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/server/placeholders/placeholders-shared/src/main/kotlin/app/simplecloud/plugin/placeholders/shared/PlaceholdersPlugin.kt b/server/placeholders/placeholders-shared/src/main/kotlin/app/simplecloud/plugin/placeholders/shared/PlaceholdersPlugin.kt new file mode 100644 index 0000000..be0c99a --- /dev/null +++ b/server/placeholders/placeholders-shared/src/main/kotlin/app/simplecloud/plugin/placeholders/shared/PlaceholdersPlugin.kt @@ -0,0 +1,27 @@ +package app.simplecloud.plugin.placeholders.shared + +import app.simplecloud.api.CloudApi +import app.simplecloud.api.CloudApiOptions +import app.simplecloud.plugin.placeholders.shared.placeholder.GroupPlaceholders +import app.simplecloud.plugin.placeholders.shared.placeholder.PersistentServerPlaceholders +import app.simplecloud.plugin.placeholders.shared.placeholder.PlaceholderType +import app.simplecloud.plugin.placeholders.shared.placeholder.ServerPlaceholders +import app.simplecloud.plugin.placeholders.shared.query.PlaceholderQueryParser +import app.simplecloud.plugin.placeholders.shared.resolve.PlaceholderResolver + +class PlaceholdersPlugin { + + private val api: CloudApi = CloudApi.create(CloudApiOptions.builder().component("placeholders").build()) + + val types: List> = listOf(PersistentServerPlaceholders, ServerPlaceholders, GroupPlaceholders) + val resolver = PlaceholderResolver(api, PlaceholderQueryParser(types)) + + fun start() { + resolver.preloadCurrent(types) + } + + fun shutdown() { + resolver.shutdown() + api.close() + } +} diff --git a/server/placeholders/placeholders-shared/src/main/kotlin/app/simplecloud/plugin/placeholders/shared/placeholder/GroupPlaceholders.kt b/server/placeholders/placeholders-shared/src/main/kotlin/app/simplecloud/plugin/placeholders/shared/placeholder/GroupPlaceholders.kt new file mode 100644 index 0000000..2fb8a80 --- /dev/null +++ b/server/placeholders/placeholders-shared/src/main/kotlin/app/simplecloud/plugin/placeholders/shared/placeholder/GroupPlaceholders.kt @@ -0,0 +1,35 @@ +package app.simplecloud.plugin.placeholders.shared.placeholder + +import app.simplecloud.api.CloudApi +import app.simplecloud.api.group.Group +import app.simplecloud.api.runtime.SimpleCloudRuntime +import app.simplecloud.api.server.ServerQuery +import kotlinx.coroutines.future.await + +object GroupPlaceholders : PlaceholderType { + + data class Data(val group: Group, val playerCount: Int) + + override val prefix = "group" + + override val properties = setOf("name", "type", "min_memory", "max_memory", "max_players", "player_count") + + override fun getValue(resource: Data, property: String): Any? = when (property) { + "name" -> resource.group.name + "type" -> resource.group.type + "min_memory" -> resource.group.minMemory + "max_memory" -> resource.group.maxMemory + "max_players" -> resource.group.maxPlayers + "player_count" -> resource.playerCount + else -> null + } + + override suspend fun loadCurrent(api: CloudApi): Data? = load(api, SimpleCloudRuntime.groupName()) + + override suspend fun load(api: CloudApi, target: String): Data? { + val group = api.group().getGroupByName(target).await() ?: return null + val query = ServerQuery.create().filterByServerGroupName(group.name) + val servers = api.server().getAllServers(query).await() + return Data(group, servers.sumOf { it.playerCount ?: 0 }) + } +} diff --git a/server/placeholders/placeholders-shared/src/main/kotlin/app/simplecloud/plugin/placeholders/shared/placeholder/PersistentServerPlaceholders.kt b/server/placeholders/placeholders-shared/src/main/kotlin/app/simplecloud/plugin/placeholders/shared/placeholder/PersistentServerPlaceholders.kt new file mode 100644 index 0000000..fbdc4e4 --- /dev/null +++ b/server/placeholders/placeholders-shared/src/main/kotlin/app/simplecloud/plugin/placeholders/shared/placeholder/PersistentServerPlaceholders.kt @@ -0,0 +1,42 @@ +package app.simplecloud.plugin.placeholders.shared.placeholder + +import app.simplecloud.api.CloudApi +import app.simplecloud.api.persistentserver.PersistentServer +import app.simplecloud.api.runtime.SimpleCloudRuntime +import kotlinx.coroutines.future.await + +object PersistentServerPlaceholders : PlaceholderType { + + override val prefix = "persistent_server" + + override val properties = setOf( + "id", + "name", + "type", + "host", + "max_players", + "min_memory", + "max_memory", + "player_count", + "is_active", + ) + + override fun getValue(resource: PersistentServer, property: String): Any? = when (property) { + "id" -> resource.persistentServerId + "name" -> resource.name + "type" -> resource.type + "host" -> resource.serverhostId + "max_players" -> resource.maxPlayers + "min_memory" -> resource.minMemory + "max_memory" -> resource.maxMemory + "player_count" -> resource.playerCount + "is_active" -> resource.isActive + else -> null + } + + override suspend fun loadCurrent(api: CloudApi): PersistentServer? = + api.persistentServer().getPersistentServerById(SimpleCloudRuntime.serverId()).await() + + override suspend fun load(api: CloudApi, target: String): PersistentServer? = + api.persistentServer().getPersistentServerByName(target).await() +} diff --git a/server/placeholders/placeholders-shared/src/main/kotlin/app/simplecloud/plugin/placeholders/shared/placeholder/PlaceholderType.kt b/server/placeholders/placeholders-shared/src/main/kotlin/app/simplecloud/plugin/placeholders/shared/placeholder/PlaceholderType.kt new file mode 100644 index 0000000..cc774d0 --- /dev/null +++ b/server/placeholders/placeholders-shared/src/main/kotlin/app/simplecloud/plugin/placeholders/shared/placeholder/PlaceholderType.kt @@ -0,0 +1,16 @@ +package app.simplecloud.plugin.placeholders.shared.placeholder + +import app.simplecloud.api.CloudApi + +interface PlaceholderType { + + val prefix: String + + val properties: Set + + fun getValue(resource: T, property: String): Any? + + suspend fun loadCurrent(api: CloudApi): T? + + suspend fun load(api: CloudApi, target: String): T? +} diff --git a/server/placeholders/placeholders-shared/src/main/kotlin/app/simplecloud/plugin/placeholders/shared/placeholder/ServerPlaceholders.kt b/server/placeholders/placeholders-shared/src/main/kotlin/app/simplecloud/plugin/placeholders/shared/placeholder/ServerPlaceholders.kt new file mode 100644 index 0000000..e61400e --- /dev/null +++ b/server/placeholders/placeholders-shared/src/main/kotlin/app/simplecloud/plugin/placeholders/shared/placeholder/ServerPlaceholders.kt @@ -0,0 +1,61 @@ +package app.simplecloud.plugin.placeholders.shared.placeholder + +import app.simplecloud.api.CloudApi +import app.simplecloud.api.runtime.SimpleCloudRuntime +import app.simplecloud.api.server.Server +import kotlinx.coroutines.future.await + +object ServerPlaceholders : PlaceholderType { + + private const val ID_PREFIX = "id:" + + override val prefix = "server" + + override val properties = setOf( + "id", + "type", + "host", + "numerical_id", + "ip", + "port", + "max_players", + "min_memory", + "max_memory", + "player_count", + "state", + ) + + override fun getValue(resource: Server, property: String): Any? = when (property) { + "id" -> resource.serverId + "type" -> resource.serverBase.type + "host" -> resource.serverhostId + "numerical_id" -> resource.numericalId + "ip" -> resource.ip + "port" -> resource.port + "max_players" -> resource.maxPlayers + "min_memory" -> resource.minMemory + "max_memory" -> resource.maxMemory + "player_count" -> resource.playerCount + "state" -> resource.state + else -> null + } + + override suspend fun loadCurrent(api: CloudApi): Server? = api.server().getServerById(SimpleCloudRuntime.serverId()).await() + + override suspend fun load(api: CloudApi, target: String): Server? = + if (target.startsWith(ID_PREFIX)) loadById(api, target.removePrefix(ID_PREFIX)) else loadByNumericalId(api, target) + + private suspend fun loadById(api: CloudApi, serverId: String): Server? { + if (serverId.isBlank()) return null + return api.server().getServerById(serverId).await() + } + + private suspend fun loadByNumericalId(api: CloudApi, target: String): Server? { + val separator = target.lastIndexOf(':') + if (separator <= 0) return null + + val groupName = target.substring(0, separator) + val numericalId = target.substring(separator + 1).toIntOrNull() ?: return null + return api.server().getServerByNumericalId(groupName, numericalId).await() + } +} diff --git a/server/placeholders/placeholders-shared/src/main/kotlin/app/simplecloud/plugin/placeholders/shared/query/PlaceholderQuery.kt b/server/placeholders/placeholders-shared/src/main/kotlin/app/simplecloud/plugin/placeholders/shared/query/PlaceholderQuery.kt new file mode 100644 index 0000000..ec2cd51 --- /dev/null +++ b/server/placeholders/placeholders-shared/src/main/kotlin/app/simplecloud/plugin/placeholders/shared/query/PlaceholderQuery.kt @@ -0,0 +1,9 @@ +package app.simplecloud.plugin.placeholders.shared.query + +import app.simplecloud.plugin.placeholders.shared.placeholder.PlaceholderType + +data class PlaceholderQuery( + val type: PlaceholderType<*>, + val target: String?, + val property: String, +) diff --git a/server/placeholders/placeholders-shared/src/main/kotlin/app/simplecloud/plugin/placeholders/shared/query/PlaceholderQueryParser.kt b/server/placeholders/placeholders-shared/src/main/kotlin/app/simplecloud/plugin/placeholders/shared/query/PlaceholderQueryParser.kt new file mode 100644 index 0000000..0d94d01 --- /dev/null +++ b/server/placeholders/placeholders-shared/src/main/kotlin/app/simplecloud/plugin/placeholders/shared/query/PlaceholderQueryParser.kt @@ -0,0 +1,30 @@ +package app.simplecloud.plugin.placeholders.shared.query + +import app.simplecloud.plugin.placeholders.shared.placeholder.GroupPlaceholders +import app.simplecloud.plugin.placeholders.shared.placeholder.PersistentServerPlaceholders +import app.simplecloud.plugin.placeholders.shared.placeholder.PlaceholderType +import app.simplecloud.plugin.placeholders.shared.placeholder.ServerPlaceholders + +class PlaceholderQueryParser(private val types: List> = DEFAULT_TYPES) { + + fun parse(params: String): PlaceholderQuery? { + for (type in types) { + val prefix = "${type.prefix}_" + if (!params.startsWith(prefix)) continue + + val rest = params.removePrefix(prefix) + val property = findProperty(type, rest) ?: continue + val target = rest.removeSuffix(property).removeSuffix("_").ifEmpty { null } + return PlaceholderQuery(type, target, property) + } + return null + } + + private fun findProperty(type: PlaceholderType<*>, rest: String): String? = type.properties + .sortedByDescending { it.length } + .firstOrNull { rest == it || rest.endsWith("_$it") } + + private companion object { + val DEFAULT_TYPES = listOf(PersistentServerPlaceholders, ServerPlaceholders, GroupPlaceholders) + } +} diff --git a/server/placeholders/placeholders-shared/src/main/kotlin/app/simplecloud/plugin/placeholders/shared/resolve/PlaceholderResolver.kt b/server/placeholders/placeholders-shared/src/main/kotlin/app/simplecloud/plugin/placeholders/shared/resolve/PlaceholderResolver.kt new file mode 100644 index 0000000..ed20701 --- /dev/null +++ b/server/placeholders/placeholders-shared/src/main/kotlin/app/simplecloud/plugin/placeholders/shared/resolve/PlaceholderResolver.kt @@ -0,0 +1,70 @@ +package app.simplecloud.plugin.placeholders.shared.resolve + +import app.simplecloud.api.CloudApi +import app.simplecloud.plugin.placeholders.shared.placeholder.PlaceholderType +import app.simplecloud.plugin.placeholders.shared.query.PlaceholderQuery +import app.simplecloud.plugin.placeholders.shared.query.PlaceholderQueryParser +import com.github.benmanes.caffeine.cache.AsyncCacheLoader +import com.github.benmanes.caffeine.cache.AsyncLoadingCache +import com.github.benmanes.caffeine.cache.Caffeine +import kotlinx.coroutines.* +import kotlinx.coroutines.future.future +import org.slf4j.LoggerFactory +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds +import kotlin.time.toJavaDuration + +class PlaceholderResolver( + private val api: CloudApi, + private val parser: PlaceholderQueryParser = PlaceholderQueryParser(), + private val loadingValue: String = "...", +) { + + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + private val logger = LoggerFactory.getLogger(PlaceholderResolver::class.java) + + private val cache: AsyncLoadingCache = Caffeine.newBuilder() + .maximumSize(1_000L) + .refreshAfterWrite(5.seconds.toJavaDuration()) + .expireAfterAccess(5.minutes.toJavaDuration()) + .buildAsync(AsyncCacheLoader { key, _ -> scope.future { CachedResource(load(key)) } }) + + fun resolve(params: String): String? { + val query = parser.parse(params) ?: return null + return resolve(query) + } + + fun resolve(query: PlaceholderQuery): String { + val future = cache.get(ResourceKey(query.type, query.target)) + if (!future.isDone) return loadingValue + + val resource = future.exceptionally { null }.getNow(null)?.resource ?: return "" + + @Suppress("UNCHECKED_CAST") + val type = query.type as PlaceholderType + return type.getValue(resource, query.property)?.toString().orEmpty() + } + + @Suppress("IgnoredReturnValue") + fun preloadCurrent(types: List>) { + types.forEach { cache.get(ResourceKey(it, null)) } + } + + fun shutdown() { + scope.cancel() + } + + private suspend fun load(key: ResourceKey): Any? = try { + if (key.target == null) key.type.loadCurrent(api) else key.type.load(api, key.target) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + logger.warn("Failed to load {} placeholder for target {}", key.type.prefix, key.target ?: "", e) + null + } + + private data class ResourceKey(val type: PlaceholderType<*>, val target: String?) + + private class CachedResource(val resource: Any?) +}