feat: Add /firm timer command

This commit is contained in:
Linnea Gräf
2024-12-24 01:01:10 +01:00
parent 39d35afb70
commit 24110c24af
4 changed files with 218 additions and 2 deletions

View File

@@ -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;
}
}

View 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")
}
}

View 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])
}
}
}
}
}
}

View File

@@ -142,8 +142,7 @@ fun MutableText.bold(): MutableText = styled { it.withBold(true) }
fun MutableText.clickCommand(command: String): MutableText {
require(command.startsWith("/"))
return this.styled {
it.withClickEvent(ClickEvent(ClickEvent.Action.RUN_COMMAND,
"/firm disablereiwarning"))
it.withClickEvent(ClickEvent(ClickEvent.Action.RUN_COMMAND, command))
}
}