feat: Add /firm timer command
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
package moe.nea.firmament.mixins;
|
||||||
|
|
||||||
|
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
|
||||||
|
import com.llamalad7.mixinextras.sugar.Local;
|
||||||
|
import net.fabricmc.fabric.impl.command.client.ClientCommandInternals;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
|
||||||
|
@Mixin(ClientCommandInternals.class)
|
||||||
|
public class AlwaysDisplayFirmamentClientCommandErrors {
|
||||||
|
@ModifyExpressionValue(method = "executeCommand", at = @At(value = "INVOKE", target = "Lnet/fabricmc/fabric/impl/command/client/ClientCommandInternals;isIgnoredException(Lcom/mojang/brigadier/exceptions/CommandExceptionType;)Z"))
|
||||||
|
private static boolean markFirmamentExceptionsAsNotIgnores(boolean original, @Local(argsOnly = true) String command) {
|
||||||
|
if (command.startsWith("firm ") || command.equals("firm") || command.startsWith("firmament ") || command.equals("firmament")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return original;
|
||||||
|
}
|
||||||
|
}
|
||||||
75
src/main/kotlin/commands/Duration.kt
Normal file
75
src/main/kotlin/commands/Duration.kt
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package moe.nea.firmament.commands
|
||||||
|
|
||||||
|
import com.mojang.brigadier.StringReader
|
||||||
|
import com.mojang.brigadier.arguments.ArgumentType
|
||||||
|
import com.mojang.brigadier.context.CommandContext
|
||||||
|
import com.mojang.brigadier.exceptions.DynamicCommandExceptionType
|
||||||
|
import com.mojang.brigadier.suggestion.Suggestions
|
||||||
|
import com.mojang.brigadier.suggestion.SuggestionsBuilder
|
||||||
|
import java.util.concurrent.CompletableFuture
|
||||||
|
import java.util.function.Function
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
import kotlin.time.DurationUnit
|
||||||
|
import kotlin.time.toDuration
|
||||||
|
import moe.nea.firmament.util.tr
|
||||||
|
|
||||||
|
object DurationArgumentType : ArgumentType<Duration> {
|
||||||
|
val unknownTimeCode = DynamicCommandExceptionType { timeCode ->
|
||||||
|
tr("firmament.command-argument.duration.error",
|
||||||
|
"Unknown time code '$timeCode'")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun parse(reader: StringReader): Duration {
|
||||||
|
val start = reader.cursor
|
||||||
|
val string = reader.readUnquotedString()
|
||||||
|
val matcher = regex.matcher(string)
|
||||||
|
var s = 0
|
||||||
|
var time = 0.seconds
|
||||||
|
fun createError(till: Int) {
|
||||||
|
throw unknownTimeCode.createWithContext(
|
||||||
|
reader.also { it.cursor = start + s },
|
||||||
|
string.substring(s, till))
|
||||||
|
}
|
||||||
|
|
||||||
|
while (matcher.find()) {
|
||||||
|
if (matcher.start() != s) {
|
||||||
|
createError(matcher.start())
|
||||||
|
}
|
||||||
|
s = matcher.end()
|
||||||
|
val amount = matcher.group("count").toDouble()
|
||||||
|
val what = timeSuffixes[matcher.group("what").single()]!!
|
||||||
|
time += amount.toDuration(what)
|
||||||
|
}
|
||||||
|
if (string.length != s) {
|
||||||
|
createError(string.length)
|
||||||
|
}
|
||||||
|
return time
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun <S : Any?> listSuggestions(
|
||||||
|
context: CommandContext<S>,
|
||||||
|
builder: SuggestionsBuilder
|
||||||
|
): CompletableFuture<Suggestions> {
|
||||||
|
val remaining = builder.remainingLowerCase.substringBefore(' ')
|
||||||
|
if (remaining.isEmpty()) return super.listSuggestions(context, builder)
|
||||||
|
if (remaining.last().isDigit()) {
|
||||||
|
for (timeSuffix in timeSuffixes.keys) {
|
||||||
|
builder.suggest(remaining + timeSuffix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder.buildFuture()
|
||||||
|
}
|
||||||
|
|
||||||
|
val timeSuffixes = mapOf(
|
||||||
|
'm' to DurationUnit.MINUTES,
|
||||||
|
's' to DurationUnit.SECONDS,
|
||||||
|
'h' to DurationUnit.HOURS,
|
||||||
|
)
|
||||||
|
val regex = "(?<count>[0-9]+)(?<what>[${timeSuffixes.keys.joinToString("")}])".toPattern()
|
||||||
|
|
||||||
|
override fun getExamples(): Collection<String> {
|
||||||
|
return listOf("3m", "20s", "1h45m")
|
||||||
|
}
|
||||||
|
}
|
||||||
124
src/main/kotlin/features/misc/TimerFeature.kt
Normal file
124
src/main/kotlin/features/misc/TimerFeature.kt
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
package moe.nea.firmament.features.misc
|
||||||
|
|
||||||
|
import com.mojang.brigadier.arguments.IntegerArgumentType
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
import moe.nea.firmament.Firmament
|
||||||
|
import moe.nea.firmament.annotations.Subscribe
|
||||||
|
import moe.nea.firmament.commands.DurationArgumentType
|
||||||
|
import moe.nea.firmament.commands.RestArgumentType
|
||||||
|
import moe.nea.firmament.commands.get
|
||||||
|
import moe.nea.firmament.commands.thenArgument
|
||||||
|
import moe.nea.firmament.commands.thenExecute
|
||||||
|
import moe.nea.firmament.events.CommandEvent
|
||||||
|
import moe.nea.firmament.events.TickEvent
|
||||||
|
import moe.nea.firmament.util.CommonSoundEffects
|
||||||
|
import moe.nea.firmament.util.FirmFormatters
|
||||||
|
import moe.nea.firmament.util.MC
|
||||||
|
import moe.nea.firmament.util.MinecraftDispatcher
|
||||||
|
import moe.nea.firmament.util.TimeMark
|
||||||
|
import moe.nea.firmament.util.clickCommand
|
||||||
|
import moe.nea.firmament.util.lime
|
||||||
|
import moe.nea.firmament.util.red
|
||||||
|
import moe.nea.firmament.util.tr
|
||||||
|
import moe.nea.firmament.util.yellow
|
||||||
|
|
||||||
|
object TimerFeature {
|
||||||
|
data class Timer(
|
||||||
|
val start: TimeMark,
|
||||||
|
val duration: Duration,
|
||||||
|
val message: String,
|
||||||
|
val timerId: Int,
|
||||||
|
) {
|
||||||
|
fun timeLeft() = (duration - start.passedTime()).coerceAtLeast(0.seconds)
|
||||||
|
fun isDone() = start.passedTime() >= duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Theoretically for optimal performance this could be a treeset keyed to the end time
|
||||||
|
val timers = mutableListOf<Timer>()
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
fun tick(event: TickEvent) {
|
||||||
|
timers.removeAll {
|
||||||
|
if (it.isDone()) {
|
||||||
|
MC.sendChat(tr("firmament.timer.finished",
|
||||||
|
"The timer you set ${FirmFormatters.formatTimespan(it.duration)} ago just went off: ${it.message}")
|
||||||
|
.yellow())
|
||||||
|
Firmament.coroutineScope.launch {
|
||||||
|
withContext(MinecraftDispatcher) {
|
||||||
|
repeat(5) {
|
||||||
|
CommonSoundEffects.playSuccess()
|
||||||
|
delay(0.2.seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startTimer(duration: Duration, message: String) {
|
||||||
|
val timerId = createTimerId++
|
||||||
|
timers.add(Timer(TimeMark.now(), duration, message, timerId))
|
||||||
|
MC.sendChat(
|
||||||
|
tr("firmament.timer.start",
|
||||||
|
"Timer started for $message in ${FirmFormatters.formatTimespan(duration)}.").lime()
|
||||||
|
.append(" ")
|
||||||
|
.append(
|
||||||
|
tr("firmament.timer.cancelbutton",
|
||||||
|
"Click here to cancel the timer."
|
||||||
|
).clickCommand("/firm timer clear $timerId").red()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearTimer(timerId: Int) {
|
||||||
|
val timer = timers.indexOfFirst { it.timerId == timerId }
|
||||||
|
if (timer < 0) {
|
||||||
|
MC.sendChat(tr("firmament.timer.cancel.fail",
|
||||||
|
"Could not cancel that timer. Maybe it was already cancelled?").red())
|
||||||
|
} else {
|
||||||
|
val timerData = timers[timer]
|
||||||
|
timers.removeAt(timer)
|
||||||
|
MC.sendChat(tr("firmament.timer.cancel.done",
|
||||||
|
"Cancelled timer ${timerData.message}. It would have been done in ${
|
||||||
|
FirmFormatters.formatTimespan(timerData.timeLeft())
|
||||||
|
}.").lime())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var createTimerId = 0
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
fun onCommands(event: CommandEvent.SubCommand) {
|
||||||
|
event.subcommand("cleartimer") {
|
||||||
|
thenArgument("timerId", IntegerArgumentType.integer(0)) { timerId ->
|
||||||
|
thenExecute {
|
||||||
|
clearTimer(this[timerId])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
thenExecute {
|
||||||
|
timers.map { it.timerId }.forEach {
|
||||||
|
clearTimer(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event.subcommand("timer") {
|
||||||
|
thenArgument("time", DurationArgumentType) { duration ->
|
||||||
|
thenExecute {
|
||||||
|
startTimer(this[duration], "no message")
|
||||||
|
}
|
||||||
|
thenArgument("message", RestArgumentType) { message ->
|
||||||
|
thenExecute {
|
||||||
|
startTimer(this[duration], this[message])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -142,8 +142,7 @@ fun MutableText.bold(): MutableText = styled { it.withBold(true) }
|
|||||||
fun MutableText.clickCommand(command: String): MutableText {
|
fun MutableText.clickCommand(command: String): MutableText {
|
||||||
require(command.startsWith("/"))
|
require(command.startsWith("/"))
|
||||||
return this.styled {
|
return this.styled {
|
||||||
it.withClickEvent(ClickEvent(ClickEvent.Action.RUN_COMMAND,
|
it.withClickEvent(ClickEvent(ClickEvent.Action.RUN_COMMAND, command))
|
||||||
"/firm disablereiwarning"))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user