Skip to content

Commit

Permalink
Add InputDialogueEntries
Browse files Browse the repository at this point in the history
  • Loading branch information
gabber235 committed Dec 16, 2024
1 parent 7bde95e commit 8621557
Show file tree
Hide file tree
Showing 5 changed files with 350 additions and 3 deletions.
4 changes: 2 additions & 2 deletions engine/engine-paper/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ plugins {
repositories {
mavenCentral()
// Floodgate
maven("https://repo.opencollab.dev/maven-snapshots/")
maven("https://repo.opencollab.dev/main/")
// PacketEvents, CommandAPI
maven("https://repo.codemc.io/repository/maven-releases/")
// PlaceholderAPI
Expand Down Expand Up @@ -59,7 +59,7 @@ dependencies {
compileOnlyApi("com.github.retrooper:packetevents-api:2.6.0-SNAPSHOT")
compileOnlyApi("com.github.retrooper:packetevents-spigot:2.6.0-SNAPSHOT")
compileOnly("me.clip:placeholderapi:2.11.6")
compileOnlyApi("org.geysermc.floodgate:api:2.2.0-SNAPSHOT")
compileOnlyApi("org.geysermc.floodgate:api:2.2.3-SNAPSHOT")

testImplementation("org.mockbukkit.mockbukkit:mockbukkit-v1.21:4.3.1")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package com.typewritermc.basic.entries.dialogue

import com.typewritermc.basic.entries.dialogue.messengers.input.BedrockInputDialogueDialogueMessenger
import com.typewritermc.basic.entries.dialogue.messengers.input.JavaInputDialogueDialogueMessenger
import com.typewritermc.core.books.pages.Colors
import com.typewritermc.core.entries.Ref
import com.typewritermc.core.entries.emptyRef
import com.typewritermc.core.extension.annotations.*
import com.typewritermc.core.interaction.EntryContextKey
import com.typewritermc.core.interaction.InteractionContext
import com.typewritermc.core.utils.failure
import com.typewritermc.core.utils.ok
import com.typewritermc.core.utils.replaceAll
import com.typewritermc.engine.paper.entry.Criteria
import com.typewritermc.engine.paper.entry.Modifier
import com.typewritermc.engine.paper.entry.TriggerableEntry
import com.typewritermc.engine.paper.entry.dialogue.DialogueMessenger
import com.typewritermc.engine.paper.entry.entries.ConstVar
import com.typewritermc.engine.paper.entry.entries.DialogueEntry
import com.typewritermc.engine.paper.entry.entries.SpeakerEntry
import com.typewritermc.engine.paper.entry.entries.Var
import com.typewritermc.engine.paper.snippets.snippet
import com.typewritermc.engine.paper.utils.isFloodgate
import org.bukkit.entity.Player
import java.time.Duration
import java.util.*
import kotlin.reflect.KClass

interface InputDialogueEntry : DialogueEntry {
@MultiLine
@Placeholder
@Colored
val text: Var<String>
val duration: Var<Duration>
}

@Entry("text_input_dialogue", "An input dialogue which excepts any text", Colors.CYAN, "fa6-solid:keyboard")
@ContextKeys(TextInputContextKeys::class)
class TextInputDialogueEntry(
override val id: String = "",
override val name: String = "",
override val criteria: List<Criteria> = emptyList(),
override val modifiers: List<Modifier> = emptyList(),
override val triggers: List<Ref<TriggerableEntry>> = emptyList(),
override val speaker: Ref<SpeakerEntry> = emptyRef(),
override val text: Var<String> = ConstVar(""),
override val duration: Var<Duration> = ConstVar(Duration.ZERO),
) : InputDialogueEntry {
override fun messenger(player: Player, context: InteractionContext): DialogueMessenger<*> {
return messenger(player, context, this, TextInputContextKeys.TEXT) { ok(it) }
}
}

enum class TextInputContextKeys(override val klass: KClass<*>) : EntryContextKey {
@KeyType(String::class)
TEXT(String::class),
}

val integerInputNotAIntegerMessage: String by snippet(
"dialogue.input.error.integer.not_a_integer",
"<red>Input must be an integer"
)
val numberInputOutOfRangeMessage: String by snippet(
"dialogue.input.error.number.out_of_range",
"<red>Input must be between <min> and <max>"
)

@Entry("integer_input_dialogue", "An input dialogue which excepts an integer", Colors.CYAN, "fa6-solid:keyboard")
@ContextKeys(IntegerInputContextKeys::class)
class IntegerInputDialogueEntry(
override val id: String = "",
override val name: String = "",
override val criteria: List<Criteria> = emptyList(),
override val modifiers: List<Modifier> = emptyList(),
override val triggers: List<Ref<TriggerableEntry>> = emptyList(),
override val speaker: Ref<SpeakerEntry> = emptyRef(),
override val text: Var<String> = ConstVar(""),
override val duration: Var<Duration> = ConstVar(Duration.ZERO),
val range: Optional<ClosedRange<Int>> = Optional.empty(),
) : InputDialogueEntry {
override fun messenger(player: Player, context: InteractionContext): DialogueMessenger<*> {
return messenger(player, context, this, IntegerInputContextKeys.NUMBER) parser@{
val number = it.toIntOrNull() ?: return@parser failure(integerInputNotAIntegerMessage)
if (range.isPresent && number !in range.get()) {
return@parser failure(
numberInputOutOfRangeMessage
.replaceAll(
"<min>" to range.get().start.toString(),
"<max>" to range.get().endInclusive.toString()
)
)
}

ok(number)
}
}
}

enum class IntegerInputContextKeys(override val klass: KClass<*>) : EntryContextKey {
@KeyType(Int::class)
NUMBER(Int::class),
}

val doubleInputNotADoubleMessage: String by snippet(
"dialogue.input.error.double.not_a_double",
"<red>Input must be a double"
)

@Entry("double_input_dialogue", "An input dialogue which excepts a double", Colors.CYAN, "fa6-solid:keyboard")
@ContextKeys(DoubleInputContextKeys::class)
class DoubleInputDialogueEntry(
override val id: String = "",
override val name: String = "",
override val criteria: List<Criteria> = emptyList(),
override val modifiers: List<Modifier> = emptyList(),
override val triggers: List<Ref<TriggerableEntry>> = emptyList(),
override val speaker: Ref<SpeakerEntry> = emptyRef(),
override val text: Var<String> = ConstVar(""),
override val duration: Var<Duration> = ConstVar(Duration.ZERO),
val range: Optional<ClosedRange<Double>> = Optional.empty(),
) : InputDialogueEntry {
override fun messenger(player: Player, context: InteractionContext): DialogueMessenger<*> {
return messenger(player, context, this, DoubleInputContextKeys.NUMBER) parser@{
val number = it.toDoubleOrNull() ?: return@parser failure(doubleInputNotADoubleMessage)
if (range.isPresent && number !in range.get()) {
return@parser failure(
numberInputOutOfRangeMessage.replaceAll(
"<min>" to range.get().start.toString(),
"<max>" to range.get().endInclusive.toString()
)
)
}

ok(number)
}
}
}

enum class DoubleInputContextKeys(override val klass: KClass<*>) : EntryContextKey {
@KeyType(Double::class)
NUMBER(Double::class),
}

private fun <T : Any> messenger(
player: Player,
context: InteractionContext,
entry: InputDialogueEntry,
key: EntryContextKey,
parser: (String) -> Result<T>,
): DialogueMessenger<InputDialogueEntry> {
return if (player.isFloodgate) {
BedrockInputDialogueDialogueMessenger(player, context, entry, key, parser)
} else {
JavaInputDialogueDialogueMessenger(player, context, entry, key, parser)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.typewritermc.basic.entries.dialogue.messengers.input

import com.typewritermc.basic.entries.dialogue.InputDialogueEntry
import com.typewritermc.core.interaction.EntryContextKey
import com.typewritermc.core.interaction.InteractionContext
import com.typewritermc.engine.paper.entry.dialogue.DialogueMessenger
import com.typewritermc.engine.paper.entry.dialogue.MessengerState
import com.typewritermc.engine.paper.extensions.placeholderapi.parsePlaceholders
import com.typewritermc.engine.paper.utils.legacy
import org.bukkit.entity.Player
import org.geysermc.cumulus.form.CustomForm

class BedrockInputDialogueDialogueMessenger<T : Any>(
player: Player,
context: InteractionContext,
entry: InputDialogueEntry,
private val key: EntryContextKey,
private val parser: (String) -> Result<T>,
) : DialogueMessenger<InputDialogueEntry>(player, context, entry) {

override fun init() {
super.init()
sendForm()
}

private fun sendForm() {
org.geysermc.floodgate.api.FloodgateApi.getInstance().sendForm(
player.uniqueId,
CustomForm.builder()
.title("<bold>${entry.speakerDisplayName.get(player).parsePlaceholders(player)}</bold>".legacy())
.label("${entry.text.get(player).parsePlaceholders(player).legacy()}\n\n")
.input("Value")
.closedOrInvalidResultHandler { _, _ ->
state = MessengerState.CANCELLED
}
.validResultHandler { _, response ->
val input = response.asInput()
if (input == null) {
sendForm()
return@validResultHandler
}
val value = parser(input)
if (value.isFailure) {
sendForm()
return@validResultHandler
}
val data = value.getOrNull() ?: return@validResultHandler
context[entry, key] = data
state = MessengerState.FINISHED
}
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package com.typewritermc.basic.entries.dialogue.messengers.input

import com.typewritermc.basic.entries.dialogue.InputDialogueEntry
import com.typewritermc.core.interaction.EntryContextKey
import com.typewritermc.core.interaction.InteractionContext
import com.typewritermc.engine.paper.entry.dialogue.DialogueMessenger
import com.typewritermc.engine.paper.entry.dialogue.MessengerState
import com.typewritermc.engine.paper.entry.dialogue.TickContext
import com.typewritermc.engine.paper.entry.dialogue.typingDurationType
import com.typewritermc.engine.paper.extensions.placeholderapi.parsePlaceholders
import com.typewritermc.engine.paper.interaction.chatHistory
import com.typewritermc.engine.paper.snippets.snippet
import com.typewritermc.engine.paper.utils.*
import io.papermc.paper.event.player.AsyncChatEvent
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder
import org.bukkit.entity.Player
import org.bukkit.event.EventHandler
import java.time.Duration

val inputFormat: String by snippet(
"dialogue.input.format", """
|<gray><st>${" ".repeat(60)}</st>
|
|<gray><padding>[ <bold><speaker></bold><reset><gray> ]
|
|<message>
|
|<gray><info_padding><info_text>
|<gray><st>${" ".repeat(60)}</st>
""".trimMargin()
)

val inputInfoText: String by snippet("dialogue.input.info", "Type answer in chat")

val inputPadding: String by snippet("dialogue.input.padding", " ")
val inputMinLength: Int by snippet("dialogue.input.minLength", 1)
val inputMaxLineLength: Int by snippet("dialogue.input.maxLineLength", 40)

class JavaInputDialogueDialogueMessenger<T : Any>(
player: Player,
context: InteractionContext,
entry: InputDialogueEntry,
private val key: EntryContextKey,
private val parser: (String) -> Result<T>,
) :
DialogueMessenger<InputDialogueEntry>(player, context, entry) {

private var speakerDisplayName = ""
private var text = ""
private var infoText = ""
private var typingDuration = Duration.ZERO
private var playedTime = Duration.ZERO

override var isCompleted: Boolean
// We don't want to be able to complete the dialogue if the player clicks on a npc.
get() {
if (!infoText.startsWith("<red>")) {
infoText = "<red>$infoText"
}
return state == MessengerState.FINISHED
}
set(value) {
playedTime = if (!value) Duration.ZERO
else typingDuration
}

override fun init() {
super.init()

speakerDisplayName = entry.speakerDisplayName.get(player).parsePlaceholders(player)
text = entry.text.get(player).parsePlaceholders(player)
infoText = inputInfoText
typingDuration = typingDurationType.totalDuration(text.stripped(), entry.duration.get(player))
}

@EventHandler
private fun onPlayerChat(event: AsyncChatEvent) {
if (event.player.uniqueId != player.uniqueId) return
event.isCancelled = true
val message = event.message().plainText()
val result = parser(message)
if (result.isFailure) {
infoText = result.exceptionOrNull()?.message ?: "<red>Invalid input"
player.sendInputDialogue()
return
}

val value = result.getOrNull() ?: return
context[entry, key] = value
state = MessengerState.FINISHED
}

private val percentage: Double
get() = if (!typingDuration.isZero) playedTime.toMillis().toDouble() / typingDuration.toMillis() else 1.2

override fun tick(context: TickContext) {
if (state != MessengerState.RUNNING) return
playedTime += context.deltaTime

val playedTicks = playedTime.toTicks()

// When the messages is send we don't want to keep sending the message every tick.
// However, we only start reducing after the message has been displayed fully.
if (percentage > 1.1) {
// Change in highlight color
val shouldDisplay = playedTicks % 50 == 0L
if (!shouldDisplay) return
}

player.sendInputDialogue()
}

private fun Player.sendInputDialogue() {
val rawText = text.stripped()
val resultingLines = rawText.limitLineLength(inputMaxLineLength).lineCount

val message = text.asPartialFormattedMini(
percentage,
padding = inputPadding,
minLines = inputMinLength.coerceAtLeast(resultingLines),
maxLineLength = inputMaxLineLength
)

val charactersLeft = (50 - infoText.stripped().length).coerceAtLeast(0)
val charactersPadding = charactersLeft / 2

val component = inputFormat.asMiniWithResolvers(
Placeholder.parsed("speaker", speakerDisplayName),
Placeholder.component("message", message),
Placeholder.parsed("info_text", infoText),
Placeholder.unparsed("info_padding", " ".repeat(charactersPadding)),
Placeholder.parsed("padding", inputPadding)
)

val componentWithDarkMessages = chatHistory.composeDarkMessage(component)
sendMessage(componentWithDarkMessages)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class TypewriterModulePlugin : Plugin<Project> {
}
// Add Geyser repository
repositories.maven {
it.setUrl("https://repo.opencollab.dev/maven-snapshots/")
it.setUrl("https://repo.opencollab.dev/main/")
}
}
}
Expand Down

0 comments on commit 8621557

Please sign in to comment.