aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin/features/world
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/kotlin/features/world')
-rw-r--r--src/main/kotlin/features/world/FairySouls.kt131
-rw-r--r--src/main/kotlin/features/world/NPCWaypoints.kt40
-rw-r--r--src/main/kotlin/features/world/NavigableWaypoint.kt22
-rw-r--r--src/main/kotlin/features/world/NavigationHelper.kt121
-rw-r--r--src/main/kotlin/features/world/NpcWaypointGui.kt68
-rw-r--r--src/main/kotlin/features/world/Waypoints.kt297
6 files changed, 679 insertions, 0 deletions
diff --git a/src/main/kotlin/features/world/FairySouls.kt b/src/main/kotlin/features/world/FairySouls.kt
new file mode 100644
index 0000000..8a8291a
--- /dev/null
+++ b/src/main/kotlin/features/world/FairySouls.kt
@@ -0,0 +1,131 @@
+
+
+package moe.nea.firmament.features.world
+
+import io.github.moulberry.repo.data.Coordinate
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.serializer
+import net.minecraft.text.Text
+import net.minecraft.util.math.Vec3d
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.ProcessChatEvent
+import moe.nea.firmament.events.SkyblockServerUpdateEvent
+import moe.nea.firmament.events.WorldRenderLastEvent
+import moe.nea.firmament.features.FirmamentFeature
+import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.repo.RepoManager
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.SBData
+import moe.nea.firmament.util.SkyBlockIsland
+import moe.nea.firmament.util.blockPos
+import moe.nea.firmament.util.data.ProfileSpecificDataHolder
+import moe.nea.firmament.util.render.RenderInWorldContext
+import moe.nea.firmament.util.render.RenderInWorldContext.Companion.renderInWorld
+import moe.nea.firmament.util.unformattedString
+
+
+object FairySouls : FirmamentFeature {
+
+
+ @Serializable
+ data class Data(
+ val foundSouls: MutableMap<SkyBlockIsland, MutableSet<Int>> = mutableMapOf()
+ )
+
+ override val config: ManagedConfig
+ get() = TConfig
+
+ object DConfig : ProfileSpecificDataHolder<Data>(serializer(), "found-fairysouls", ::Data)
+
+
+ object TConfig : ManagedConfig("fairy-souls") {
+ val displaySouls by toggle("show") { false }
+ val resetSouls by button("reset") {
+ DConfig.data?.foundSouls?.clear() != null
+ updateMissingSouls()
+ }
+ }
+
+
+ override val identifier: String get() = "fairy-souls"
+
+ val playerReach = 5
+ val playerReachSquared = playerReach * playerReach
+
+ var currentLocationName: SkyBlockIsland? = null
+ var currentLocationSouls: List<Coordinate> = emptyList()
+ var currentMissingSouls: List<Coordinate> = emptyList()
+
+ fun updateMissingSouls() {
+ currentMissingSouls = emptyList()
+ val c = DConfig.data ?: return
+ val fi = c.foundSouls[currentLocationName] ?: setOf()
+ val cms = currentLocationSouls.toMutableList()
+ fi.asSequence().sortedDescending().filter { it in cms.indices }.forEach { cms.removeAt(it) }
+ currentMissingSouls = cms
+ }
+
+ fun updateWorldSouls() {
+ currentLocationSouls = emptyList()
+ val loc = currentLocationName ?: return
+ currentLocationSouls = RepoManager.neuRepo.constants.fairySouls.soulLocations[loc.locrawMode] ?: return
+ }
+
+ fun findNearestClickableSoul(): Coordinate? {
+ val player = MC.player ?: return null
+ val pos = player.pos
+ val location = SBData.skyblockLocation ?: return null
+ val soulLocations: List<Coordinate> =
+ RepoManager.neuRepo.constants.fairySouls.soulLocations[location.locrawMode] ?: return null
+ return soulLocations
+ .map { it to it.blockPos.getSquaredDistance(pos) }
+ .filter { it.second < playerReachSquared }
+ .minByOrNull { it.second }
+ ?.first
+ }
+
+ private fun markNearestSoul() {
+ val nearestSoul = findNearestClickableSoul() ?: return
+ val c = DConfig.data ?: return
+ val loc = currentLocationName ?: return
+ val idx = currentLocationSouls.indexOf(nearestSoul)
+ c.foundSouls.computeIfAbsent(loc) { mutableSetOf() }.add(idx)
+ DConfig.markDirty()
+ updateMissingSouls()
+ }
+
+ @Subscribe
+ fun onWorldRender(it: WorldRenderLastEvent) {
+ if (!TConfig.displaySouls) return
+ renderInWorld(it) {
+ color(1F, 1F, 0F, 0.8F)
+ currentMissingSouls.forEach {
+ block(it.blockPos)
+ }
+ color(1f, 0f, 1f, 1f)
+ currentLocationSouls.forEach {
+ wireframeCube(it.blockPos)
+ }
+ }
+ }
+
+ @Subscribe
+ fun onProcessChat(it: ProcessChatEvent) {
+ when (it.text.unformattedString) {
+ "You have already found that Fairy Soul!" -> {
+ markNearestSoul()
+ }
+
+ "SOUL! You found a Fairy Soul!" -> {
+ markNearestSoul()
+ }
+ }
+ }
+
+ @Subscribe
+ fun onLocationChange(it: SkyblockServerUpdateEvent) {
+ currentLocationName = it.newLocraw?.skyblockLocation
+ updateWorldSouls()
+ updateMissingSouls()
+ }
+}
diff --git a/src/main/kotlin/features/world/NPCWaypoints.kt b/src/main/kotlin/features/world/NPCWaypoints.kt
new file mode 100644
index 0000000..592b8fa
--- /dev/null
+++ b/src/main/kotlin/features/world/NPCWaypoints.kt
@@ -0,0 +1,40 @@
+package moe.nea.firmament.features.world
+
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.commands.thenExecute
+import moe.nea.firmament.events.CommandEvent
+import moe.nea.firmament.events.ReloadRegistrationEvent
+import moe.nea.firmament.util.MoulConfigUtils
+import moe.nea.firmament.util.ScreenUtil
+
+object NPCWaypoints {
+
+ var allNpcWaypoints = listOf<NavigableWaypoint>()
+
+ @Subscribe
+ fun onRepoReloadRegistration(event: ReloadRegistrationEvent) {
+ event.repo.registerReloadListener {
+ allNpcWaypoints = it.items.items.values
+ .asSequence()
+ .filter { !it.island.isNullOrBlank() }
+ .map {
+ NavigableWaypoint.NPCWaypoint(it)
+ }
+ .toList()
+ }
+ }
+
+ @Subscribe
+ fun onOpenGui(event: CommandEvent.SubCommand) {
+ event.subcommand("npcs") {
+ thenExecute {
+ ScreenUtil.setScreenLater(MoulConfigUtils.loadScreen(
+ "npc_waypoints",
+ NpcWaypointGui(allNpcWaypoints),
+ null))
+ }
+ }
+ }
+
+
+}
diff --git a/src/main/kotlin/features/world/NavigableWaypoint.kt b/src/main/kotlin/features/world/NavigableWaypoint.kt
new file mode 100644
index 0000000..28a517f
--- /dev/null
+++ b/src/main/kotlin/features/world/NavigableWaypoint.kt
@@ -0,0 +1,22 @@
+package moe.nea.firmament.features.world
+
+import io.github.moulberry.repo.data.NEUItem
+import net.minecraft.util.math.BlockPos
+import moe.nea.firmament.util.SkyBlockIsland
+
+abstract class NavigableWaypoint {
+ abstract val name: String
+ abstract val position: BlockPos
+ abstract val island: SkyBlockIsland
+
+ data class NPCWaypoint(
+ val item: NEUItem,
+ ) : NavigableWaypoint() {
+ override val name: String
+ get() = item.displayName
+ override val position: BlockPos
+ get() = BlockPos(item.x, item.y, item.z)
+ override val island: SkyBlockIsland
+ get() = SkyBlockIsland.forMode(item.island)
+ }
+}
diff --git a/src/main/kotlin/features/world/NavigationHelper.kt b/src/main/kotlin/features/world/NavigationHelper.kt
new file mode 100644
index 0000000..acdfb86
--- /dev/null
+++ b/src/main/kotlin/features/world/NavigationHelper.kt
@@ -0,0 +1,121 @@
+package moe.nea.firmament.features.world
+
+import io.github.moulberry.repo.constants.Islands
+import net.minecraft.text.Text
+import net.minecraft.util.math.BlockPos
+import net.minecraft.util.math.Position
+import net.minecraft.util.math.Vec3i
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.SkyblockServerUpdateEvent
+import moe.nea.firmament.events.TickEvent
+import moe.nea.firmament.events.WorldRenderLastEvent
+import moe.nea.firmament.repo.RepoManager
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.SBData
+import moe.nea.firmament.util.SkyBlockIsland
+import moe.nea.firmament.util.WarpUtil
+import moe.nea.firmament.util.render.RenderInWorldContext
+
+object NavigationHelper {
+ var targetWaypoint: NavigableWaypoint? = null
+ set(value) {
+ field = value
+ recalculateRoute()
+ }
+
+ var nextTeleporter: Islands.Teleporter? = null
+ private set
+
+ val Islands.Teleporter.toIsland get() = SkyBlockIsland.forMode(this.getTo())
+ val Islands.Teleporter.fromIsland get() = SkyBlockIsland.forMode(this.getFrom())
+ val Islands.Teleporter.blockPos get() = BlockPos(x.toInt(), y.toInt(), z.toInt())
+
+ @Subscribe
+ fun onWorldSwitch(event: SkyblockServerUpdateEvent) {
+ recalculateRoute()
+ }
+
+ fun recalculateRoute() {
+ val tp = targetWaypoint
+ val currentIsland = SBData.skyblockLocation
+ if (tp == null || currentIsland == null) {
+ nextTeleporter = null
+ return
+ }
+ val route = findRoute(currentIsland, tp.island, mutableSetOf())
+ nextTeleporter = route?.get(0)
+ }
+
+ private fun findRoute(
+ fromIsland: SkyBlockIsland,
+ targetIsland: SkyBlockIsland,
+ visitedIslands: MutableSet<SkyBlockIsland>
+ ): MutableList<Islands.Teleporter>? {
+ var shortestChain: MutableList<Islands.Teleporter>? = null
+ for (it in RepoManager.neuRepo.constants.islands.teleporters) {
+ if (it.toIsland in visitedIslands) continue
+ if (it.fromIsland != fromIsland) continue
+ if (it.toIsland == targetIsland) return mutableListOf(it)
+ visitedIslands.add(fromIsland)
+ val nextRoute = findRoute(it.toIsland, targetIsland, visitedIslands) ?: continue
+ nextRoute.add(0, it)
+ if (shortestChain == null || shortestChain.size > nextRoute.size) {
+ shortestChain = nextRoute
+ }
+ visitedIslands.remove(fromIsland)
+ }
+ return shortestChain
+ }
+
+
+ @Subscribe
+ fun onMovement(event: TickEvent) { // TODO: add a movement tick event maybe?
+ val tp = targetWaypoint ?: return
+ val p = MC.player ?: return
+ if (p.squaredDistanceTo(tp.position.toCenterPos()) < 5 * 5) {
+ targetWaypoint = null
+ }
+ }
+
+ @Subscribe
+ fun drawWaypoint(event: WorldRenderLastEvent) {
+ val tp = targetWaypoint ?: return
+ val nt = nextTeleporter
+ RenderInWorldContext.renderInWorld(event) {
+ if (nt != null) {
+ waypoint(nt.blockPos,
+ Text.literal("Teleporter to " + nt.toIsland.userFriendlyName),
+ Text.literal("(towards " + tp.name + "§f)"))
+ } else if (tp.island == SBData.skyblockLocation) {
+ waypoint(tp.position,
+ Text.literal(tp.name))
+ }
+ }
+ }
+
+ fun tryWarpNear() {
+ val tp = targetWaypoint
+ if (tp == null) {
+ MC.sendChat(Text.literal("Could not find a waypoint to warp you to. Select one first."))
+ return
+ }
+ WarpUtil.teleportToNearestWarp(tp.island, tp.position.asPositionView())
+ }
+
+}
+
+fun Vec3i.asPositionView(): Position {
+ return object : Position {
+ override fun getX(): Double {
+ return this@asPositionView.x.toDouble()
+ }
+
+ override fun getY(): Double {
+ return this@asPositionView.y.toDouble()
+ }
+
+ override fun getZ(): Double {
+ return this@asPositionView.z.toDouble()
+ }
+ }
+}
diff --git a/src/main/kotlin/features/world/NpcWaypointGui.kt b/src/main/kotlin/features/world/NpcWaypointGui.kt
new file mode 100644
index 0000000..6146e50
--- /dev/null
+++ b/src/main/kotlin/features/world/NpcWaypointGui.kt
@@ -0,0 +1,68 @@
+package moe.nea.firmament.features.world
+
+import io.github.notenoughupdates.moulconfig.observer.ObservableList
+import io.github.notenoughupdates.moulconfig.xml.Bind
+import moe.nea.firmament.features.events.anniversity.AnniversaryFeatures.atOnce
+import moe.nea.firmament.keybindings.SavedKeyBinding
+
+class NpcWaypointGui(
+ val allWaypoints: List<NavigableWaypoint>,
+) {
+
+ data class NavigableWaypointW(val waypoint: NavigableWaypoint) {
+ @Bind
+ fun name() = waypoint.name
+
+ @Bind
+ fun isSelected() = NavigationHelper.targetWaypoint == waypoint
+
+ @Bind
+ fun click() {
+ if (SavedKeyBinding.isShiftDown()) {
+ NavigationHelper.targetWaypoint = waypoint
+ NavigationHelper.tryWarpNear()
+ } else if (isSelected()) {
+ NavigationHelper.targetWaypoint = null
+ } else {
+ NavigationHelper.targetWaypoint = waypoint
+ }
+ }
+ }
+
+ @JvmField
+ @field:Bind
+ var search: String = ""
+ var lastSearch: String? = null
+
+ @Bind("results")
+ fun results(): ObservableList<NavigableWaypointW> {
+ return results
+ }
+
+ @Bind
+ fun tick() {
+ if (search != lastSearch) {
+ updateSearch()
+ lastSearch = search
+ }
+ }
+
+ val results: ObservableList<NavigableWaypointW> = ObservableList(mutableListOf())
+
+ fun updateSearch() {
+ val split = search.split(" +".toRegex())
+ results.atOnce {
+ results.clear()
+ allWaypoints.filter { waypoint ->
+ if (search.isBlank()) {
+ true
+ } else {
+ split.all { waypoint.name.contains(it, ignoreCase = true) }
+ }
+ }.mapTo(results) {
+ NavigableWaypointW(it)
+ }
+ }
+ }
+
+}
diff --git a/src/main/kotlin/features/world/Waypoints.kt b/src/main/kotlin/features/world/Waypoints.kt
new file mode 100644
index 0000000..91a06da
--- /dev/null
+++ b/src/main/kotlin/features/world/Waypoints.kt
@@ -0,0 +1,297 @@
+
+
+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<String, TemporaryWaypoint>()
+ val temporaryPlayerWaypointMatcher = "(?i)x: (-?[0-9]+),? y: (-?[0-9]+),? z: (-?[0-9]+)".toPattern()
+
+ val waypoints = mutableListOf<BlockPos>()
+ 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<List<ColeWeightWaypoint>>(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 <E> List<E>.wrappingWindow(startIndex: Int, windowSize: Int): List<E> {
+ val result = ArrayList<E>(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
+ )
+}