diff --git a/documentation/docs/develop/02-extensions/04-entries/placeholder.mdx b/documentation/docs/develop/02-extensions/04-entries/placeholder.mdx
new file mode 100644
index 0000000000..7c86661b53
--- /dev/null
+++ b/documentation/docs/develop/02-extensions/04-entries/placeholder.mdx
@@ -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`:
+
+
+
+This placeholder can be used with `%typewriter_%` and will return `Hello !`
+
+## Sub Placeholders
+Besides just having a primary placeholder, entries can also have sub placeholders.
+These can be literal strings which needs to be matched:
+
+
+
+Where the placeholder can be used in the following ways:
+
+| Placeholder | Result |
+|------------|---------|
+| `%typewriter_%` | `Standard text` |
+| `%typewriter_:greet%` | `Hello, !` |
+| `%typewriter_:greet:enthusiastic%` | `HEY HOW IS YOUR DAY, !` |
+
+But is can also have variables:
+
+
+
+Where the placeholder can be used in the following ways:
+
+| Placeholder | Result |
+|------------|---------|
+| `%typewriter_%` | `%typewriter_%` |
+| `%typewriter_:bob%` | `Hello, bob!` |
+| `%typewriter_:alice%` | `Hello, alice!` |
+
+Notice how the default placeholder no longer works, because we don't have a supplier for it anymore.
diff --git a/documentation/docs/develop/02-extensions/07-api-changes/0.7.mdx b/documentation/docs/develop/02-extensions/07-api-changes/0.7.mdx
new file mode 100644
index 0000000000..e6c20f7817
--- /dev/null
+++ b/documentation/docs/develop/02-extensions/07-api-changes/0.7.mdx
@@ -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
+
+
+
+ ```kotlin showLineNumbers
+ override fun display(player: Player?): String? {
+ return "Hello, ${player?.name ?: "World"}!"
+ }
+ ```
+
+
+ ```kotlin showLineNumbers
+ override fun parser(): PlaceholderParser = placeholderParser {
+ supply { player ->
+ "Hello, ${player?.name ?: "World"}!"
+ }
+ }
+ ```
+
+
+
+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_%`.
+But if you want the time when the fact will expire you can use `%typewriter_:time:expires:relative%`
+Where the `:time:expires:relative` is a sub placeholder.
+
diff --git a/documentation/plugins/code-snippets/snippets.json b/documentation/plugins/code-snippets/snippets.json
index 79a2c73ae2..84fb438824 100644
--- a/documentation/plugins/code-snippets/snippets.json
+++ b/documentation/plugins/code-snippets/snippets.json
@@ -1,118 +1,130 @@
{
+ "initializer": {
+ "file": "src/main/kotlin/com/typewritermc/example/ExampleInitializer.kt",
+ "content": "import com.typewritermc.core.extension.Initializable\nimport com.typewritermc.core.extension.annotations.Initializer\n\n@Initializer\nobject ExampleInitializer : Initializable {\n override fun initialize() {\n // Do something when the extension is initialized\n }\n\n override fun shutdown() {\n // Do something when the extension is shutdown\n }\n}"
+ },
+ "simple_placeholder_entry": {
+ "file": "src/main/kotlin/com/typewritermc/example/entries/ExamplePlaceholderEntry.kt",
+ "content": "class SimpleExamplePlaceholderEntry(\n override val id: String,\n override val name: String,\n) : PlaceholderEntry {\n override fun parser(): PlaceholderParser = placeholderParser {\n supply { player ->\n \"Hello, ${player?.name ?: \"World\"}!\"\n }\n }\n}"
+ },
+ "literal_placeholder_entry": {
+ "file": "src/main/kotlin/com/typewritermc/example/entries/ExamplePlaceholderEntry.kt",
+ "content": " override fun parser(): PlaceholderParser = placeholderParser {\n literal(\"greet\") {\n literal(\"enthusiastic\") {\n supply { player ->\n \"HEY HOW IS YOUR DAY, ${player?.name ?: \"World\"}!\"\n }\n }\n supply { player ->\n \"Hello, ${player?.name ?: \"World\"}\"\n }\n }\n supply {\n \"Standard text\"\n }\n }"
+ },
+ "string_placeholder_entry": {
+ "file": "src/main/kotlin/com/typewritermc/example/entries/ExamplePlaceholderEntry.kt",
+ "content": " override fun parser(): PlaceholderParser = placeholderParser {\n string(\"name\") { name ->\n supply {\n \"Hello, ${name()}!\"\n }\n }\n }"
+ },
"cinematic_entry": {
- "file": "src\\main\\kotlin\\com\\typewritermc\\example\\entries\\cinematic\\ExampleCinematicEntry.kt",
+ "file": "src/main/kotlin/com/typewritermc/example/entries/cinematic/ExampleCinematicEntry.kt",
"content": "@Entry(\"example_cinematic\", \"An example cinematic entry\", Colors.BLUE, \"material-symbols:cinematic-blur\")\nclass ExampleCinematicEntry(\n override val id: String = \"\",\n override val name: String = \"\",\n override val criteria: List = emptyList(),\n @Segments(Colors.BLUE, \"material-symbols:cinematic-blur\")\n val segments: List = emptyList(),\n) : CinematicEntry {\n override fun create(player: Player): CinematicAction {\n return ExampleCinematicAction(player, this)\n }\n}"
},
"cinematic_segment_with_min_max": {
- "file": "src\\main\\kotlin\\com\\typewritermc\\example\\entries\\cinematic\\ExampleCinematicEntry.kt",
+ "file": "src/main/kotlin/com/typewritermc/example/entries/cinematic/ExampleCinematicEntry.kt",
"content": " @Segments(Colors.BLUE, \"material-symbols:cinematic-blur\")\n @InnerMin(Min(10))\n @InnerMax(Max(20))\n val segments: List = emptyList(),"
},
"cinematic_create_actions": {
- "file": "src\\main\\kotlin\\com\\typewritermc\\example\\entries\\cinematic\\ExampleCinematicEntry.kt",
+ "file": "src/main/kotlin/com/typewritermc/example/entries/cinematic/ExampleCinematicEntry.kt",
"content": " // This will be used when the cinematic is normally displayed to the player.\n override fun create(player: Player): CinematicAction {\n return DefaultCinematicAction(player, this)\n }\n\n // This is used during content mode to display the cinematic to the player.\n // It may be null to not show it during simulation.\n override fun createSimulating(player: Player): CinematicAction? {\n return SimulatedCinematicAction(player, this)\n }\n\n // This is used during content mode to record the cinematic.\n // It may be null to not record it during simulation.\n override fun createRecording(player: Player): CinematicAction? {\n return RecordingCinematicAction(player, this)\n }"
},
"cinematic_segment": {
- "file": "src\\main\\kotlin\\com\\typewritermc\\example\\entries\\cinematic\\ExampleCinematicEntry.kt",
+ "file": "src/main/kotlin/com/typewritermc/example/entries/cinematic/ExampleCinematicEntry.kt",
"content": "data class ExampleSegment(\n override val startFrame: Int = 0,\n override val endFrame: Int = 0,\n) : Segment"
},
"cinematic_action": {
- "file": "src\\main\\kotlin\\com\\typewritermc\\example\\entries\\cinematic\\ExampleCinematicEntry.kt",
+ "file": "src/main/kotlin/com/typewritermc/example/entries/cinematic/ExampleCinematicEntry.kt",
"content": "class ExampleCinematicAction(\n val player: Player,\n val entry: ExampleCinematicEntry,\n) : CinematicAction {\n override suspend fun setup() {\n // Initialize variables, spawn entities, etc.\n }\n\n override suspend fun tick(frame: Int) {\n val segment = entry.segments activeSegmentAt frame\n // Can be null if no segment is active\n\n // The `frame` parameter is not necessarily next frame: `frame != old(frame)+1`\n\n // Execute tick logic for the segment\n }\n\n override suspend fun teardown() {\n // Remove entities, etc.\n }\n\n override fun canFinish(frame: Int): Boolean = entry.segments canFinishAt frame\n}"
},
"cinematic_simple_action": {
- "file": "src\\main\\kotlin\\com\\typewritermc\\example\\entries\\cinematic\\ExampleCinematicEntry.kt",
+ "file": "src/main/kotlin/com/typewritermc/example/entries/cinematic/ExampleCinematicEntry.kt",
"content": "class ExampleSimpleCinematicAction(\n val player: Player,\n entry: ExampleCinematicEntry,\n) : SimpleCinematicAction() {\n override val segments: List = entry.segments\n\n override suspend fun startSegment(segment: ExampleSegment) {\n super.startSegment(segment) // Keep this\n // Called when a segment starts\n }\n\n override suspend fun tickSegment(segment: ExampleSegment, frame: Int) {\n super.tickSegment(segment, frame) // Keep this\n // Called every tick while the segment is active\n // Will always be called after startSegment and never after stopSegment\n\n // The `frame` parameter is not necessarily next frame: `frame != old(frame)+1`\n }\n\n override suspend fun stopSegment(segment: ExampleSegment) {\n super.stopSegment(segment) // Keep this\n // Called when the segment ends\n // Will also be called if the cinematic is stopped early\n }\n}"
},
"audience_entry": {
- "file": "src\\main\\kotlin\\com\\typewritermc\\example\\entries\\manifest\\ExampleAudienceEntry.kt",
+ "file": "src/main/kotlin/com/typewritermc/example/entries/manifest/ExampleAudienceEntry.kt",
"content": "@Entry(\"example_audience\", \"An example audience entry.\", Colors.GREEN, \"material-symbols:chat-rounded\")\nclass ExampleAudienceEntry(\n override val id: String = \"\",\n override val name: String = \"\",\n) : AudienceEntry {\n override fun display(): AudienceDisplay {\n return ExampleAudienceDisplay()\n }\n}"
},
"audience_display": {
- "file": "src\\main\\kotlin\\com\\typewritermc\\example\\entries\\manifest\\ExampleAudienceEntry.kt",
+ "file": "src/main/kotlin/com/typewritermc/example/entries/manifest/ExampleAudienceEntry.kt",
"content": "class ExampleAudienceDisplay : AudienceDisplay() {\n override fun initialize() {\n // This is called when the first player is added to the audience.\n super.initialize()\n // Do something when the audience is initialized\n }\n\n override fun onPlayerAdd(player: Player) {\n // Do something when a player gets added to the audience\n }\n\n override fun onPlayerRemove(player: Player) {\n // Do something when a player gets removed from the audience\n }\n\n override fun dispose() {\n super.dispose()\n // Do something when the audience is disposed\n // It will always call onPlayerRemove for all players.\n // So no player cleanup is needed here.\n }\n}"
},
"tickable_audience_display": {
- "file": "src\\main\\kotlin\\com\\typewritermc\\example\\entries\\manifest\\ExampleAudienceEntry.kt",
+ "file": "src/main/kotlin/com/typewritermc/example/entries/manifest/ExampleAudienceEntry.kt",
"content": "// highlight-next-line\nclass TickableAudienceDisplay : AudienceDisplay(), TickableDisplay {\n override fun onPlayerAdd(player: Player) {}\n override fun onPlayerRemove(player: Player) {}\n\n // highlight-start\n override fun tick() {\n // Do something when the audience is ticked\n players.forEach { player ->\n // Do something with the player\n }\n\n // This is running asynchronously\n // If you need to do something on the main thread\n ThreadType.SYNC.launch {\n // Though this will run a tick later, to sync with the bukkit scheduler.\n }\n }\n // highlight-end\n}"
},
"audience_display_with_events": {
- "file": "src\\main\\kotlin\\com\\typewritermc\\example\\entries\\manifest\\ExampleAudienceEntry.kt",
+ "file": "src/main/kotlin/com/typewritermc/example/entries/manifest/ExampleAudienceEntry.kt",
"content": "class AudienceDisplayWithEvents : AudienceDisplay() {\n override fun onPlayerAdd(player: Player) {}\n override fun onPlayerRemove(player: Player) {}\n\n // highlight-start\n @EventHandler\n fun onSomeEvent(event: SomeBukkitEvent) {\n // Do something when the event is triggered\n // This will trigger for all players, not just the ones in the audience.\n // So we need to check if the player is in the audience.\n if (event.player in this) {\n // Do something with the player\n }\n }\n // highlight-end\n}"
},
"artifact_entry": {
- "file": "src\\main\\kotlin\\com\\typewritermc\\example\\entries\\static\\ExampleArtifactEntry.kt",
+ "file": "src/main/kotlin/com/typewritermc/example/entries/static/ExampleArtifactEntry.kt",
"content": "@Entry(\"example_artifact\", \"An example artifact entry.\", Colors.BLUE, \"material-symbols:home-storage-rounded\")\nclass ExampleArtifactEntry(\n override val id: String = \"\",\n override val name: String = \"\",\n override val artifactId: String = \"\",\n) : ArtifactEntry"
},
"artifact_access": {
- "file": "src\\main\\kotlin\\com\\typewritermc\\example\\entries\\static\\ExampleArtifactEntry.kt",
+ "file": "src/main/kotlin/com/typewritermc/example/entries/static/ExampleArtifactEntry.kt",
"content": "suspend fun accessArtifactData(ref: Ref) {\n val assetManager = KoinJavaComponent.get(AssetManager::class.java)\n val entry = ref.get() ?: return\n val content: String? = assetManager.fetchAsset(entry)\n // Do something with the content\n}"
},
"asset_entry": {
- "file": "src\\main\\kotlin\\com\\typewritermc\\example\\entries\\static\\ExampleAssetEntry.kt",
+ "file": "src/main/kotlin/com/typewritermc/example/entries/static/ExampleAssetEntry.kt",
"content": "@Entry(\"example_asset\", \"An example asset entry.\", Colors.BLUE, \"material-symbols:home-storage-rounded\")\nclass ExampleAssetEntry(\n override val id: String = \"\",\n override val name: String = \"\",\n override val path: String = \"\",\n) : AssetEntry"
},
"asset_access": {
- "file": "src\\main\\kotlin\\com\\typewritermc\\example\\entries\\static\\ExampleAssetEntry.kt",
+ "file": "src/main/kotlin/com/typewritermc/example/entries/static/ExampleAssetEntry.kt",
"content": "suspend fun accessAssetData(ref: Ref) {\n val assetManager = KoinJavaComponent.get(AssetManager::class.java)\n val entry = ref.get() ?: return\n val content: String? = assetManager.fetchAsset(entry)\n // Do something with the content\n}"
},
"sound_id_entry": {
- "file": "src\\main\\kotlin\\com\\typewritermc\\example\\entries\\static\\ExampleSoundIdEntry.kt",
+ "file": "src/main/kotlin/com/typewritermc/example/entries/static/ExampleSoundIdEntry.kt",
"content": "@Entry(\"example_sound\", \"An example sound entry.\", Colors.BLUE, \"icon-park-solid:volume-up\")\nclass ExampleSoundIdEntry(\n override val id: String = \"\",\n override val name: String = \"\",\n override val soundId: String = \"\",\n) : SoundIdEntry"
},
"sound_source_entry": {
- "file": "src\\main\\kotlin\\com\\typewritermc\\example\\entries\\static\\ExampleSoundSourceEntry.kt",
+ "file": "src/main/kotlin/com/typewritermc/example/entries/static/ExampleSoundSourceEntry.kt",
"content": "@Entry(\"example_sound_source\", \"An example sound source entry.\", Colors.BLUE, \"ic:round-spatial-audio-off\")\nclass ExampleSoundSourceEntry(\n override val id: String = \"\",\n override val name: String = \"\",\n) : SoundSourceEntry {\n override fun getEmitter(player: Player): SoundEmitter {\n // Return the emitter that should be used for the sound.\n // An entity should be provided.\n return SoundEmitter(player.entityId)\n }\n}"
},
"speaker_entry": {
- "file": "src\\main\\kotlin\\com\\typewritermc\\example\\entries\\static\\ExampleSpeakerEntry.kt",
+ "file": "src/main/kotlin/com/typewritermc/example/entries/static/ExampleSpeakerEntry.kt",
"content": "@Entry(\"example_speaker\", \"An example speaker entry.\", Colors.BLUE, \"ic:round-spatial-audio-off\")\nclass ExampleSpeakerEntry(\n override val id: String = \"\",\n override val name: String = \"\",\n override val displayName: Var = ConstVar(\"\"),\n override val sound: Sound = Sound.EMPTY,\n) : SpeakerEntry"
},
"variable_entry": {
- "file": "src\\main\\kotlin\\com\\typewritermc\\example\\entries\\static\\ExampleVariableEntry.kt",
+ "file": "src/main/kotlin/com/typewritermc/example/entries/static/ExampleVariableEntry.kt",
"content": "@Entry(\"example_variable\", \"An example variable entry.\", Colors.GREEN, \"mdi:code-tags\")\nclass ExampleVariableEntry(\n override val id: String = \"\",\n override val name: String = \"\",\n) : VariableEntry {\n override fun get(context: VarContext): T {\n val player = context.player\n val klass = context.klass\n\n TODO(\"Do something with the player and the klass\")\n }\n}"
},
"variable_entry_with_data": {
- "file": "src\\main\\kotlin\\com\\typewritermc\\example\\entries\\static\\ExampleVariableEntry.kt",
+ "file": "src/main/kotlin/com/typewritermc/example/entries/static/ExampleVariableEntry.kt",
"content": "@Entry(\"example_variable_with_data\", \"An example variable entry with data.\", Colors.GREEN, \"mdi:code-tags\")\n// Register the variable data associated with this variable.\n@VariableData(ExampleVariableWithData::class)\nclass ExampleVariableWithDataEntry(\n override val id: String = \"\",\n override val name: String = \"\",\n // This data will be the same for all uses of this variable.\n val someString: String = \"\",\n) : VariableEntry {\n override fun get(context: VarContext): T {\n val player = context.player\n val klass = context.klass\n this.someString\n val data = context.getData() ?: throw IllegalStateException(\"Could not find data for ${context.klass}, data: ${context.data}\")\n\n TODO(\"Do something with the player, the klass, and the data\")\n }\n}\n\nclass ExampleVariableWithData(\n // This data can change at the place where the variable is used.\n val otherInfo: Int = 0,\n)"
},
"generic_variable_entry": {
- "file": "src\\main\\kotlin\\com\\typewritermc\\example\\entries\\static\\ExampleVariableEntry.kt",
+ "file": "src/main/kotlin/com/typewritermc/example/entries/static/ExampleVariableEntry.kt",
"content": "@Entry(\"example_generic_variable\", \"An example generic variable entry.\", Colors.GREEN, \"mdi:code-tags\")\nclass ExampleGenericVariableEntry(\n override val id: String = \"\",\n override val name: String = \"\",\n // We determine how to parse this during runtime.\n val generic: Generic = Generic.Empty,\n) : VariableEntry {\n override fun get(context: VarContext): T {\n val player = context.player\n val klass = context.klass\n\n // Parse the generic data to the correct type.\n val data = generic.get(klass)\n\n TODO(\"Do something with the player, the klass, and the generic\")\n }\n}\n\nclass ExampleGenericVariableData(\n // Generic data will always be the same as the generic type in the variable.\n val otherGeneric: Generic,\n)"
},
"constraint_variable_entry": {
- "file": "src\\main\\kotlin\\com\\typewritermc\\example\\entries\\static\\ExampleVariableEntry.kt",
+ "file": "src/main/kotlin/com/typewritermc/example/entries/static/ExampleVariableEntry.kt",
"content": "@Entry(\"example_constraint_variable\", \"An example constraint variable entry.\", Colors.GREEN, \"mdi:code-tags\")\n@GenericConstraint(String::class)\n@GenericConstraint(Int::class)\nclass ExampleConstraintVariableEntry(\n override val id: String = \"\",\n override val name: String = \"\",\n // We determine how to parse this during runtime.\n val generic: Generic = Generic.Empty,\n) : VariableEntry {\n override fun get(context: VarContext): T {\n val player = context.player\n // This can only be a String or an Int.\n val klass = context.klass\n\n // Parse the generic data to the correct type.\n val data = generic.get(klass)\n\n TODO(\"Do something with the player, the klass, and the generic\")\n }\n}"
},
"variable_usage": {
- "file": "src\\main\\kotlin\\com\\typewritermc\\example\\entries\\static\\ExampleVariableEntry.kt",
+ "file": "src/main/kotlin/com/typewritermc/example/entries/static/ExampleVariableEntry.kt",
"content": "@Entry(\"example_action_using_variable\", \"An example action that uses a variable.\", Colors.RED, \"material-symbols:touch-app-rounded\")\nclass ExampleActionUsingVariableEntry(\n override val id: String = \"\",\n override val name: String = \"\",\n override val triggers: List[> = emptyList(),\n override val criteria: List = emptyList(),\n override val modifiers: List = emptyList(),\n val someString: Var = ConstVar(\"\"),\n val someInt: Var = ConstVar(0),\n) : ActionEntry {\n override fun execute(player: Player) {\n val someString = someString.get(player)\n val someInt = someInt.get(player)\n\n // Do something with the variables\n }\n}"
},
"action_entry": {
- "file": "src\\main\\kotlin\\com\\typewritermc\\example\\entries\\trigger\\ExampleActionEntry.kt",
+ "file": "src/main/kotlin/com/typewritermc/example/entries/trigger/ExampleActionEntry.kt",
"content": "@Entry(\"example_action\", \"An example action entry.\", Colors.RED, \"material-symbols:touch-app-rounded\")\nclass ExampleActionEntry(\n override val id: String = \"\",\n override val name: String = \"\",\n override val criteria: List = emptyList(),\n override val modifiers: List = emptyList(),\n override val triggers: List][> = emptyList(),\n) : ActionEntry {\n override fun execute(player: Player) {\n super.execute(player) // This will apply all the modifiers.\n // Do something with the player\n }\n}"
},
"custom_triggering_action_entry": {
- "file": "src\\main\\kotlin\\com\\typewritermc\\example\\entries\\trigger\\ExampleCustomTriggeringActionEntry.kt",
+ "file": "src/main/kotlin/com/typewritermc/example/entries/trigger/ExampleCustomTriggeringActionEntry.kt",
"content": "@Entry(\n \"example_custom_triggering_action\",\n \"An example custom triggering entry.\",\n Colors.RED,\n \"material-symbols:touch-app-rounded\"\n)\nclass ExampleCustomTriggeringActionEntry(\n override val id: String = \"\",\n override val name: String = \"\",\n override val criteria: List = emptyList(),\n override val modifiers: List = emptyList(),\n @SerializedName(\"triggers\")\n override val customTriggers: List][> = emptyList(),\n) : CustomTriggeringActionEntry {\n override fun execute(player: Player) {\n super.execute(player) // This will apply the modifiers.\n // Do something with the player\n player.triggerCustomTriggers() // Can be called later to trigger the next entries.\n }\n}"
},
"dialogue_entry": {
- "file": "src\\main\\kotlin\\com\\typewritermc\\example\\entries\\trigger\\ExampleDialogueEntry.kt",
+ "file": "src/main/kotlin/com/typewritermc/example/entries/trigger/ExampleDialogueEntry.kt",
"content": "@Entry(\"example_dialogue\", \"An example dialogue entry.\", Colors.BLUE, \"material-symbols:chat-rounded\")\nclass ExampleDialogueEntry(\n override val id: String = \"\",\n override val name: String = \"\",\n override val criteria: List = emptyList(),\n override val modifiers: List = emptyList(),\n override val triggers: List][> = emptyList(),\n override val speaker: Ref = emptyRef(),\n @MultiLine\n @Placeholder\n @Colored\n @Help(\"The text to display to the player.\")\n val text: String = \"\",\n) : DialogueEntry"
},
"dialogue_messenger": {
- "file": "src\\main\\kotlin\\com\\typewritermc\\example\\entries\\trigger\\ExampleDialogueEntry.kt",
+ "file": "src/main/kotlin/com/typewritermc/example/entries/trigger/ExampleDialogueEntry.kt",
"content": "@Messenger(ExampleDialogueEntry::class)\nclass ExampleDialogueDialogueMessenger(player: Player, entry: ExampleDialogueEntry) :\n DialogueMessenger(player, entry) {\n\n companion object : MessengerFilter {\n override fun filter(player: Player, entry: DialogueEntry): Boolean = true\n }\n\n // Called every game tick (20 times per second).\n // The cycle is a parameter that is incremented every tick, starting at 0.\n override fun tick(context: TickContext) {\n super.tick(context)\n if (state != MessengerState.RUNNING) return\n\n player.sendMessage(\"${entry.speakerDisplayName}: ${entry.text}\".parsePlaceholders(player).asMini())\n\n // When we want the dialogue to end, we can set the state to FINISHED.\n state = MessengerState.FINISHED\n }\n}"
},
"event_entry": {
- "file": "src\\main\\kotlin\\com\\typewritermc\\example\\entries\\trigger\\ExampleEventEntry.kt",
+ "file": "src/main/kotlin/com/typewritermc/example/entries/trigger/ExampleEventEntry.kt",
"content": "@Entry(\"example_event\", \"An example event entry.\", Colors.YELLOW, \"material-symbols:bigtop-updates\")\nclass ExampleEventEntry(\n override val id: String = \"\",\n override val name: String = \"\",\n override val triggers: List][> = emptyList(),\n) : EventEntry"
},
"event_entry_listener": {
- "file": "src\\main\\kotlin\\com\\typewritermc\\example\\entries\\trigger\\ExampleEventEntry.kt",
+ "file": "src/main/kotlin/com/typewritermc/example/entries/trigger/ExampleEventEntry.kt",
"content": "// Must be scoped to be public\n@EntryListener(ExampleEventEntry::class)\nfun onEvent(event: SomeBukkitEvent, query: Query) {\n // Do something\n val entries = query.find() // Find all the entries of this type, for more information see the Query section\n // Do something with the entries, for example trigger them\n entries triggerAllFor event.player\n}"
- },
- "initializer": {
- "file": "src\\main\\kotlin\\com\\typewritermc\\example\\ExampleInitializer.kt",
- "content": "import com.typewritermc.core.extension.Initializable\nimport com.typewritermc.core.extension.annotations.Initializer\n\n@Initializer\nobject ExampleInitializer : Initializable {\n override fun initialize() {\n // Do something when the extension is initialized\n }\n\n override fun shutdown() {\n // Do something when the extension is shutdown\n }\n}"
}
}
\ No newline at end of file
diff --git a/engine/build.gradle.kts b/engine/build.gradle.kts
index ac2d7b17e1..5356627410 100644
--- a/engine/build.gradle.kts
+++ b/engine/build.gradle.kts
@@ -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")
}
diff --git a/engine/engine-core/src/main/kotlin/com/typewritermc/core/utils/DurationFormat.kt b/engine/engine-core/src/main/kotlin/com/typewritermc/core/utils/DurationFormat.kt
new file mode 100644
index 0000000000..ee222ba7a0
--- /dev/null
+++ b/engine/engine-core/src/main/kotlin/com/typewritermc/core/utils/DurationFormat.kt
@@ -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()
+}
\ No newline at end of file
diff --git a/engine/engine-paper/build.gradle.kts b/engine/engine-paper/build.gradle.kts
index b4e2829e1c..12eb6268be 100644
--- a/engine/engine-paper/build.gradle.kts
+++ b/engine/engine-paper/build.gradle.kts
@@ -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 {
diff --git a/engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/TypewriterPaperPlugin.kt b/engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/TypewriterPaperPlugin.kt
index c66228ab99..c0b158ec9f 100644
--- a/engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/TypewriterPaperPlugin.kt
+++ b/engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/TypewriterPaperPlugin.kt
@@ -172,6 +172,10 @@ class TypewriterPaperPlugin : KotlinPlugin(), KoinComponent {
get().load()
get().load()
CustomCommandEntry.registerAll()
+
+ if (server.pluginManager.getPlugin("PlaceholderAPI") != null) {
+ PlaceholderExpansion.load()
+ }
}
suspend fun unload() {
@@ -184,6 +188,10 @@ class TypewriterPaperPlugin : KotlinPlugin(), KoinComponent {
get().unload()
get().unload()
+ if (server.pluginManager.getPlugin("PlaceholderAPI") != null) {
+ PlaceholderExpansion.unload()
+ }
+
// Needs to be last, as it will unload the classLoader
get().unload()
}
diff --git a/engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/entry/PlaceholderEntry.kt b/engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/entry/PlaceholderEntry.kt
index 3c4a3e946a..d667b42a88 100644
--- a/engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/entry/PlaceholderEntry.kt
+++ b/engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/entry/PlaceholderEntry.kt
@@ -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, Any>
+) {
+ fun getArgument(reference: ArgumentReference): T {
+ val value = arguments[reference]
+ return reference.type.cast(value)
+ }
+
+ fun hasArgument(reference: ArgumentReference): Boolean {
+ return arguments.containsKey(reference)
+ }
+
+ operator fun ArgumentReference.invoke(): T {
+ return getArgument(this)
+ }
+}
+
+interface PlaceholderArgument {
+ fun parse(player: Player?, argument: String): Result
+}
+
+class ArgumentReference(
+ val id: String = UUID.randomUUID().toString(),
+ val type: KClass,
+) {
+ 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(
+ val name: String,
+ val reference: ArgumentReference,
+ val argument: PlaceholderArgument,
+ val children: List
+) : PlaceholderNode
+
+class PlaceholderParser(
+ val nodes: List,
+) {
+ fun parse(player: Player?, arguments: List): String? {
+ val parsedArguments = mutableMapOf, Any>()
+ var currentNodes = nodes
+ for (argument in arguments) {
+ val nextNodes = mutableListOf()
+ 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()
+ if (suppliers.isEmpty()) {
+ return null
+ }
+
+ return suppliers.first().supplier.supply(context)
+ }
+}
+
+class PlaceholderNodeBuilder {
+ internal val nodes = mutableListOf()
+ 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 = PlaceholderNodeBuilder.(ArgumentReference) -> Unit
+
+fun PlaceholderNodeBuilder.argument(
+ name: String,
+ type: KClass,
+ argument: PlaceholderArgument,
+ builder: ArgumentBuilder,
+) {
+ 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 {
+ override fun parse(player: Player?, argument: String): Result {
+ if (argument != name) return failure("Literal '$name' didn't match argument '$argument'")
+ return ok(argument)
+ }
+}
+
+fun PlaceholderNodeBuilder.string(name: String, builder: ArgumentBuilder) =
+ argument(name, String::class, StringArgument, builder)
+
+object StringArgument : PlaceholderArgument {
+ override fun parse(player: Player?, argument: String): Result {
+ return ok(argument)
+ }
+}
\ No newline at end of file
diff --git a/engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/entry/entries/EntityEntry.kt b/engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/entry/entries/EntityEntry.kt
index 982deded9e..7e24c1d3cf 100644
--- a/engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/entry/entries/EntityEntry.kt
+++ b/engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/entry/entries/EntityEntry.kt
@@ -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
@@ -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) }
+ }
}
/**
diff --git a/engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/entry/entries/FactEntry.kt b/engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/entry/entries/FactEntry.kt
index 620041dc18..0fe2738a02 100644
--- a/engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/entry/entries/FactEntry.kt
+++ b/engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/entry/entries/FactEntry.kt
@@ -4,15 +4,23 @@ import com.typewritermc.core.entries.Ref
import com.typewritermc.core.extension.annotations.Help
import com.typewritermc.core.extension.annotations.MultiLine
import com.typewritermc.core.extension.annotations.Tags
-import com.typewritermc.engine.paper.entry.PlaceholderEntry
-import com.typewritermc.engine.paper.entry.StaticEntry
+import com.typewritermc.core.utils.formatCompact
+import com.typewritermc.engine.paper.entry.*
import com.typewritermc.engine.paper.facts.FactData
import com.typewritermc.engine.paper.facts.FactDatabase
import com.typewritermc.engine.paper.facts.FactId
+import com.typewritermc.engine.paper.loader.serializers.DurationSerializer
import lirand.api.extensions.server.server
+import org.apache.commons.lang3.time.DurationFormatUtils
+import org.apache.commons.lang3.time.DurationUtils
import org.bukkit.entity.Player
import org.koin.java.KoinJavaComponent.get
+import java.time.LocalDateTime
+import java.time.ZoneOffset
+import java.time.format.DateTimeFormatter
import java.util.*
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.seconds
@Tags("fact")
interface FactEntry : StaticEntry {
@@ -31,7 +39,7 @@ interface FactEntry : StaticEntry {
entry.groupId(player) ?: return null
} else {
// If no group entry is set, we assume that the player is the group for backwards compatibility
- com.typewritermc.engine.paper.entry.entries.GroupId(player.uniqueId)
+ GroupId(player.uniqueId)
}
return FactId(id, groupId)
@@ -61,14 +69,31 @@ interface ReadableFactEntry : FactEntry, PlaceholderEntry {
*/
fun readSinglePlayer(player: Player): FactData
- override fun display(player: Player?): String? {
- if (player == null) return null
- return readForPlayersGroup(player).value.toString()
+ override fun parser(): PlaceholderParser = placeholderParser {
+ literal("time") {
+ literal("lastUpdated") {
+ literal("relative") {
+ supplyPlayer { player ->
+ val lastUpdate = readForPlayersGroup(player).lastUpdate
+ val now = LocalDateTime.now()
+ val difference = (now.toEpochSecond(ZoneOffset.UTC) - lastUpdate.toEpochSecond(ZoneOffset.UTC)).seconds
+
+ "${difference.formatCompact()} ago"
+ }
+ }
+ supplyPlayer { player ->
+ readForPlayersGroup(player).lastUpdate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
+ }
+ }
+ }
+ supplyPlayer { player ->
+ readForPlayersGroup(player).value.toString()
+ }
}
}
@Tags("writable-fact")
-interface WritableFactEntry : com.typewritermc.engine.paper.entry.entries.FactEntry {
+interface WritableFactEntry : FactEntry {
fun write(player: Player, value: Int) {
val factId = identifier(player) ?: return
write(factId, value)
@@ -78,10 +103,10 @@ interface WritableFactEntry : com.typewritermc.engine.paper.entry.entries.FactEn
}
@Tags("cachable-fact")
-interface CachableFactEntry : com.typewritermc.engine.paper.entry.entries.ReadableFactEntry,
- com.typewritermc.engine.paper.entry.entries.WritableFactEntry {
+interface CachableFactEntry : ReadableFactEntry,
+ WritableFactEntry {
- override fun readForGroup(groupId: com.typewritermc.engine.paper.entry.entries.GroupId): FactData {
+ override fun readForGroup(groupId: GroupId): FactData {
return read(FactId(id, groupId))
}
@@ -104,11 +129,11 @@ interface CachableFactEntry : com.typewritermc.engine.paper.entry.entries.Readab
}
@Tags("persistable-fact")
-interface PersistableFactEntry : com.typewritermc.engine.paper.entry.entries.CachableFactEntry {
+interface PersistableFactEntry : CachableFactEntry {
fun canPersist(id: FactId, data: FactData): Boolean = true
}
@Tags("expirable-fact")
-interface ExpirableFactEntry : com.typewritermc.engine.paper.entry.entries.CachableFactEntry {
+interface ExpirableFactEntry : CachableFactEntry {
fun hasExpired(id: FactId, data: FactData): Boolean = false
}
\ No newline at end of file
diff --git a/engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/entry/entries/LinesEntry.kt b/engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/entry/entries/LinesEntry.kt
index 20692aa4e1..2de8d92ed8 100644
--- a/engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/entry/entries/LinesEntry.kt
+++ b/engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/entry/entries/LinesEntry.kt
@@ -2,7 +2,7 @@ package com.typewritermc.engine.paper.entry.entries
import com.typewritermc.core.entries.PriorityEntry
import com.typewritermc.core.extension.annotations.Tags
-import com.typewritermc.engine.paper.entry.PlaceholderEntry
+import com.typewritermc.engine.paper.entry.*
import com.typewritermc.engine.paper.extensions.placeholderapi.parsePlaceholders
import org.bukkit.entity.Player
import kotlin.reflect.KClass
@@ -15,7 +15,9 @@ interface LinesEntry : EntityData, AudienceEntry, PlaceholderEntr
*/
fun lines(player: Player): String
- override fun display(player: Player?): String? = player?.let { lines(it) }
+ override fun parser(): PlaceholderParser = placeholderParser {
+ supplyPlayer { player -> lines(player) }
+ }
override fun type(): KClass = LinesProperty::class
override fun build(player: Player): LinesProperty = LinesProperty(lines(player))
diff --git a/engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/entry/entries/QuestEntry.kt b/engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/entry/entries/QuestEntry.kt
index bd689660f1..f6efd5335c 100644
--- a/engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/entry/entries/QuestEntry.kt
+++ b/engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/entry/entries/QuestEntry.kt
@@ -32,7 +32,14 @@ interface QuestEntry : AudienceFilterEntry, PlaceholderEntry {
val facts: List][> get() = emptyList()
fun questStatus(player: Player): QuestStatus
- override fun display(player: Player?): String = displayName.get(player)?.parsePlaceholders(player) ?: ""
+ fun display(player: Player): String {
+ return displayName.get(player).parsePlaceholders(player)
+ }
+
+ override fun parser(): PlaceholderParser = placeholderParser {
+ supplyPlayer { player -> display(player) }
+ }
+
override fun display(): AudienceFilter = QuestAudienceFilter(
ref()
)
@@ -73,7 +80,7 @@ interface ObjectiveEntry : AudienceFilterEntry, PlaceholderEntry, PriorityEntry
)
}
- override fun display(player: Player?): String {
+ fun display(player: Player?): String {
val text = when {
player == null -> inactiveObjectiveDisplay
criteria.matches(player) -> showingObjectiveDisplay
@@ -83,6 +90,10 @@ interface ObjectiveEntry : AudienceFilterEntry, PlaceholderEntry, PriorityEntry
return text.asMiniWithResolvers(parsed("display", display)).asMini().parsePlaceholders(player)
}
+
+ override fun parser(): PlaceholderParser = placeholderParser {
+ supply { player -> display(player) }
+ }
}
diff --git a/engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/entry/entries/SidebarEntry.kt b/engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/entry/entries/SidebarEntry.kt
index d75bafb77b..fa72dcffc6 100644
--- a/engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/entry/entries/SidebarEntry.kt
+++ b/engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/entry/entries/SidebarEntry.kt
@@ -33,10 +33,14 @@ interface SidebarEntry : AudienceFilterEntry, PlaceholderEntry, PriorityEntry {
@Placeholder
val title: Var
- override fun display(player: Player?): String? {
+ fun display(player: Player?): String {
return title.get(player)?.parsePlaceholders(player) ?: ""
}
+ override fun parser(): PlaceholderParser = placeholderParser {
+ supply { player -> display(player) }
+ }
+
override fun display(): AudienceFilter = SidebarFilter(ref()) { player ->
PlayerSidebarDisplay(player, SidebarFilter::class, ref())
}
@@ -66,7 +70,7 @@ private class PlayerSidebarDisplay(
override fun initialize() {
super.initialize()
val sidebar = ref.get() ?: return
- val title = sidebar.display(player) ?: ""
+ val title = sidebar.display(player)
createSidebar(title)
}
@@ -80,7 +84,7 @@ private class PlayerSidebarDisplay(
super.tick()
val sidebar = ref.get() ?: return
- val title = sidebar.display(player) ?: ""
+ val title = sidebar.display(player)
val lines = lines
.filter { player.inAudience(it) }
diff --git a/engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/entry/entries/SoundEntry.kt b/engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/entry/entries/SoundEntry.kt
index 02b0806731..97142a8632 100644
--- a/engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/entry/entries/SoundEntry.kt
+++ b/engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/entry/entries/SoundEntry.kt
@@ -1,8 +1,7 @@
package com.typewritermc.engine.paper.entry.entries
import com.typewritermc.core.extension.annotations.Tags
-import com.typewritermc.engine.paper.entry.PlaceholderEntry
-import com.typewritermc.engine.paper.entry.StaticEntry
+import com.typewritermc.engine.paper.entry.*
import net.kyori.adventure.sound.Sound
import org.bukkit.entity.Player
@@ -10,7 +9,9 @@ import org.bukkit.entity.Player
interface SoundIdEntry : StaticEntry, PlaceholderEntry {
val soundId: String
- override fun display(player: Player?): String? = soundId
+ override fun parser(): PlaceholderParser = placeholderParser {
+ supply { soundId }
+ }
}
@Tags("sound_source")
diff --git a/engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/extensions/placeholderapi/PlaceholderExpansion.kt b/engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/extensions/placeholderapi/PlaceholderExpansion.kt
index 335b158acc..7b6da3a8be 100644
--- a/engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/extensions/placeholderapi/PlaceholderExpansion.kt
+++ b/engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/extensions/placeholderapi/PlaceholderExpansion.kt
@@ -1,7 +1,9 @@
package com.typewritermc.engine.paper.extensions.placeholderapi
import com.typewritermc.core.entries.Query
+import com.typewritermc.core.utils.Reloadable
import com.typewritermc.engine.paper.entry.PlaceholderEntry
+import com.typewritermc.engine.paper.entry.PlaceholderParser
import lirand.api.extensions.server.server
import me.clip.placeholderapi.PlaceholderAPI
import me.clip.placeholderapi.expansion.PlaceholderExpansion
@@ -14,11 +16,13 @@ import org.bukkit.plugin.Plugin
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.util.*
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.ConcurrentMap
private val noneTracked by snippet("quest.tracked.none", "None tracked")
-object PlaceholderExpansion : PlaceholderExpansion(), KoinComponent {
+object PlaceholderExpansion : PlaceholderExpansion(), KoinComponent, Reloadable {
private val plugin: Plugin by inject()
override fun getIdentifier(): String = "typewriter"
@@ -28,6 +32,10 @@ object PlaceholderExpansion : PlaceholderExpansion(), KoinComponent {
override fun persist(): Boolean = true
+ override fun load() = cachedParsers.clear()
+ override fun unload() = cachedParsers.clear()
+
+ private val cachedParsers: ConcurrentHashMap = ConcurrentHashMap()
override fun onPlaceholderRequest(player: Player?, params: String): String? {
if (params == "tracked_quest") {
if (player == null) return null
@@ -40,8 +48,15 @@ object PlaceholderExpansion : PlaceholderExpansion(), KoinComponent {
.ifBlank { noneTracked }
}
- val entry: PlaceholderEntry = Query.findById(params) ?: Query.findByName(params) ?: return null
- return entry.display(player)
+ val parts = params.split(':')
+ val id = parts[0]
+
+ val parser = cachedParsers.getOrPut(id) {
+ val entry: PlaceholderEntry = Query.findById(id) ?: Query.findByName(id) ?: return null
+ entry.parser()
+ }
+
+ return parser.parse(player, parts.subList(1, parts.size))
}
}
diff --git a/engine/engine-paper/src/test/kotlin/com/typewritermc/utils/DurationParserTest.kt b/engine/engine-paper/src/test/kotlin/com/typewritermc/utils/DurationParserTest.kt
index 9598315dae..e6885fd4fa 100644
--- a/engine/engine-paper/src/test/kotlin/com/typewritermc/utils/DurationParserTest.kt
+++ b/engine/engine-paper/src/test/kotlin/com/typewritermc/utils/DurationParserTest.kt
@@ -1,59 +1,43 @@
package com.typewritermc.utils
import com.typewritermc.engine.paper.utils.DurationParser
+import io.kotest.core.spec.style.FunSpec
+import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
-internal class DurationParserTest {
-
- @Test
- fun `When a single duration is provided expect it to be parsed correctly`() {
- Assertions.assertEquals(1, DurationParser.parse("1ms").inWholeMilliseconds)
- Assertions.assertEquals(123, DurationParser.parse("123sec").inWholeSeconds)
- Assertions.assertEquals(52, DurationParser.parse("52 m").inWholeMinutes)
- Assertions.assertEquals(23, DurationParser.parse("23hr").inWholeHours)
- Assertions.assertEquals(31, DurationParser.parse("31d").inWholeDays)
- Assertions.assertEquals(12, DurationParser.parse("12 w").inWholeDays / 7)
- Assertions.assertEquals(5, DurationParser.parse("5 months").inWholeDays / 30)
- Assertions.assertEquals(2, DurationParser.parse("2yr").inWholeDays / 365)
- }
-
- @Test
- fun `When multiple durations are provided expect the sum of the durations as result`() {
- Assertions.assertEquals(
- 123 + 52 * 60 + 23 * 60 * 60 + 31 * 24 * 60 * 60 + 12 * 7 * 24 * 60 * 60 + 5 * 30 * 24 * 60 * 60 + 2 * 365 * 24 * 60 * 60,
- DurationParser.parse("123sec 52 m 23hr 31d 12 w 5 months 2yr").inWholeSeconds
- )
-
- Assertions.assertEquals(
- 60 + 20,
- DurationParser.parse("1 hr 20 mins").inWholeMinutes
- )
-
- Assertions.assertEquals(
- 60 + 20,
- DurationParser.parse("1h20m0s").inWholeMinutes
- )
- }
-
- @Test
- fun `When a duration string contains other accepted characters expect it to parse`() {
- Assertions.assertEquals(
- 27681,
- DurationParser.parse("27,681 ms").inWholeMilliseconds
- )
-
- Assertions.assertEquals(
- 27681,
- DurationParser.parse("27_681 milliseconds").inWholeMilliseconds
- )
- }
-
- @Test
- fun `When an empty duration string is given expect the resulting duration to be zero`() {
- Assertions.assertEquals(
- 0,
- DurationParser.parse("").inWholeSeconds
- )
- }
-}
\ No newline at end of file
+class DurationParserTest : FunSpec({
+ test("single duration parsing") {
+ DurationParser.parse("1ms").inWholeMilliseconds shouldBe 1
+ DurationParser.parse("123sec").inWholeSeconds shouldBe 123
+ DurationParser.parse("52 m").inWholeMinutes shouldBe 52
+ DurationParser.parse("23hr").inWholeHours shouldBe 23
+ DurationParser.parse("31d").inWholeDays shouldBe 31
+ DurationParser.parse("12 w").inWholeDays / 7 shouldBe 12
+ DurationParser.parse("5 months").inWholeDays / 30 shouldBe 5
+ DurationParser.parse("2yr").inWholeDays / 365 shouldBe 2
+ }
+
+ test("multiple duration sum parsing") {
+ // Complex duration sum
+ DurationParser.parse("123sec 52 m 23hr 31d 12 w 5 months 2yr").inWholeSeconds shouldBe
+ 123 + 52 * 60 + 23 * 60 * 60 + 31 * 24 * 60 * 60 +
+ 12 * 7 * 24 * 60 * 60 + 5 * 30 * 24 * 60 * 60 +
+ 2 * 365 * 24 * 60 * 60
+
+ // Hour and minutes
+ DurationParser.parse("1 hr 20 mins").inWholeMinutes shouldBe 80
+
+ // Compact notation
+ DurationParser.parse("1h20m0s").inWholeMinutes shouldBe 80
+ }
+
+ test("duration parsing with special characters") {
+ DurationParser.parse("27,681 ms").inWholeMilliseconds shouldBe 27681
+ DurationParser.parse("27_681 milliseconds").inWholeMilliseconds shouldBe 27681
+ }
+
+ test("empty duration string parsing") {
+ DurationParser.parse("").inWholeSeconds shouldBe 0
+ }
+})
\ No newline at end of file
diff --git a/engine/engine-paper/src/test/kotlin/com/typewritermc/utils/PlaceholderParserTest.kt b/engine/engine-paper/src/test/kotlin/com/typewritermc/utils/PlaceholderParserTest.kt
new file mode 100644
index 0000000000..bd7c8a3850
--- /dev/null
+++ b/engine/engine-paper/src/test/kotlin/com/typewritermc/utils/PlaceholderParserTest.kt
@@ -0,0 +1,204 @@
+package com.typewritermc.utils
+
+import com.typewritermc.engine.paper.entry.literal
+import com.typewritermc.engine.paper.entry.placeholderParser
+import com.typewritermc.engine.paper.entry.string
+import com.typewritermc.engine.paper.entry.supply
+import io.kotest.core.spec.style.FunSpec
+import io.kotest.matchers.shouldBe
+
+class PlaceholderParserTest : FunSpec({
+ test("Parser should parse with no arguments") {
+ val parser = placeholderParser {
+ supply {
+ "Hey"
+ }
+ }
+
+ parser.parse(null, emptyList()) shouldBe "Hey"
+ }
+
+ test("Parser with no argument should fail when given an argument") {
+ val parser = placeholderParser {
+ supply {
+ "Hey"
+ }
+ }
+
+ parser.parse(null, listOf("Hello")) shouldBe null
+ }
+
+ context("Literals") {
+ test("Parser with single literal should parse when given the literal, otherwise it should fail") {
+ val parser = placeholderParser {
+ literal("hello") {
+ supply {
+ "Hey"
+ }
+ }
+ }
+
+ parser.parse(null, listOf("hello")) shouldBe "Hey"
+ parser.parse(null, listOf("world")) shouldBe null
+ parser.parse(null, listOf("hello", "world")) shouldBe null
+ parser.parse(null, emptyList()) shouldBe null
+ }
+ test("Parser with multiple literals should only parse when all literals are supplied") {
+ val parser = placeholderParser {
+ literal("hello") {
+ supply {
+ "Hey"
+ }
+ }
+ literal("world") {
+ supply {
+ "World"
+ }
+ }
+ }
+
+ parser.parse(null, listOf("hello")) shouldBe "Hey"
+ parser.parse(null, listOf("world")) shouldBe "World"
+ parser.parse(null, listOf("something")) shouldBe null
+ parser.parse(null, listOf("hello", "world")) shouldBe null
+ parser.parse(null, emptyList()) shouldBe null
+ }
+ test("Parser with multiple nested literals should only parse the correct literals") {
+ val parser = placeholderParser {
+ literal("hello") {
+ literal("world") {
+ supply {
+ "Hey"
+ }
+ }
+ }
+ }
+
+ parser.parse(null, listOf("hello", "world")) shouldBe "Hey"
+ parser.parse(null, listOf("hello", "something")) shouldBe null
+ parser.parse(null, listOf("something", "world")) shouldBe null
+ parser.parse(null, emptyList()) shouldBe null
+ }
+ test("Parser with multiple executors should only supply the correct executor") {
+ val parser = placeholderParser {
+ literal("hello") {
+ supply {
+ "Hey"
+ }
+ }
+ supply {
+ "World"
+ }
+ }
+
+ parser.parse(null, listOf("hello")) shouldBe "Hey"
+ parser.parse(null, listOf("world")) shouldBe null
+ parser.parse(null, emptyList()) shouldBe "World"
+ }
+
+ test("Parser with overlapping literals will only parse where all arguments match") {
+ val parser = placeholderParser {
+ literal("hello") {
+ literal("world") {
+ supply {
+ "Hey"
+ }
+ }
+ supply {
+ "World"
+ }
+ }
+ literal("hello") {
+ literal("sun") {
+ supply {
+ "Sun"
+ }
+ }
+ supply {
+ "Impossible"
+ }
+ }
+ }
+
+ parser.parse(null, listOf("hello", "world")) shouldBe "Hey"
+ parser.parse(null, listOf("hello", "sun")) shouldBe "Sun"
+ parser.parse(null, listOf("hello", "something")) shouldBe null
+ parser.parse(null, listOf("hello")) shouldBe "World"
+ parser.parse(null, emptyList()) shouldBe null
+ }
+ }
+
+ context("String Argument") {
+ test("Parser with string argument should parse when given the argument, otherwise it should fail") {
+ val parser = placeholderParser {
+ string("name") { string ->
+ supply {
+ "Hey ${string()}"
+ }
+ }
+ }
+
+ parser.parse(null, listOf("bob")) shouldBe "Hey bob"
+ parser.parse(null, listOf("alice")) shouldBe "Hey alice"
+ parser.parse(null, emptyList()) shouldBe null
+ }
+
+ test("Parser with multiple nested string arguments should only parse when all arguments are supplied") {
+ val parser = placeholderParser {
+ string("name") { string ->
+ string("action") { action ->
+ supply {
+ "Hey ${string()}, ${action()}"
+ }
+ }
+ }
+ }
+
+ parser.parse(null, listOf("bob", "jump")) shouldBe "Hey bob, jump"
+ parser.parse(null, listOf("bob", "run")) shouldBe "Hey bob, run"
+ parser.parse(null, listOf("alice", "jump")) shouldBe "Hey alice, jump"
+ parser.parse(null, listOf("alice", "run")) shouldBe "Hey alice, run"
+ parser.parse(null, listOf("bob")) shouldBe null
+ parser.parse(null, emptyList()) shouldBe null
+ }
+
+ test("Parser with literal and string argument should only run the literal") {
+ val parser = placeholderParser {
+ literal("hello") {
+ supply {
+ "Hey"
+ }
+ }
+
+ string("name") { string ->
+ supply {
+ "Hey ${string()}"
+ }
+ }
+ }
+
+ parser.parse(null, listOf("hello")) shouldBe "Hey"
+ parser.parse(null, listOf("bob")) shouldBe "Hey bob"
+ parser.parse(null, emptyList()) shouldBe null
+ }
+
+ test("Parser with where multiple paths are possible should only use the first") {
+ val parser = placeholderParser {
+ string("name") { string ->
+ supply {
+ "Hey ${string()}"
+ }
+ }
+
+ string("name") { string ->
+ supply {
+ "Whats up ${string()}"
+ }
+ }
+ }
+
+ parser.parse(null, listOf("bob")) shouldBe "Hey bob"
+ parser.parse(null, emptyList()) shouldBe null
+ }
+ }
+})
\ No newline at end of file
diff --git a/extensions/BasicExtension/src/main/kotlin/com/typewritermc/basic/entries/audience/CinematicSkippableAudienceEntry.kt b/extensions/BasicExtension/src/main/kotlin/com/typewritermc/basic/entries/audience/CinematicSkippableAudienceEntry.kt
index 0c9f11b3b1..e78c9c15d6 100644
--- a/extensions/BasicExtension/src/main/kotlin/com/typewritermc/basic/entries/audience/CinematicSkippableAudienceEntry.kt
+++ b/extensions/BasicExtension/src/main/kotlin/com/typewritermc/basic/entries/audience/CinematicSkippableAudienceEntry.kt
@@ -6,12 +6,11 @@ import com.typewritermc.core.books.pages.Colors
import com.typewritermc.core.entries.Ref
import com.typewritermc.core.entries.ref
import com.typewritermc.core.extension.annotations.Entry
-import com.typewritermc.engine.paper.entry.PlaceholderEntry
+import com.typewritermc.engine.paper.entry.*
import com.typewritermc.engine.paper.entry.entries.AudienceEntry
import com.typewritermc.engine.paper.entry.entries.AudienceFilter
import com.typewritermc.engine.paper.entry.entries.AudienceFilterEntry
import com.typewritermc.engine.paper.entry.entries.Invertible
-import com.typewritermc.engine.paper.entry.findDisplay
import com.typewritermc.engine.paper.events.AsyncCinematicEndEvent
import org.bukkit.entity.Player
import org.bukkit.event.EventHandler
@@ -43,11 +42,13 @@ class CinematicSkippableAudienceEntry(
return CinematicSkippableAudienceDisplay(ref())
}
- override fun display(player: Player?): String? {
- val default = SkipConfirmationKey.SNEAK.keybind
- if (player == null) return default
- val display = ref().findDisplay() as? CinematicSkippableAudienceDisplay ?: return default
- return display.confirmationKey(player)?.keybind ?: default
+ override fun parser(): PlaceholderParser = placeholderParser {
+ supply { player ->
+ val default = SkipConfirmationKey.SNEAK.keybind
+ if (player == null) return@supply default
+ val display = ref().findDisplay() as? CinematicSkippableAudienceDisplay ?: return@supply default
+ display.confirmationKey(player)?.keybind ?: default
+ }
}
}
diff --git a/extensions/BasicExtension/src/main/kotlin/com/typewritermc/basic/entries/fact/CountdownFact.kt b/extensions/BasicExtension/src/main/kotlin/com/typewritermc/basic/entries/fact/CountdownFact.kt
index fad650747f..3b16b1f1ec 100644
--- a/extensions/BasicExtension/src/main/kotlin/com/typewritermc/basic/entries/fact/CountdownFact.kt
+++ b/extensions/BasicExtension/src/main/kotlin/com/typewritermc/basic/entries/fact/CountdownFact.kt
@@ -1,9 +1,11 @@
package com.typewritermc.basic.entries.fact
import com.typewritermc.core.books.pages.Colors
-import com.typewritermc.core.extension.annotations.Entry
import com.typewritermc.core.entries.Ref
import com.typewritermc.core.entries.emptyRef
+import com.typewritermc.core.extension.annotations.Entry
+import com.typewritermc.core.utils.formatCompact
+import com.typewritermc.engine.paper.entry.*
import com.typewritermc.engine.paper.entry.entries.ExpirableFactEntry
import com.typewritermc.engine.paper.entry.entries.GroupEntry
import com.typewritermc.engine.paper.entry.entries.PersistableFactEntry
@@ -11,7 +13,9 @@ import com.typewritermc.engine.paper.facts.FactData
import com.typewritermc.engine.paper.facts.FactId
import java.time.Duration
import java.time.LocalDateTime
+import java.time.format.DateTimeFormatter
import kotlin.math.max
+import kotlin.time.Duration.Companion.seconds
@Entry(
"countdown_fact",
@@ -52,4 +56,29 @@ class CountdownFact(
val timeDifference = Duration.between(data.lastUpdate, LocalDateTime.now())
return max(0, (data.value - timeDifference.seconds).toInt())
}
+
+ override fun parser(): PlaceholderParser = placeholderParser {
+ include(super.parser())
+
+ literal("time") {
+ literal("expires") {
+ literal("relative") {
+ supplyPlayer { player ->
+ val data = readForPlayersGroup(player)
+ val duration = data.value.seconds
+
+ duration.formatCompact()
+ }
+ }
+
+ supplyPlayer { player ->
+ val data = readForPlayersGroup(player)
+ val now = LocalDateTime.now()
+ val expireTime = now.plusSeconds(data.value.toLong())
+
+ expireTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
+ }
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/extensions/BasicExtension/src/main/kotlin/com/typewritermc/basic/entries/fact/CronFactEntry.kt b/extensions/BasicExtension/src/main/kotlin/com/typewritermc/basic/entries/fact/CronFactEntry.kt
index 5e48a57c3d..ecbce55102 100644
--- a/extensions/BasicExtension/src/main/kotlin/com/typewritermc/basic/entries/fact/CronFactEntry.kt
+++ b/extensions/BasicExtension/src/main/kotlin/com/typewritermc/basic/entries/fact/CronFactEntry.kt
@@ -1,17 +1,22 @@
package com.typewritermc.basic.entries.fact
import com.typewritermc.core.books.pages.Colors
-import com.typewritermc.core.extension.annotations.Entry
-import com.typewritermc.core.extension.annotations.Help
import com.typewritermc.core.entries.Ref
import com.typewritermc.core.entries.emptyRef
+import com.typewritermc.core.extension.annotations.Entry
+import com.typewritermc.core.extension.annotations.Help
+import com.typewritermc.core.utils.formatCompact
+import com.typewritermc.engine.paper.entry.*
import com.typewritermc.engine.paper.entry.entries.ExpirableFactEntry
import com.typewritermc.engine.paper.entry.entries.GroupEntry
import com.typewritermc.engine.paper.entry.entries.PersistableFactEntry
import com.typewritermc.engine.paper.facts.FactData
import com.typewritermc.engine.paper.facts.FactId
import com.typewritermc.engine.paper.utils.CronExpression
+import java.time.Duration
import java.time.LocalDateTime
+import java.time.format.DateTimeFormatter
+import kotlin.time.toKotlinDuration
@Entry("cron_fact", "Saved until a specified date, like (0 0 * * 1)", Colors.PURPLE, "mingcute:calendar-time-add-fill")
/**
@@ -33,4 +38,34 @@ class CronFactEntry(
override fun hasExpired(id: FactId, data: FactData): Boolean {
return cron.nextLocalDateTimeAfter(data.lastUpdate).isBefore(LocalDateTime.now())
}
+
+ override fun parser(): PlaceholderParser = placeholderParser {
+ include(super.parser())
+
+ literal("time") {
+ literal("expires") {
+ literal("relative") {
+ supplyPlayer { player ->
+ val lastUpdate = readForPlayersGroup(player).lastUpdate
+ val expires = cron.nextLocalDateTimeAfter(lastUpdate)
+ val now = LocalDateTime.now()
+ if (now.isAfter(expires)) {
+ return@supplyPlayer "now"
+ }
+ val difference =
+ Duration.between(lastUpdate, now).toKotlinDuration()
+
+ difference.formatCompact()
+ }
+ }
+
+ supplyPlayer { player ->
+ val lastUpdate = readForPlayersGroup(player).lastUpdate
+ val expires = cron.nextLocalDateTimeAfter(lastUpdate)
+
+ expires.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
+ }
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/extensions/BasicExtension/src/main/kotlin/com/typewritermc/basic/entries/fact/TimedFactEntry.kt b/extensions/BasicExtension/src/main/kotlin/com/typewritermc/basic/entries/fact/TimedFactEntry.kt
index ea07c0e405..88aa69c87f 100644
--- a/extensions/BasicExtension/src/main/kotlin/com/typewritermc/basic/entries/fact/TimedFactEntry.kt
+++ b/extensions/BasicExtension/src/main/kotlin/com/typewritermc/basic/entries/fact/TimedFactEntry.kt
@@ -1,10 +1,12 @@
package com.typewritermc.basic.entries.fact
import com.typewritermc.core.books.pages.Colors
-import com.typewritermc.core.extension.annotations.Entry
-import com.typewritermc.core.extension.annotations.Help
import com.typewritermc.core.entries.Ref
import com.typewritermc.core.entries.emptyRef
+import com.typewritermc.core.extension.annotations.Entry
+import com.typewritermc.core.extension.annotations.Help
+import com.typewritermc.core.utils.formatCompact
+import com.typewritermc.engine.paper.entry.*
import com.typewritermc.engine.paper.entry.entries.ExpirableFactEntry
import com.typewritermc.engine.paper.entry.entries.GroupEntry
import com.typewritermc.engine.paper.entry.entries.PersistableFactEntry
@@ -12,6 +14,10 @@ import com.typewritermc.engine.paper.facts.FactData
import com.typewritermc.engine.paper.facts.FactId
import java.time.Duration
import java.time.LocalDateTime
+import java.time.ZoneOffset
+import java.time.format.DateTimeFormatter
+import kotlin.time.Duration.Companion.seconds
+import kotlin.time.toKotlinDuration
@Entry("timed_fact", "Saved for a specified duration, like 20 minutes", Colors.PURPLE, "bi:stopwatch-fill")
/**
@@ -33,4 +39,34 @@ class TimedFactEntry(
override fun hasExpired(id: FactId, data: FactData): Boolean {
return LocalDateTime.now().isAfter(data.lastUpdate.plus(duration))
}
+
+ override fun parser(): PlaceholderParser = placeholderParser {
+ include(super.parser())
+
+ literal("time") {
+ literal("expires") {
+ literal("relative") {
+ supplyPlayer { player ->
+ val lastUpdate = readForPlayersGroup(player).lastUpdate
+ val expires = lastUpdate + duration
+ val now = LocalDateTime.now()
+ if (now.isAfter(expires)) {
+ return@supplyPlayer "now"
+ }
+ val difference =
+ Duration.between(lastUpdate, now).toKotlinDuration()
+
+ difference.formatCompact()
+ }
+ }
+
+ supplyPlayer { player ->
+ val lastUpdate = readForPlayersGroup(player).lastUpdate
+ val expires = lastUpdate + duration
+
+ expires.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
+ }
+ }
+ }
+ }
}
diff --git a/extensions/_DocsExtension/src/main/kotlin/com/typewritermc/example/entries/ExamplePlaceholderEntry.kt b/extensions/_DocsExtension/src/main/kotlin/com/typewritermc/example/entries/ExamplePlaceholderEntry.kt
new file mode 100644
index 0000000000..3ec93ff2cc
--- /dev/null
+++ b/extensions/_DocsExtension/src/main/kotlin/com/typewritermc/example/entries/ExamplePlaceholderEntry.kt
@@ -0,0 +1,54 @@
+package com.typewritermc.example.entries
+
+import com.typewritermc.engine.paper.entry.*
+
+//
+class SimpleExamplePlaceholderEntry(
+ override val id: String,
+ override val name: String,
+) : PlaceholderEntry {
+ override fun parser(): PlaceholderParser = placeholderParser {
+ supply { player ->
+ "Hello, ${player?.name ?: "World"}!"
+ }
+ }
+}
+//
+
+class LiteralExamplePlaceholderEntry(
+ override val id: String,
+ override val name: String,
+) : PlaceholderEntry {
+//
+ override fun parser(): PlaceholderParser = placeholderParser {
+ literal("greet") {
+ literal("enthusiastic") {
+ supply { player ->
+ "HEY HOW IS YOUR DAY, ${player?.name ?: "World"}!"
+ }
+ }
+ supply { player ->
+ "Hello, ${player?.name ?: "World"}"
+ }
+ }
+ supply {
+ "Standard text"
+ }
+ }
+//
+}
+
+class StringExamplePlaceholderEntry(
+ override val id: String,
+ override val name: String,
+) : PlaceholderEntry {
+//
+ override fun parser(): PlaceholderParser = placeholderParser {
+ string("name") { name ->
+ supply {
+ "Hello, ${name()}!"
+ }
+ }
+ }
+//
+}
\ No newline at end of file
]