/* * SPDX-FileCopyrightText: 2023 Linnea Gräf * SPDX-FileCopyrightText: 2024 Linnea Gräf * * SPDX-License-Identifier: GPL-3.0-or-later */ package moe.nea.firmament.features.world import com.mojang.brigadier.arguments.IntegerArgumentType import me.shedaniel.math.Color import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource import kotlinx.serialization.Serializable import kotlin.collections.component1 import kotlin.collections.component2 import kotlin.collections.set import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.seconds import net.minecraft.command.argument.BlockPosArgumentType import net.minecraft.server.command.ServerCommandSource import net.minecraft.text.Text import net.minecraft.util.math.BlockPos import net.minecraft.util.math.Vec3d import moe.nea.firmament.Firmament import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.commands.get import moe.nea.firmament.commands.thenArgument import moe.nea.firmament.commands.thenExecute import moe.nea.firmament.commands.thenLiteral import moe.nea.firmament.events.CommandEvent import moe.nea.firmament.events.ProcessChatEvent import moe.nea.firmament.events.TickEvent import moe.nea.firmament.events.WorldReadyEvent import moe.nea.firmament.events.WorldRenderLastEvent import moe.nea.firmament.features.FirmamentFeature import moe.nea.firmament.gui.config.ManagedConfig import moe.nea.firmament.util.ClipboardUtils import moe.nea.firmament.util.MC import moe.nea.firmament.util.TimeMark import moe.nea.firmament.util.render.RenderInWorldContext object Waypoints : FirmamentFeature { override val identifier: String get() = "waypoints" object TConfig : ManagedConfig(identifier) { val tempWaypointDuration by duration("temp-waypoint-duration", 0.seconds, 1.hours) { 30.seconds } val showIndex by toggle("show-index") { true } val skipToNearest by toggle("skip-to-nearest") { false } // TODO: look ahead size } data class TemporaryWaypoint( val pos: BlockPos, val postedAt: TimeMark, ) override val config get() = TConfig val temporaryPlayerWaypointList = mutableMapOf() val temporaryPlayerWaypointMatcher = "(?i)x: (-?[0-9]+),? y: (-?[0-9]+),? z: (-?[0-9]+)".toPattern() val waypoints = mutableListOf() var ordered = false var orderedIndex = 0 @Serializable data class ColeWeightWaypoint( val x: Int, val y: Int, val z: Int, val r: Int = 0, val g: Int = 0, val b: Int = 0, ) @Subscribe fun onRenderOrderedWaypoints(event: WorldRenderLastEvent) { if (waypoints.isEmpty()) return RenderInWorldContext.renderInWorld(event) { if (!ordered) { waypoints.withIndex().forEach { color(0f, 0.3f, 0.7f, 0.5f) block(it.value) color(1f, 1f, 1f, 1f) if (TConfig.showIndex) withFacingThePlayer(it.value.toCenterPos()) { text(Text.literal(it.index.toString())) } } } else { orderedIndex %= waypoints.size val firstColor = Color.ofRGBA(0, 200, 40, 180) color(firstColor) tracer(waypoints[orderedIndex].toCenterPos(), lineWidth = 3f) waypoints.withIndex().toList() .wrappingWindow(orderedIndex, 3) .zip( listOf( firstColor, Color.ofRGBA(180, 200, 40, 150), Color.ofRGBA(180, 80, 20, 140), ) ) .reversed() .forEach { (waypoint, col) -> val (index, pos) = waypoint color(col) block(pos) color(1f, 1f, 1f, 1f) if (TConfig.showIndex) withFacingThePlayer(pos.toCenterPos()) { text(Text.literal(index.toString())) } } } } } @Subscribe fun onTick(event: TickEvent) { if (waypoints.isEmpty() || !ordered) return orderedIndex %= waypoints.size val p = MC.player?.pos ?: return if (TConfig.skipToNearest) { orderedIndex = (waypoints.withIndex().minBy { it.value.getSquaredDistance(p) }.index + 1) % waypoints.size } else { if (waypoints[orderedIndex].isWithinDistance(p, 3.0)) { orderedIndex = (orderedIndex + 1) % waypoints.size } } } @Subscribe fun onProcessChat(it: ProcessChatEvent) { val matcher = temporaryPlayerWaypointMatcher.matcher(it.unformattedString) if (it.nameHeuristic != null && TConfig.tempWaypointDuration > 0.seconds && matcher.find()) { temporaryPlayerWaypointList[it.nameHeuristic] = TemporaryWaypoint( BlockPos( matcher.group(1).toInt(), matcher.group(2).toInt(), matcher.group(3).toInt(), ), TimeMark.now() ) } } @Subscribe fun onCommand(event: CommandEvent.SubCommand) { event.subcommand("waypoint") { thenArgument("pos", BlockPosArgumentType.blockPos()) { pos -> thenExecute { val position = pos.get(this).toAbsoluteBlockPos(source.asFakeServer()) waypoints.add(position) source.sendFeedback( Text.stringifiedTranslatable( "firmament.command.waypoint.added", position.x, position.y, position.z ) ) } } } event.subcommand("waypoints") { thenLiteral("clear") { thenExecute { waypoints.clear() source.sendFeedback(Text.translatable("firmament.command.waypoint.clear")) } } thenLiteral("toggleordered") { thenExecute { ordered = !ordered if (ordered) { val p = MC.player?.pos ?: Vec3d.ZERO orderedIndex = waypoints.withIndex().minByOrNull { it.value.getSquaredDistance(p) }?.index ?: 0 } source.sendFeedback(Text.translatable("firmament.command.waypoint.ordered.toggle.$ordered")) } } thenLiteral("skip") { thenExecute { if (ordered && waypoints.isNotEmpty()) { orderedIndex = (orderedIndex + 1) % waypoints.size source.sendFeedback(Text.translatable("firmament.command.waypoint.skip")) } else { source.sendError(Text.translatable("firmament.command.waypoint.skip.error")) } } } thenLiteral("remove") { thenArgument("index", IntegerArgumentType.integer(0)) { indexArg -> thenExecute { val index = get(indexArg) if (index in waypoints.indices) { waypoints.removeAt(index) source.sendFeedback(Text.stringifiedTranslatable( "firmament.command.waypoint.remove", index)) } else { source.sendError(Text.stringifiedTranslatable("firmament.command.waypoint.remove.error")) } } } } thenLiteral("import") { thenExecute { val contents = ClipboardUtils.getTextContents() val data = try { Firmament.json.decodeFromString>(contents) } catch (ex: Exception) { Firmament.logger.error("Could not load waypoints from clipboard", ex) source.sendError(Text.translatable("firmament.command.waypoint.import.error")) return@thenExecute } waypoints.clear() data.mapTo(waypoints) { BlockPos(it.x, it.y, it.z) } source.sendFeedback( Text.stringifiedTranslatable( "firmament.command.waypoint.import", data.size ) ) } } } } @Subscribe fun onRenderTemporaryWaypoints(event: WorldRenderLastEvent) { temporaryPlayerWaypointList.entries.removeIf { it.value.postedAt.passedTime() > TConfig.tempWaypointDuration } if (temporaryPlayerWaypointList.isEmpty()) return RenderInWorldContext.renderInWorld(event) { color(1f, 1f, 0f, 1f) temporaryPlayerWaypointList.forEach { (player, waypoint) -> block(waypoint.pos) } color(1f, 1f, 1f, 1f) temporaryPlayerWaypointList.forEach { (player, waypoint) -> val skin = MC.networkHandler?.listedPlayerListEntries?.find { it.profile.name == player } ?.skinTextures ?.texture withFacingThePlayer(waypoint.pos.toCenterPos()) { waypoint(waypoint.pos, Text.stringifiedTranslatable("firmament.waypoint.temporary", player)) if (skin != null) { matrixStack.translate(0F, -20F, 0F) // Head front texture( skin, 16, 16, 1 / 8f, 1 / 8f, 2 / 8f, 2 / 8f, ) // Head overlay texture( skin, 16, 16, 5 / 8f, 1 / 8f, 6 / 8f, 2 / 8f, ) } } } } } @Subscribe fun onWorldReady(event: WorldReadyEvent) { temporaryPlayerWaypointList.clear() } } fun List.wrappingWindow(startIndex: Int, windowSize: Int): List { val result = ArrayList(windowSize) if (startIndex + windowSize < size) { result.addAll(subList(startIndex, startIndex + windowSize)) } else { result.addAll(subList(startIndex, size)) result.addAll(subList(0, minOf(windowSize - (size - startIndex), startIndex))) } return result } fun FabricClientCommandSource.asFakeServer(): ServerCommandSource { val source = this return ServerCommandSource( source.player, source.position, source.rotation, null, 0, "FakeServerCommandSource", Text.literal("FakeServerCommandSource"), null, source.player ) }