Skip to content

Commit

Permalink
Allow entry placeholders to have parameters
Browse files Browse the repository at this point in the history
  • Loading branch information
gabber235 committed Nov 21, 2024
1 parent 2599569 commit 5a41054
Show file tree
Hide file tree
Showing 22 changed files with 823 additions and 130 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import CodeSnippet from "@site/src/components/CodeSnippet";

# Placeholder
Entries can expose a placeholder.
The placeholders can be used by users or other plugins with the PlaceholderAPI.

:::danger
Placeholder is an additional interface for an existing entry. It cannot be used on its own.
:::

## Basic Usage
To just expose a single placeholder, extend your entry with `PlaceholderEntry`:

<CodeSnippet tag="simple_placeholder_entry" json={require("../../snippets.json")} />

This placeholder can be used with `%typewriter_<entry id>%` and will return `Hello <player name>!`

## Sub Placeholders
Besides just having a primary placeholder, entries can also have sub placeholders.
These can be literal strings which needs to be matched:

<CodeSnippet tag="literal_placeholder_entry" json={require("../../snippets.json")} />

Where the placeholder can be used in the following ways:

| Placeholder | Result |
|------------|---------|
| `%typewriter_<entry id>%` | `Standard text` |
| `%typewriter_<entry id>:greet%` | `Hello, <player name>!` |
| `%typewriter_<entry id>:greet:enthusiastic%` | `HEY HOW IS YOUR DAY, <player name>!` |

But is can also have variables:

<CodeSnippet tag="string_placeholder_entry" json={require("../../snippets.json")} />

Where the placeholder can be used in the following ways:

| Placeholder | Result |
|------------|---------|
| `%typewriter_<entry id>%` | `%typewriter_<entry id>%` |
| `%typewriter_<entry id>:bob%` | `Hello, bob!` |
| `%typewriter_<entry id>:alice%` | `Hello, alice!` |

Notice how the default placeholder no longer works, because we don't have a supplier for it anymore.
41 changes: 41 additions & 0 deletions documentation/docs/develop/02-extensions/07-api-changes/0.7.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
title: 0.7.X API Changes
---

import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";

# All API changes to 0.7.X

The v0.7.X release contains the new Dynamic variable.
To learn how to use them, see [Dynamic Variables](/develop/extensions/entries/static/variable).

## PlaceholderEntry Changes

<Tabs>
<TabItem value="old" label="Old">
```kotlin showLineNumbers
override fun display(player: Player?): String? {
return "Hello, ${player?.name ?: "World"}!"
}
```
</TabItem>
<TabItem value="new" label="New" default>
```kotlin showLineNumbers
override fun parser(): PlaceholderParser = placeholderParser {
supply { player ->
"Hello, ${player?.name ?: "World"}!"
}
}
```
</TabItem>
</Tabs>

The placeholder now returns a parser instead of directly parsing.
This allows for more complex placeholders to be created.
With sub placeholders, for example.

For example the `TimedFact` returns the fact value by default for fact `%typewriter_<entry id>%`.
But if you want the time when the fact will expire you can use `%typewriter_<entry id>:time:expires:relative%`
Where the `:time:expires:relative` is a sub placeholder.

76 changes: 44 additions & 32 deletions documentation/plugins/code-snippets/snippets.json

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion engine/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@ subprojects {
dependencies {
api("io.insert-koin:koin-core:3.5.6")
compileOnly("com.google.code.gson:gson:2.11.0")
testImplementation(kotlin("test"))

val kotestVersion = "5.7.2"
testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion")
testImplementation("io.kotest:kotest-assertions-core:$kotestVersion")
testImplementation("io.kotest:kotest-property:$kotestVersion")
}


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.typewritermc.core.utils

import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes

fun Duration.formatCompact(): String {
val days = this.inWholeDays
val hours = (this - days.days).inWholeHours
val minutes = (this - days.days - hours.hours).inWholeMinutes
val seconds = (this - days.days - hours.hours - minutes.minutes).inWholeSeconds

return buildString {
if (days > 0) append("${days}d ")
if (hours > 0) append("${hours}h ")
if (minutes > 0) append("${minutes}m ")
if (seconds > 0 || this.isEmpty()) append("${seconds}s")
}.trim()
}
2 changes: 1 addition & 1 deletion engine/engine-paper/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ dependencies {
compileOnly("me.clip:placeholderapi:2.11.6")
compileOnlyApi("org.geysermc.floodgate:api:2.2.0-SNAPSHOT")

testImplementation("org.junit.jupiter:junit-jupiter:5.9.0")
testImplementation("org.mockbukkit.mockbukkit:mockbukkit-v1.21:4.3.1")
}

tasks.withType<ShadowJar> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,10 @@ class TypewriterPaperPlugin : KotlinPlugin(), KoinComponent {
get<AudienceManager>().load()
get<MessengerFinder>().load()
CustomCommandEntry.registerAll()

if (server.pluginManager.getPlugin("PlaceholderAPI") != null) {
PlaceholderExpansion.load()
}
}

suspend fun unload() {
Expand All @@ -184,6 +188,10 @@ class TypewriterPaperPlugin : KotlinPlugin(), KoinComponent {
get<RoadNetworkManager>().unload()
get<StagingManager>().unload()

if (server.pluginManager.getPlugin("PlaceholderAPI") != null) {
PlaceholderExpansion.unload()
}

// Needs to be last, as it will unload the classLoader
get<TypewriterCore>().unload()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,173 @@ package com.typewritermc.engine.paper.entry

import com.typewritermc.core.entries.Entry
import com.typewritermc.core.extension.annotations.Tags
import com.typewritermc.core.utils.failure
import com.typewritermc.core.utils.ok
import org.bukkit.entity.Player
import java.util.*
import kotlin.reflect.KClass
import kotlin.reflect.cast

@Tags("placeholder")
interface PlaceholderEntry : Entry {
fun display(player: Player?): String?
fun parser(): PlaceholderParser
}

interface PlaceholderSupplier {
fun supply(context: ParsingContext): String?
}

class ParsingContext(
val player: Player?,
private val arguments: Map<ArgumentReference<out Any>, Any>
) {
fun <T : Any> getArgument(reference: ArgumentReference<T>): T {
val value = arguments[reference]
return reference.type.cast(value)
}

fun hasArgument(reference: ArgumentReference<out Any>): Boolean {
return arguments.containsKey(reference)
}

operator fun <T : Any> ArgumentReference<T>.invoke(): T {
return getArgument(this)
}
}

interface PlaceholderArgument<T : Any> {
fun parse(player: Player?, argument: String): Result<T>
}

class ArgumentReference<T : Any>(
val id: String = UUID.randomUUID().toString(),
val type: KClass<T>,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ArgumentReference<*>) return false

if (id != other.id) return false

return true
}

override fun hashCode(): Int {
return id.hashCode()
}

override fun toString(): String {
return "ArgumentReference(id='$id', type=$type)"
}
}

sealed interface PlaceholderNode

class SupplierNode(val supplier: PlaceholderSupplier) : PlaceholderNode

class ArgumentNode<T : Any>(
val name: String,
val reference: ArgumentReference<T>,
val argument: PlaceholderArgument<T>,
val children: List<PlaceholderNode>
) : PlaceholderNode

class PlaceholderParser(
val nodes: List<PlaceholderNode>,
) {
fun parse(player: Player?, arguments: List<String>): String? {
val parsedArguments = mutableMapOf<ArgumentReference<out Any>, Any>()
var currentNodes = nodes
for (argument in arguments) {
val nextNodes = mutableListOf<PlaceholderNode>()
for (node in currentNodes) {
when (node) {
is SupplierNode -> {}
is ArgumentNode<*> -> {
val result = node.argument.parse(player, argument)
if (result.isSuccess) {
parsedArguments[node.reference] = result.getOrThrow()
nextNodes.addAll(node.children)
}
}
}
}
if (nextNodes.isEmpty()) {
return null
}
currentNodes = nextNodes
}

val context = ParsingContext(player, parsedArguments)
val suppliers = currentNodes.filterIsInstance<SupplierNode>()
if (suppliers.isEmpty()) {
return null
}

return suppliers.first().supplier.supply(context)
}
}

class PlaceholderNodeBuilder {
internal val nodes = mutableListOf<PlaceholderNode>()
operator fun plusAssign(node: PlaceholderNode) {
nodes += node
}
}

fun placeholderParser(builder: PlaceholderNodeBuilder.() -> Unit): PlaceholderParser {
val nodes = PlaceholderNodeBuilder().apply(builder).nodes
return PlaceholderParser(nodes)
}

fun PlaceholderNodeBuilder.include(parser: PlaceholderParser) {
nodes.addAll(parser.nodes)
}

fun PlaceholderNodeBuilder.supply(supplier: ParsingContext.(Player?) -> String?) {
this += SupplierNode(object : PlaceholderSupplier {
override fun supply(context: ParsingContext): String? {
return supplier(context, context.player)
}
})
}

fun PlaceholderNodeBuilder.supplyPlayer(supplier: ParsingContext.(Player) -> String?) {
this += SupplierNode(object : PlaceholderSupplier {
override fun supply(context: ParsingContext): String? {
return supplier(context, context.player ?: return null)
}
})
}

typealias ArgumentBuilder<T> = PlaceholderNodeBuilder.(ArgumentReference<T>) -> Unit

fun <T : Any> PlaceholderNodeBuilder.argument(
name: String,
type: KClass<T>,
argument: PlaceholderArgument<T>,
builder: ArgumentBuilder<T>,
) {
val reference = ArgumentReference(type = type)
val children = PlaceholderNodeBuilder().apply { builder(reference) }.nodes
this += ArgumentNode(name, reference, argument, children)
}

fun PlaceholderNodeBuilder.literal(name: String, builder: PlaceholderNodeBuilder.() -> Unit) =
argument(name, String::class, LiteralArgument(name)) { builder() }

class LiteralArgument(val name: String) : PlaceholderArgument<String> {
override fun parse(player: Player?, argument: String): Result<String> {
if (argument != name) return failure("Literal '$name' didn't match argument '$argument'")
return ok(argument)
}
}

fun PlaceholderNodeBuilder.string(name: String, builder: ArgumentBuilder<String>) =
argument(name, String::class, StringArgument, builder)

object StringArgument : PlaceholderArgument<String> {
override fun parse(player: Player?, argument: String): Result<String> {
return ok(argument)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,8 @@ import com.typewritermc.core.entries.Ref
import com.typewritermc.core.entries.ref
import com.typewritermc.core.extension.annotations.*
import com.typewritermc.core.utils.point.Position
import com.typewritermc.engine.paper.entry.ManifestEntry
import com.typewritermc.engine.paper.entry.PlaceholderEntry
import com.typewritermc.engine.paper.entry.*
import com.typewritermc.engine.paper.entry.entity.*
import com.typewritermc.engine.paper.entry.findDisplay
import com.typewritermc.engine.paper.entry.inAudience
import com.typewritermc.engine.paper.utils.Sound
import org.bukkit.entity.Player
import kotlin.reflect.KClass
Expand All @@ -24,7 +21,9 @@ interface SpeakerEntry : PlaceholderEntry {
@Help("The sound that will be played when the entity speaks.")
val sound: Sound

override fun display(player: Player?): String? = displayName.get(player)
override fun parser(): PlaceholderParser = placeholderParser {
supply { player -> displayName.get(player) }
}
}

/**
Expand Down
Loading

0 comments on commit 5a41054

Please sign in to comment.