From 88c08c3cb98963e2e2c0180bd40f669a4458b7ff Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Sat, 27 Apr 2024 11:52:25 -0400 Subject: [PATCH 01/46] fix: properly update changed state of observable set --- src/main/kotlin/gg/skytils/skytilsmod/utils/ObservableSet.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/kotlin/gg/skytils/skytilsmod/utils/ObservableSet.kt b/src/main/kotlin/gg/skytils/skytilsmod/utils/ObservableSet.kt index e51bd61ff..6dd603833 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/utils/ObservableSet.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/utils/ObservableSet.kt @@ -36,6 +36,7 @@ class ObservableSet(val backingSet: MutableSet) : MutableSet by backing override fun addAll(elements: Collection): Boolean { return backingSet.addAll(elements).also { if (it) { + setChanged() elements.forEach { notifyObservers(ObservableAddEvent(it)) } @@ -46,6 +47,7 @@ class ObservableSet(val backingSet: MutableSet) : MutableSet by backing override fun remove(element: E): Boolean { return backingSet.remove(element).also { if (it) { + setChanged() notifyObservers(ObservableRemoveEvent(element)) } } @@ -54,6 +56,7 @@ class ObservableSet(val backingSet: MutableSet) : MutableSet by backing override fun removeAll(elements: Collection): Boolean { return backingSet.removeAll(elements).also { if (it) { + setChanged() elements.forEach { notifyObservers(ObservableRemoveEvent(it)) } @@ -64,6 +67,7 @@ class ObservableSet(val backingSet: MutableSet) : MutableSet by backing override fun retainAll(elements: Collection): Boolean { return backingSet.retainAll(elements).also { if (it) { + setChanged() backingSet.forEach { notifyObservers(ObservableRemoveEvent(it)) } From 3d21b9e2ad10ffd216b86015613b8a4ad8226b41 Mon Sep 17 00:00:00 2001 From: hannibal2 <24389977+hannibal002@users.noreply.github.com> Date: Mon, 29 Apr 2024 15:05:46 +0200 Subject: [PATCH 02/46] fix: check if corleone entity is visible to the player (#483) Co-authored-by: hannibal2 <24389977+hannibal00212@users.noreply.github.com> --- .../skytilsmod/features/impl/mining/MiningFeatures.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/mining/MiningFeatures.kt b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/mining/MiningFeatures.kt index 86953c361..35bca0c4c 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/mining/MiningFeatures.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/mining/MiningFeatures.kt @@ -39,7 +39,6 @@ import gg.skytils.skytilsmod.events.impl.PacketEvent import gg.skytils.skytilsmod.features.impl.handlers.MayorInfo import gg.skytils.skytilsmod.utils.* import gg.skytils.skytilsmod.utils.RenderUtil.highlight -import gg.skytils.skytilsmod.utils.graphics.SmartFontRenderer import gg.skytils.skytilsmod.utils.graphics.colors.ColorFactory import net.minecraft.client.entity.EntityOtherPlayerMP import net.minecraft.client.renderer.GlStateManager @@ -356,10 +355,11 @@ object MiningFeatures { @SubscribeEvent fun onRenderLivingPre(event: RenderLivingEvent.Pre) { if (!Utils.inSkyblock) return - if (Skytils.config.crystalHollowWaypoints && event.entity is EntityOtherPlayerMP && event.entity.name == "Team Treasurite" && event.entity.baseMaxHealth == if (MayorInfo.mayorPerks.contains( - "DOUBLE MOBS HP!!!" - ) - ) 2_000_000.0 else 1_000_000.0 + if (Skytils.config.crystalHollowWaypoints && + event.entity is EntityOtherPlayerMP && + event.entity.name == "Team Treasurite" && + mc.thePlayer.canEntityBeSeen(event.entity) && + event.entity.baseMaxHealth == if (MayorInfo.mayorPerks.contains("DOUBLE MOBS HP!!!")) 2_000_000.0 else 1_000_000.0 ) { waypoints["Corleone"] = event.entity.position } From 41c3f02ce5054af58206a394342c98c1805cb145 Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Mon, 13 May 2024 18:15:57 -0400 Subject: [PATCH 03/46] feat: introduce burrow estimation based on harp sound --- .../features/impl/events/GriffinBurrows.kt | 144 ++++++++++++++++-- .../gg/skytils/skytilsmod/utils/Utils.kt | 3 + .../resources/assets/skytils/grassdata.txt | 1 + 3 files changed, 134 insertions(+), 14 deletions(-) create mode 100644 src/main/resources/assets/skytils/grassdata.txt diff --git a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt index 4aac765ca..4488710e7 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt @@ -19,20 +19,27 @@ package gg.skytils.skytilsmod.features.impl.events import com.google.common.collect.EvictingQueue import gg.essential.universal.UMatrixStack +import gg.essential.universal.UMinecraft +import gg.essential.universal.wrappers.UPlayer import gg.skytils.skytilsmod.Skytils import gg.skytils.skytilsmod.Skytils.Companion.mc import gg.skytils.skytilsmod.events.impl.MainReceivePacketEvent import gg.skytils.skytilsmod.events.impl.PacketEvent import gg.skytils.skytilsmod.utils.* import net.minecraft.client.renderer.GlStateManager +import net.minecraft.entity.item.EntityArmorStand import net.minecraft.init.Blocks +import net.minecraft.init.Items import net.minecraft.item.ItemStack import net.minecraft.network.play.client.C07PacketPlayerDigging import net.minecraft.network.play.client.C08PacketPlayerBlockPlacement +import net.minecraft.network.play.server.S04PacketEntityEquipment +import net.minecraft.network.play.server.S29PacketSoundEffect import net.minecraft.network.play.server.S2APacketParticles import net.minecraft.util.AxisAlignedBB import net.minecraft.util.BlockPos import net.minecraft.util.EnumParticleTypes +import net.minecraft.util.Vec3 import net.minecraft.util.Vec3i import net.minecraftforge.client.event.ClientChatReceivedEvent import net.minecraftforge.client.event.RenderWorldLastEvent @@ -42,6 +49,11 @@ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent import net.minecraftforge.fml.common.gameevent.TickEvent import net.minecraftforge.fml.common.gameevent.TickEvent.ClientTickEvent import java.awt.Color +import java.time.Duration +import java.time.Instant +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.sin object GriffinBurrows { val particleBurrows = hashMapOf() @@ -50,6 +62,19 @@ object GriffinBurrows { var hasSpadeInHotbar = false + object BurrowEstimation { + val arrows = mutableMapOf() + val guesses = mutableMapOf() + fun getDistanceFromPitch(pitch: Double) = + 2805 * pitch + 1375 + + val grassData by lazy { + this::class.java.getResource("/assets/skytils/grassdata.txt")!!.readBytes() + } + + class Arrow(val directionVector: Vec3, val pos: Vec3) + } + @SubscribeEvent fun onTick(event: ClientTickEvent) { @@ -57,6 +82,12 @@ object GriffinBurrows { hasSpadeInHotbar = mc.thePlayer != null && Utils.inSkyblock && (0..7).any { mc.thePlayer.inventory.getStackInSlot(it).isSpade } + BurrowEstimation.guesses.entries.removeIf { (_, instant) -> + Duration.between(instant, Instant.now()).toMinutes() > 30 + } + BurrowEstimation.arrows.entries.removeIf { (_, instant) -> + Duration.between(instant, Instant.now()).toMinutes() > 5 + } } @SubscribeEvent(receiveCanceled = true, priority = EventPriority.HIGHEST) @@ -106,6 +137,23 @@ object GriffinBurrows { pb.drawWaypoint(event.partialTicks, matrixStack) } } + for (bg in BurrowEstimation.guesses.keys) { + bg.drawWaypoint(event.partialTicks, matrixStack) + } + for (arrow in BurrowEstimation.arrows.keys) { + RenderUtil.drawCircle( + matrixStack, + arrow.pos.x, + arrow.pos.y + 0.2, + arrow.pos.z, + event.partialTicks, + 5.0, + 100, + 255, + 128, + 0, + ) + } } } @@ -118,23 +166,81 @@ object GriffinBurrows { @SubscribeEvent fun onReceivePacket(event: MainReceivePacketEvent<*, *>) { if (!Utils.inSkyblock) return - if (Skytils.config.showGriffinBurrows && hasSpadeInHotbar && event.packet is S2APacketParticles) { - if (SBInfo.mode != SkyblockIsland.Hub.mode) return - event.packet.apply { - val type = ParticleType.getParticleType(this) ?: return - val pos = BlockPos(x, y, z).down() - if (recentlyDugParticleBurrows.contains(pos)) return - val burrow = particleBurrows.getOrPut(pos) { - ParticleBurrow(pos, hasFootstep = false, hasEnchant = false, type = -1) + when (event.packet) { + is S2APacketParticles -> { + if (Skytils.config.showGriffinBurrows && hasSpadeInHotbar) { + if (SBInfo.mode != SkyblockIsland.Hub.mode) return + event.packet.apply { + val type = ParticleType.getParticleType(this) ?: return + val pos = BlockPos(x, y, z).down() + if (recentlyDugParticleBurrows.contains(pos)) return + BurrowEstimation.guesses.keys.associateWith { guess -> + pos.distanceSq( + guess.x.toDouble(), + guess.y.toDouble(), + guess.z.toDouble() + ) + }.minByOrNull { it.value }?.let { (guess, distance) -> + printDevMessage("Nearest guess is $distance blocks away", "griffin", "griffinguess") + if (distance <= 625) { + BurrowEstimation.guesses.remove(guess) + } + } + val burrow = particleBurrows.getOrPut(pos) { + ParticleBurrow(pos, hasFootstep = false, hasEnchant = false, type = -1) + } + when (type) { + ParticleType.FOOTSTEP -> burrow.hasFootstep = true + ParticleType.ENCHANT -> burrow.hasEnchant = true + ParticleType.EMPTY -> burrow.type = 0 + ParticleType.MOB -> burrow.type = 1 + ParticleType.TREASURE -> burrow.type = 2 + } + } } - when (type) { - ParticleType.FOOTSTEP -> burrow.hasFootstep = true - ParticleType.ENCHANT -> burrow.hasEnchant = true - ParticleType.EMPTY -> burrow.type = 0 - ParticleType.MOB -> burrow.type = 1 - ParticleType.TREASURE -> burrow.type = 2 + } + is S04PacketEntityEquipment -> { + val entity = UMinecraft.getMinecraft().theWorld.getEntityByID(event.packet.entityID) + (entity as? EntityArmorStand)?.let { armorStand -> + if (event.packet.itemStack.item != Items.arrow) return + if (armorStand.getDistanceSq(UPlayer.getPlayer()?.position) >= 27) return + printDevMessage("Found armor stand with arrow", "griffin", "griffinguess") + val yaw = Math.toRadians(armorStand.rotationYaw.toDouble()) + val lookVec = Vec3( + -sin(yaw), + 0.0, + cos(yaw) + ) + val offset = Vec3(-sin(yaw + PI/2), 0.0, cos(yaw + PI/2)) * 0.9 + val origin = armorStand.positionVector.add(offset) + BurrowEstimation.arrows.put(BurrowEstimation.Arrow(lookVec, origin), Instant.now()) } } + is S29PacketSoundEffect -> { + if (event.packet.soundName != "note.harp") return + val (arrow, distance) = BurrowEstimation.arrows.keys + .associateWith { arrow -> + arrow.pos.squareDistanceTo(event.packet.x, event.packet.y, event.packet.z) + }.minByOrNull { it.value } ?: return + printDevMessage("Nearest arrow is $distance blocks away ${arrow.pos}", "griffin", "griffinguess") + if (distance > 25) return + val guessPos = arrow.pos.add( + arrow.directionVector * BurrowEstimation.getDistanceFromPitch(event.packet.pitch.toDouble()) + ) + + var y: Int + var x = guessPos.x.toInt() + var z = guessPos.z.toInt() + // offset of 300 blocks for both x and z + // x ranges from 195 to -281 + // z ranges from 207 to -233 + do { + y = BurrowEstimation.grassData.get((x++ % 507) * 507 + (z++ % 495)).toInt() + } while (y < 2) + val guess = BurrowGuess(guessPos.x.toInt(), y, guessPos.z.toInt()) + BurrowEstimation.arrows.remove(arrow) + BurrowEstimation.guesses[guess] = Instant.now() + } } } @@ -181,6 +287,16 @@ object GriffinBurrows { } } + data class BurrowGuess( + override val x: Int, + override val y: Int, + override val z: Int + ) : Diggable() { + override var type = 0 + override val waypointText = "§aBurrow §6(Guess)" + override val color = Color.ORANGE + } + data class ParticleBurrow( override val x: Int, override val y: Int, diff --git a/src/main/kotlin/gg/skytils/skytilsmod/utils/Utils.kt b/src/main/kotlin/gg/skytils/skytilsmod/utils/Utils.kt index 502885527..f5f2f458c 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/utils/Utils.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/utils/Utils.kt @@ -414,6 +414,9 @@ operator fun Vec3.minus(other: Vec3): Vec3 = subtract(other) operator fun Vec3.times(scaleValue: Double): Vec3 = Vec3(xCoord * scaleValue, yCoord * scaleValue, zCoord * scaleValue) +fun Vec3.squareDistanceTo(x: Double, y: Double, z: Double)= + (x - xCoord) * (x - xCoord) + (y - yCoord) * (y - yCoord) + (z - zCoord) * (z - zCoord) + /** * @author Ilya * @link https://stackoverflow.com/a/56043547 diff --git a/src/main/resources/assets/skytils/grassdata.txt b/src/main/resources/assets/skytils/grassdata.txt new file mode 100644 index 000000000..0bae94a8d --- /dev/null +++ b/src/main/resources/assets/skytils/grassdata.txt @@ -0,0 +1 @@ +YYXYYYYYYYYYYYYYYXYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYi[YiiiiiiiiiiiiiiiiiiTTUiiiUUV\\ZUVVh\\ZZZZVWWg\hhZZZZZZZZZWWWghZZZZZZZZXXXXW7fg\hZZZZZZZZXXXXXW88888NZZZZZZZYYYXXXWW998888886MMNNNNNNM\\hZZZZZZZYYYYXXWV99999988888776MMNNNNNNNNNN\ZYYYYXXWV:::9999888876MMMNNNNNNNNN\]ZYYYYXXV::::9999988876MMMMMMMNNNNNNNN\\][[[[ZZYYYXXWVU;:::99998876MMMMMNNNNN\\]][[[[ZZYYYWWVU;:::9998876MMMMNNNNNN][[[[[[ZZZYYYXWVU;;;;:::99887MMNNNNNNNN\\hh]\\[[ZZZZZYYYXW;;;;:::9987NNNNNNNNNNN\\\\[[ZZZZZZYX<<;;:::9987NNNNNNNNOOOOhh\\\\[[ZZZZZ<<;::9988NNNNNOOOOOOOOdhh]\\\\[[ZZZZZ<<;;::98MNNNNOOOOOOPPPPPdhh]]hh\\\\\\\[[[ZZZZ<<<;998NNNNOOOOPPPPPPPPPddd]]]h\\\\\\[[[[ZZZZ=<<;::99NNNOOOOPPPPPPTUdd]]\\\\\\\\\[[[ZZZ=<<;::99NOOOOPPPPVWi]]]h\\\\\\\\\\[[[[[ZZZ>=;::9OOOOPPPPTXXXYiihhh]hh\\\\\\\[[[[[ZZ@?=<;;:9NOOOOPPPPXXYYYiiih]]\\\\\[[[[[[ZZZ@?><<;::LLOOORSSPPPYYYZZih]]\\[[[[[ZZZ@?>=::9MOORSSSSQYZZZ[[\\\[[[[[[ZZZBAA??=<::9MMMNRSSTTTTSRZZ[[[ch]\\\[[[[[[[ZZZYYXB@@?><;::99MMMMRSSTTTUUUTWXYYZZ[[[ccc]]]\\\[[[[[[ZYYYXBAA@?<;;:99MMMMPSSTTUUUVVWXYYZZ[[ccc]]\\\[[[[ZZYYXWCBBAA@?>=<<;:9HMMMRSTTUUUVVWXXYYZ[cccc]]]]\\\\[[[[[ZZYXWDDCCA@?><;::IIMMSTUUUVVVWYYZ[cccc\\[[[[ZZYYDDDCCBBA@@>=<;;:9HIIIIJJJMUUVVVWWXYYZZ[[[ccccccc\\\\[[[[[[ZZYDDDDCCBAA??><;;:9HIIJJJJJJJJKMMMUVVVWXXYZZ[[[cccc\\\\[[[[[ZZYXDDDBA@?><::HIIJJJJKKKKKLMMVVVWWXYYZZZ[[[\\\\[[[[[ZZYBAA@?><<;;HIJJJJKKKKLLLLLMMMVVWWXYZZZ[[[[[[\\\\\[[[[[ZZYYBA@??=;:9HIIJJJKKKKKKLLLLLLMMMMVWWWXYYZZ[[[[[[[[\\\[[[[[[ZZBA??<:9HIIJJKKKKLLLLLMMMMMVVWWWXYZZZ[[[[[[[[[[[[[[[[[[[[[ZBA@?:HIIIJKKKKKLLLLMMMMMMMVVWWXXXYYZZ[[[[[[[[[[[[[[[[[[[[[[A@?;;::HHIIIIJKKKKKKLLLMMMMMMMMMSVVWWXXXYYZ[[[[[[[[[[[[[[[[[[[[[[Z@?=;9HIIIJJKKKKKLLMMMMMMMMMMQRSTVWWXXXXYZ[[[[[[[[[[[[[[[[[[[[[ZA;::EGGIIIJJJKKKLLMMMMMMMMMNNOOPQQTVWXXXXYZZZ[[[[[[[[[[[[[[[[[[[[[ZZBBBBAA;::CDDEFFGGGGIIIJJJKKKKKKLLLMMMMMMMMMNOOPPRRUWWXXXYYYZ[[[ZZZZZ[[[ZZ[ZZZ[[[[[[[[ZZZCCBBBAA;;:DEEFFGGGGHHHHHIIJJJJKKKKKKLLLMMMMMMMMNNOOPQTUWWXXXXYYZ[[ZZZZZZ[[ZZZZZZiii[[[[[[[[ZZZCCCCBBA;;:DEEFFGGGHHHHHHHHIIIIJJJJKKKKKLLLMMMMMMMMNNOOQUVWWXXXYYZZZ[[ZZZZZZZZZZZZi[[[[[[[[[[[ZZZZZYCCCBB;DDCCCCDEFFGGGHHHHHHHHHIIIIJJJJKKKKKLLLMMMMMMMMMMNOOPPSTTWXXXYYYZZ[[ZZZZZZZZZZZZZ[[[[[[[[ZZZZZZZYCBB;;DDDDDDDCCCDDEFGGGHHHHHHHHHHIIIJJJJKKKKKLLMMMMMMMNNOOPSVWXXXYYZZZ[[[ZZZZZZ[[[[[[[ZZZZZZZYYCCBB<;7CCDDEEEEDDDDDCCDEEFGGHHHHHHHHHHHIIIJJJJJKKKKKLMMMMMMMMMMNNOOPQRSWXXXYYYZZZ[[ZZiZZZ[[[[[[[[[ZZZZZZZZYCBB<;:DDEEFFFFEEEEEEEDDDDCDDEFFGHHHHHHHHHHHIIIIIJJJKKKKLMMMMMMMMMNOOOPQSWXXXYYYZZZZZZZZiiZ[[[[[[[[[[[[[ZZZZZZZYYBB<:7EEFFFFFFFFFEEEEEEEDDDDDDDDEEEFGGHHHHHHHHHHIIIIIJJJJKKKKKLMMMMMMMMNOOPPQRXXXYYYYZZZZZZiZZZZ[[[[[[[[[[[[[ZZZZZZZYXBDEEFFFFGGGGFFFFFFFFEEEEEEEEEEEEEEEFFGHHHHHHHHHHIIIJJJJKKKKLMMMMMMMNOOPQQRSXXXXYYYZZZZZZZZZiiiZZZZZZZ[[[[[[[[ZZZZZZZZYYBB;FFFFGGGGGGGGGGFFFFFFFFFEEEEEEEEEEEFFFFFGGHHHHHHHHHIIIIJJJJKKKKKMMMMMMNOPPQRXXXYYYYZZZZZZZiZZZZZZZZZZZZZ[[[[ZZZZZZZYXB>=<:GGGGGGGGGGGHHHHHGGGFFFFFFFFFFEEEEFFFFFFFGGGGHHHHHHHHHHHHIIIIIIIJJJJKKKKLLMMMMMMMOPPQQRSSXXXYYYYZZZZZZZZZZZZZZZiZZZZZZZZZZZZZZZZYYXBA><:FGGGGGGGGHHHHIIIIIHHHHHHHGGGGGFFFFFFFFGGGGGGGGGGGHHHHHHHHHHHIIIIIIIIJJJJJKKKKKKLMMMOPPQQQRSSXXXXYYYYZZZZZZZZZZZZZZZZZZiiYYYZZZZZZZZZZZZZZZZYXXA@>;FGGGHHHHHHHHHEHHIIIIIIIIIHHHHHHHGGGGGGGGGGGGGGGGGGGHHHHHHHHHHHHHHHHHIIIIIIIIIJJJJKKKKLLMMMQQQQQRSSXXXXXYYYYZZZZiZZZZZZZZZZiiYYYYYYYYZZZZZZZZZZZYYXXB??==;9FFGGGHHHHHHHHHHHHFHHIIIIIIIIIIIIHHHHHHHHHGGGGGGGGGGGHHHHHHHHHHHHHHHHHHHIIIIIIIIIIJJJJJKKKKKLMMQRRSWXXXXYYYYYZZZZZZZZZZZZZiiYYYYYYYYYYYYYZZZZZZZZZZYYYXA><:9FFGGHHHHHHIIIIIIHHHHHIIIIIIIIIIIIIIIHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHIIIIIIIIIIIJJJJJJKKKLMMMRRRSUWXXXXYYYYYZZZZZZZZZZiYYYYYYYYYYYYYYYYYZZZZZZZYYYYXXBBA?==<:FFGGHHHHHIIIIIIIIIIIHHIIIIIIIIIIIIIIIIHHHHHHHHHHHHHHHHHHHHHHHHHHHHHIIIIIIIIIIIIIIJJJJJJKKKKLMMQRRRRSUWXXXXXYYYYYZZZZZZZYYYYYYYYYYYYYYYYYYZZYYYYXXWBBBA@>=<FFGGHHHHIIIIIIIIIIIIIIIHHHIIIIIIIIIIIIIIIIIIIIIIHHHHHHHHHHHHHHHHHHHHHHHHIIIIIIIIIIIIIIIIIJJJJJKKKLQQRRRRSSTUWWXXXXYYYYYZZZiYYYYYYYYYXXXYYYYYYYYYYYYYXXWUCBBBBAA@=<FFGGHHHIIIIIIIIIIIIIIIIIIHHIIIIIIIIIIIIIIIIIIIIIIIIIIHHHHHHHHHHHHHHHIIIIIIIIIIIIIIIIIIIIIJJJJJKKKKKLPQQQRRRRSSTUWWXXXXXYYYZZZZZiiYiYYYYYXXXXXXXXXXYYYYYYYYYYXXWVBBBBBA@@<8FGGHHIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIHHHHHHHIIIIIIIIIIIIIIIIIIIIIIIIJJJJJJKKKKLMPPPQQRRRRSSTVWWXXXXYYYZZZZZYiYYYXXXXXXXXXXXXXYYYYYXXXXWDCCCCCBBBBBAA@?<<;9FFGHHIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIJJJJJKKKLLLMMOPPPPQQRRRSSTVWWXXXXXYZZZZYYiYXXXXXXXXXXXXXXXXWWWCCCCBBBBAA@@?;;9EFGGHIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIJJJJJJKKLLLMMOOOPPQRRSSUWWXXXXXYZZYYYYYYXXXXXXXXXXXXWWWWVVCCCBBBBAA@@??><;:EFGHIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIJJJJJJKKKLLMMMMMMNNNNOOOOPPQRRSTUVWWXXXXYYYYYYYYYYXXXXXXXXXXXXWWXXXXXWWWVVVVCCBBAAA@@?=<<;:FGGIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIJJJJJJIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIJJJJJKKKLLLMMMMMMNNNNOOQQRRSTTVWWXXXXYYYYYYYYYXXXXXXXXXXXXXXXXXWWWWWWXXWWWWWWVVVVBBAAA@@@=<;;:FGIIIIIIIIIIIIIIIIIIIIIIIJJJJJJJJJJJJJJJJJJJJIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIJJJJJJKKKLLLMMMMMMMMNNNOPQRSSTTUVWXXYYYYYYXXXXXXXXXXXXXXXXXXXXXXXXXXWWWWWWWWWWWWWWWWWWVVCBBAA@@@?<<;::FGHIIIIIIIIIIIIIIIIIIIJJJJJJJJJJJJJJJJJJJJJJJJJJIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIJJJJJKKKKLLLLMMMMMMMMNNOQRRSSTTUVWXXYYYYYXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXWWWWVVVVVWWWWWWWWWWWVVVCBBAA@@@<<;::GIIIIIIIIIIIIIIIIJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJIIIIIIIIIIIIIIIIIIIIIIIIIJJJJJJKKKKKLLLLLMMMMMMNOQQRSSTTTUVVWXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXWWWWWVVVVVVVVVWWWWWWWWWWWVVVCBBAA@@@??>>==<;;:9EEEHIIIIIIIIIIIIIIJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJIIIIIIIIIIIIIIIIIIIIIIJJJJJKKKKKKKLLLLLMMMMNNOQRRSSTTTUVWWXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXWWWWWVVVVVVVVVVVVVVWWWWVVVVVBBBAA@@@??>>==<<;;:FFFFGGGGHIIIIIIIIIIIIIJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJIIIIIIIIIIIIIIIIIIIJJJJJJKKKKKKKKLLLLLMMMNNQRSSSTTTTUVWWXWXXXXXXXXWXXXXXXXXXXXXXXXXXXWWWWVVVVVVVVVVVVVVVVWWWVVVVVVVBBAA@@@??>>>=<::FFFGGHHGFFFGHHIIIIIIIIIJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJIIIIIIIIIIIIIIIIIIIIIJJJJJJKKKKKKKKKLLLQRSSSSTTTUUVWWWWWWXXXXXWWVXXXXWWWWXXXXXXXXXXWWWVVVVVVVVVVVVVVVVVVVVVVVVVVVVUUUBBAA@@@??>>===9FFFGGHHHHHGGGHHHHHIIIIIIIIJJJJJJJJJJJJJJJJJJJJJJJJJJJKKKKKKJJJJJJJJJJIIIIIIIIIIIIIIIIIIIIIIJJJJJJKKKKKKKKKSSSSSTTTUUVVWWWWWWWWWWVVVWWWWWWWWWXXXXXWWWVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUUUUUUAAA@@??>>>==FFGGGHHHHHHHHHHHHHIIIIIIIIJJJJJJJJJJJJJJJJJJJJJJJJJJJKKKKKKKKKKJJJJJJJJIIIIIIIIIIIIIIIIIIIIIIIIIJJJJKKKKKKKSSSSTTTUUWWWWVVVUVWWWWWWWWWWWVVVVVVVVVVVVVVVVVVVVVVUUUUUUUUUUBAA@@@??>>====FFGGGHHHHHHHHHHHHHIIIIJJJJJJJJJJJJJJJJJJJJJJJJJKKKKKKKKKKKKKKKJJJJJJJIIIIIIIIIIIIIIIIIIIIIIIIIIJJJJKKKKKSSSSTTTUUVWWTWWWWWWWWVVVVVVVVVVVVVVVVVUUUUUUUUUUBBAA@@???>>==88FFGGHHHHHHHHHHHHIIIJJJJJJJJJJJJJJJJJJJJJJJJKKKKKKKKKKKKKKKKKKKKJJJJIIIIIIIIIIIIIIIIIIIIIIIIIIIJJJJKKKKSSSTTTUUUVVWWWWWVVVVVVVVVUUUUUUUUBAA@@??>>==98EFFGGGHHHHHHHHHHHIIIJJJJJJJJJJJJJJJJJJJJJKKKKKKKKKKKKKKKKKKKKKKKJJJJJIIIIIIIIIIIIIIIIIIIIIIIIIIIJJJKKKKSSSTTTUUVVWWWWWVVVVVVVVVVVVVVUUUUUUUUUUBBBBBAAA@???>>=<=8CFFGGGGHHHHHHHHHHHIIIJJJJJJJJJJJJJJJJJJJJKKKKKKKKKKKKKKKKKKKKKKKKKKJJJIIIIIIIIIIIIIIIIIIIIIIHHHIIIIJJJKKSSSTTTUUVVVWWWWVVVVVVVVVUUUUUUUUUUTTTTBBBBAA@@???>>===9GGGGGHHHHHHHHHHHIIIIJJJJJJJJJJJJJJJJKKKKKKKKKKKKKKKKKKKKKKKKKKKKJJJIIIIIIIIIIIIIIIIIIIIIHHHHHHHIIIJJKKSSSSTTTUUUVVVVVVVWVVVUUUUUUUUUTTTTTTBBAAA@@??>>====GGGGGHHHHHHHHHHHHIIIIIIJJJJJJJJJJJJJJJJJKKKKKKKKKKKKKKKKKKKKKKKKKKKKKJJJIIIIIIIIIIIIIIIIIIIIHHHHHHHHHIIIJJJKKSRSSTTTTUUUUVVVVVTTVVUUUUUUUUUTTTTTTTTSBBAA@@???>>===FGGGGGHHHHHHHHHHHHHIIIIIIJJJJJJJJJJJJJJJJKKKKKKKKKKKKKKKKKLLLLLKKKKKKKKKJIIIIIIIIIIIIIIIIIIIIHHHHHHHHHIIIJJKRRSSSTTTTUUUUUUVVVVVTTUUVVVUUUUUUUUUTTTTTTTTSSSRBBA@@???>>=====FGGGGGHHHHHHHHHHHHHHIIIIIIIIIIIIIIIIJJJJJJJJJJJJKKKKKKKKKKKKKKLLLLLLLLLLKKKKKKKJIIIIIIIIIIIIIIIIIIHHHHHHHHHHHIIJJJKKRRRSSSTTTTTTUUVVVVUUUUUUTTTTTTUVVVUUUUTTTTTTTTSSRRQQQQCAA@@??>>>=====::FFGGGGGHHHHHHHHHHHHIIIIIIIIIIIIIIJJJJJJJJJJJJKKKKKKKKKKKKKLLLLLLLLLLLLLKKKKKKJJIIIIIIIIIIIIIIIHHHHHHHHHHHHHIIJJJKKRRSSSSTTTTUUUUUUUUUUUUTTTTSSSTUVVVVVVVUUTTTTTSSSSSRRBAAA???>>>===::FFGGGGGHHHHIIIIIJJJJJJJJJJJJJJKKKKKKKKKKLLLLLLLLLLLLLLKKKKKKJIIIIIIIIIIIIIIIIIIHHHHHHHHHHHHHIIIJJKKKKRSSSSSSUUUUUUUUUTTTSSSSTTUUUVVVVUUUUTTTTTSSSSSRRRQDDCCAAA@@???>>>==;;:C?FFFGGGGIIIIIJJJJJJJJJJJJJJKKKKKKKKKLLLLLLLLLLLLLLLLLLKKKKKJJJIIIIIIIIIIIIIIIIIIIHHHHHHHHHHHHHHHIJJKKKRSSSSSUUUUUUUTTSSSRRSSTUUUUUUUUUUUUUUTTTTSSSSSRRRRRQQQCAAA@????>>==EEFFFGGGGGHHHHHHHIIIIIIIIIIIIJJJJJJJJJJJJJJJKKKKKKKKKLLLLLLLLLLLLLLLLLLLKKKKKJJJIIIIIIIIIIIIIIIIIHHHHHHHHHHHHHHHIIJJJKKKRRRSSUUUUUUUUUTTTSSRRQSSTTUUUUUUUUUUTTTTSSSRRRRRQQQQPDDCCA@??>>=;:EEEEF@AFGGGGGGHHHHHHHHHHHHIIIIIIIIIIIIIIJJJJJJJJJJJJJJJKKKKKKKKLLLLLLLLLLLLLLLLLLKKKJJIIIIIIIIIIIIIIIIIIHHHHHHHHHHHHHHHHIIJJJKQRRRRTTUUTRQQQSSTUUUUUUUUUUTTTTSSSSSSRRRQQQQPPPDCCCA@@???>>><;;;9EEFFFFFFFFFGGGGGGHHHHHHHHHHHHHHHIIIIIIIIIIIIIJJJJJJJJJJJJJJJJKKKKKKKLLLLLLLLLLLLLLLLLLLLLKKKJJJIIIIIIIIIIIIIIIIIIIHHHHHHHHHHHHHHHHIIJJJKKQRRTSTTSRQQRSTTUUUUUUUTTTTSSSSSSSSRRRQQQQPPPPPOODDCCCCB?>>>>=<;;;8CEEFFFFGGGGGFFFFFEFFFFGGGGGGGGHHHHHHHHHHHHHHHHHIIIIIIIIIIIIIIJJJJJJJJJJJJJJJJKKKKKKKLLLLLLLLLLLLLLLLLLLLKKKJJJIIIIIIIIIIIIIIIIIIIHHHHHHGGGHHHHHHHHHIIJJKKSSQQQRSSTTTUUUTTTTSSSSSSSRRQQPPPPPOOOODDCCCCA?>==<<<;;EEEFFFGGGGGGGGGGFFFFFFGGGGGGGGGGHHHHHHHHHHHHHHHHHHHIIIIIIIIIIIJJJJJJJJJJJJJJJJJKKKKKKLLLLLLLLLLLLLLLLLLLLKKKJJJIIIIIIIIIIIIIIIIIIHHHHHHGGGGGGHHHHHHHHIIJJJKKJSQQQQRRSSTTTTTTTSSSSSSSRRRRQPOOOOOOOOOODCCCCC?>==<<;;:EEEFFFGGGGGGGGGGGGGGGGGGGGGGGGGGGHHHHHHHHHHHHHHHHHHHIIIIIIIIIIIJJJJJJJJJJJJJJJJJKKKKKKLLLLLLLLLLLLLLLLLLLLLKKKJJJIIIIIIIIIIIIIIIIIHHHHHHHGGGGGGGGHHHHHHHIIIJJKKJJJIIQQPPPRRSSSSTTTSSSSRQPOOOOOOOONDCCCCCB@@>>==<<<<<;EEFFFFFGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGHHHHHHHHHHHHHHHHHIIIIIIIIIIJJJJJJJJJJJJJJJJJJKKKKKLLLLLLLLLLLLLLLLLLLLLKKKJJJIIIIIIIIIIIIIIIIHHHHHHHHGGGGGGGGGGGHHHHHHHIIIIJJJIIHPPPPPQRRSSSSSSSRRQPOOOOOOOONNNDDDCCCCCB@@@?>>>==<<<<<<;FFFFFFGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGHHHHHHHHHHHHHHHHIIIIIIJJJJJJJJJJJJJJJJJKKKKKKLLLLLLLLLLLLLLLLLLLKKKKJJJIIIIIIIIIIIIIIHHHHHHHHHHGGGGGGGGGGGGGHHHHHHHHIIIIIIHHIPPPPPPPPQRRSSSRRQPPPOOOOONNNNLDDCCCCCBBA@@?>>==<<<<<<;9FFFFFFFGGGGGGGGGGGGGGGGGGGGGGGGGGGGGHHHHHHHHHHHHHHIIIIIJJJJJJJJJJJJJJJJJJKKKKKLLLLLLLLLLLLLLLLLLLLKKKKJJJIIIIIIIIIIIIHHHHHHHHHHHGGGGGGGGGGGGGGGGGGHHHHHHHHHHIIPPPPPPQQRRRRSRRRRQPOOOOONNNNMMDDDCCCCCBBA@@??=<<<<FFFFFFFGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGHHHHHHHHHHHHHIIIIJJJJJJJJJJJJJJJJJKKKKKKLLLLLLLLLLLLLLLLLLKKKKJJJIIIIIIIIIIHHHHHHHHHHHHHGGGGGGGGGGGGGGGGGGGGGGHHHHHHHHHHHIIIKPQQQRRRRRRRRRPOOONNNNMMKDDDCCCCCCBBAA@??>==<<;FFFFGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGHHHHHHHHHHHHHHIIIJJJJJJJJJJJJJJJJJJJKKKKKKLLLLLLLLLLLLLLKKKKKKKJJJIIIIIIIHHHHHHHHHHHHHGGGGGGGGGGGGGGGGGGGGGGGGGHHHHHHHHLLMNNOOOPQQQRRRRRRRRQQPPOOONNNMDDDDCCCCCCBBBAA@??><<<;:FFGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGHHHHHHHHHHHHHHHIIJJJJJJJJJJJJJJJJJKKKKKKLLLLLLLLLLLLKKKKKKKKJJJIHHHHHHHHHHHHHHHGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGHHHHHHKLMNNOOPPQQQRRRRRRRQQPPONNNMMDCCCCCCCCBBBAA??>>=<<;:GGGHHHHGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGHHHHHHHHHIIJJJJJJJJJJJJJJJJJKKKKKKKLLLLLLLKKKKKKKKKJJJJIHHHHHHHHHHHHHGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGHHHHKLLMNNNPPPQQQRRRRRRRRRQPPONNMMDCCCCCCCCBBBBAAA>>==<:GGGHHHHHHHGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGHHHHHHHIJJJJJJJJJJJJJJJJJJKKKKKKKKKKLLLKKKKKKKKKKKJJJJIHHHHHHHHHHHHGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGHHKKLLMNNPPPPPQQQRRRRRNNMMLDCCCCCCCCBBBBA?>>==<::GHHHHHHHHHHGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGHHHHHHHIIJJJJJJJJJJJJJJJJJJKKKKKKKKKKKKKKKKKKKKKKJJJJIIHHHHHHHHHHHHHGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGHKLMMNPPPPPPPQQRRRRRRRRRRRRNMMICCCCCCCBBBAA?>=<;HHHHHHHHHHHGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGHHHHIIIJJJJJJJJJJJJJJJJJJJKKKKKKKKKKKKKKKKKKKJJJJIIIIHHHHHHHHHHHHGGGGGGGGGGGGGGGGGGGGFFFGGGGGGGGGKKKLLMNPPPPPPPOOQQQRRRRRQRKMMLHCCCCCCCBBBBAAA?>==<<HHHHHHHHHHHHHHGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGHHHHIIIIIJJJJJJJJJJJJJJJJJJJKKKKKKKKKKKKKKKJJJJIIIIHHHHHHHHHHGGGGGGGGGGGGGGGGFFFFFFFFFFGGGGGGIIIJKLLMNOPPPPOOOOOOOQQPOQQQPLHGFCCCCCCCBBBBAA@>>==<;;::HHHHHHHHHHHHHHGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGHHHHIIIIIIIJJJJJJJJJJJJJJJJJJJJJKKKKKKKKKKJJJJJJIIIIIHHHHHHHHHHHHGGGGGGGGGGGGGGGGGFFFFFFFFFFFFGGGGHHIIIJKLMMOOPOOOOONNNNNHGGECCCCCCBBBBBAA@??>>==<;;HHHHHHHHHHHGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGHHHIIIIIJJJJJJJJJJJJJJJJJJJJJJJJJKKKKJJJJJJJJJIIIHHHHHHHHHHHHHGGGGGGGGGGGGGGGFFFFFFFFFFFFFFFFGGGGHHIIIJJKLMNOOOOOONNNNNMMIHHGEEECCCCCCBBBBAA@?>>===;HHHHHHHHHGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGHHHHHIIIIJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJIIIIIHHHHHHHHHHHHHHHGGGGGGGGGGGGGFFFFFFFFFFFFFFFFFFFGGHHHIIIJNNOOOOONNNNMMMMMHHGFEEEDDCCCCCBBBBAAA@??>>==<;HHHHHHHHHGGGGGGGGGGGGGGGGGGGGGFFGGGGGGHHHHHHHIIJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJIIIIHHHHHHHHHHHHHHHGGGGGGGGGGGGFFFFFFFFFFFFFFFFFFFFGGGGHHHIIJLNNOOOONNNMMMMMLLLHHGGFEEECCCCBBBAAAA@?>==<<HHHHHHHHHHHHGGGGGGGGGGGGGGGGGFFFFFFGGGGGHHHHHHHHHIIJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJIIIIIIHHHHHHHHHHHHHHHHGGGGGGGGGGGFFFFFFFFEEEEEEEFFFFFFGGGGHHHIIJLMMMNNOOONNMMMMLLLLKKKLIHHGFFEDCBBBAAA@@?>=<<GHHHHHHHHHGGGGGGGGGGGGGGGFFFFFFFFFFFGGGGGHHHHHHHIIJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJIIIIIHHHHHHHHHHHHHGGGGGGGGGGFFFFFFEEEEEEEEEEEFFFGGGGGHHHIIJLMNNOMMLLLLKKKKLLHHGGFEEEDCBBBAAA@@@???==<<HHHHHHHHGGGGGGGGGGGGGGGFFFFFFFFFGGGGGGGHHHHHHIJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJIIIIIIHHHHHHHHHHHGGGGGGGGFFFFFEEEEEEEEEEEEEEEFGGGGGGHHHIIJLMMLLLKKKKJLLLIHHGGFEEEDDDBBBAA@@@@??>>=<<GHHHHHHHHGGGGGGGGGGGGGGGGGGFFFFFFFFFGGGGGGGGGHHHHJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJIIIIIIIHHHHHHHHHHHHHGGGGGGGFFFFEEEEEEEEEEEEEEEEEFFGGGGHHHIIJLLKLLLKKKKJJJJJJJJKKKKLLJIHHHGFFEEDDDDDDDAAA@@@???>==<HHHHHHHGGGGGGGGGGGGGGGGGGGFFFFFFFFGGGGGGGGGGGHHJJJJJJJJJJJJJJJJJJJJJJJJJJJIIIIIIIIIHHHHHHHHHHHHHHHHHHGGGGGGGFFFFEEEEEEEEEEEEEEEEEFFFFGGGHHIIJKKKKKJJJJIIJJJJJJKKKKLLLKKIHGGFFEDDDDDBAA@@@@??>>=<<HHHHHHGGGGGGGGGGGGGGGGGGGFFFFFFFFFFGGGGGGGGGHHIJJJJJJJJJJJJJJJJJJJJJJJJJJJIIIIIIIIIHHHHHHHHHHHHHHHHHHHGGGGGGFFFFEEEEEEEEEEEEEEEFEFFFFGGHHHIIJJKJJJJIIIIIIIJJJJKKKKHGGFEEDDDA@@@??>==<<HHHHHHHHGGGGGGGGGGGGGGGGGGFFFFFFFFGGGGGGGGHIJJJJJJJJJJKKKJJJJJJJJJJJJJIIIIIIIIIHHHHHHHHHHHHHHHHHHGGGGGGFFFFEEEEEEEEEEEEEEEEEFEEFFFGGGHHIIIJKHHHHHIIIIIIIIJJKKKHGGFFEEDDA@@@??>>==<<HHHHHHHHHHHHHGGGGGGGGGGGGGGFFFFFFFGGGGGGGHIIJJJJJJJKKKKKKKJJJJJJJJJJIIIIIIIIHHHHHHHHHHHHHHHHHHGGGGFFFFEEEEEEEEEEEEEEEEEEEEEFFFGGHHHIIIHHHHGGHHHIIIIIIIGGFFEEEDDD@@???>===<<GHHHHHHHHHHHHHHHHHHHGGGGGGGGGFFFFFGGGGGGHIIJJJJJJKKKKKKKKKJJJJJJIIIIIIIHHHHHHHHGGGGFFFFEEEEEEEEEEEEEEEEEEEEFFGGGHHHIIIHHHHGGGGFFFFGGHHIIIIIJJFFEEDDDDBA@@@??>>>>=<EFGGHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHGGGGGGGGFFFFFFFFGGGGHIIJJJJJKKKKKKKKKKKJJJJJIIIIIIHHHHHHHHHGGGGFFFFEEEEEEEEEEEEEEEEEEEEEEFFGGGGHHHIIIHHHHGGGGGFFFFHHHIIIIGFFEEEDDDDBA@@@?>>>>==<<GGGGHHHHHHHHHHHHHHHHHHHHHHHHHHHHGGGGGGFFFFFFFFFGGGGIIIJJJJKKKKKKKKKKKJJJJIIIIIIHHHHHHHHHGGGGFFFFEEEEEEEEEEEEEEEEEEEEEEEEFFGGGGHHHHIIIHHHGGGGGGFFFFFFGGGHHHIIIIIGFFEEEEDDDBAAA@??>>>>>=<<GGGHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHGGGFFFFFFFFFFGGGGHIIJJJKKKKKKKKKKKKJJJJIIIIIIIHHHHHHHHHHHHHHGGGGGFFFEEEEEEEEEEEEEEEEEEEEEEEFFGGGGGGHHHHHHGGGGGGGFFFFFFFFFFFFFGGGHHHIIIIIIHHFFFEEEDDDDCBBBAAA@??>>>>>>=<GHHHHHHHHIIIHHHHHHHHHHHHHHHHHHHHHFFFFFFFFFFFGGGHIIIJJKKKKKKKKKKKKJJJJIIIIIHHHHHHHHHHHHHHGGGGGFFFEEEEEEEEEEEEDDDDEEEEEEEEEEEEEFFGGGGGGGGHHHGGGGGGGGFFFFFFFEEEEEFFFFFFGGGHHHIIIIIHHFFFFEEEEDDDDDCCCBBBAA@??>>>>>=<;;FGGHHHHHHIIIIIIIIIHHHHHHHHHHHHHHHHHHHGFFFFFFFFFFFFGGGHIIIJKKKKKKKKKKKKKJJJIIIIIHHHHHHHHHHHHHHHHHGGGGGFFFFEEEEEEEEEDDDDDDDEEEEEEEEEEFFFGGGGGGGGGGGGGGGGFFFFFFFFEEEEEEEEEFFFFFGGGHHHIIIHHHGFFFFFEEEEDDDDDDDCCCCBBBAA@?>>>>><<;:GGHHHHIIIIIIIIIIIIIHHHHHHHHHHHHHHHHHHFFFFFFFFFFFFFFFFGGGHHIIJKKKKKKKKKKKKJJJJIIIHHHHHHHHHHHHHHHHHHGGGGGFFFFEEEEEEEEDDDDDDDDDEEEEEEEEEFFFGGGGGGGGGGGGGFFFFFFFEEEEEEEEEEEEEFFFFGHHHHHHHHHGFFFFFFFFEEEDDDDDDDDDCCCCBBBBA@@?>>>>><<;GHHHHIIIIIIIIIIIIIIHHHHHHHHHHHHHHHHHHHGGGGGGFFFFFFFFFFFFFFFGGIIJKKKKKKKKKKKKJJJJIIHHHHHHHHHHHHHHHHHHHHHHGGGGGGFFFEEEEEEEDDDDDDDDDDDEEEEEEEEEFFFFGGGGGGGGGFFFFFFEEEEEEEEEEEEEEEEEFFGGGHHHHHHHGGFFFFFFFFEEEEDDDDDDDDCCCCCBBBBA@@?>>>>=<<;:GHHIIIIIIIIIIIIIHHHHHHHHHHHHHHHHHHHHGGGGGGGGGGGGGGFFFFFFFFFFFFFFFFFGJKKKKKKKKJJJJJIIIHHHHHHHHHHHHGGGGGGGFFFEEEEEEEDDDDDDDDDDDDDEEEEEEEEEEFFFFFGGGFFFFFFFEEEEEEEEEEEEEEEEEEFFFFFGGGHHHHHHGFFFFFFFFEEEDDDDDDDDDCCCCBBBBBAA@??>>>=<;;GHHIIIIIIIIIIIIIIHHHHHHHHHHHHHHHHHHHHHHHHGGGGGGGGGGGGGGGGGGGFFFFFFFFFFFFFFFFFFFKKKKKKJJJJJJHHHHHHHHHHHHHHHGGGGGGGFFEEEEEEDDDDDDDDDDDDEEEEEEEEEEEEFFFFFFFFFFEEEEEEEEEEEEEEEEEEEEEFFFFFGGHHGGGFFFFFFFFFEEEEDDDDDDDDCCCCCBBBBBAA@@?>>>>=;:GHHIIIIIIIIIIIIIIHHHHHHHHHHHHHHHHHHHHHGGGGGGGGGGGGGGGGGGGGGGFFFFFFFFFFFFFFFFFFFFFFFFFKKKJJJJJJIIHHHHHHHHHHHHHGGGGGGGFFFEEEEEDDDDDDDDDDDDDDEEEEEEEEEEEEEEFFFFFEEEEEEEEEEEEEEEEEEEEEEEEEFFFFFFGGGGFFFFFFFFFFEEEDDDDDDDDDDCCCCCBBBBBBA@??>>==<;:HIIIIIIIIIIIIIIIIHHHHHHHHHHHHHHHHHHHGGGGGGGGGGGGGGGGGGGGGGGFFFFFFFFFFFFFFFFFFFFFFFFFKJJJJJJJJJIIIHHHHHHHHHHHHHHHHHGGGGGGGGGFFFEEEEDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFFFFFFEEEEDDDDDDDDDCCCCCBBBBBA@??>>=<<;HHIIIIIIIIIIIIIHHHHHHHHHHHHHHHHHHHHGGGGGGGGGGGGGGGGGGGGGGFFFFFFFFFFFFFFFFFFFFJJJJJHHHHHHHHHHHHHHGGGGGGGGGFFFEEEEDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFFFFFFFFEEEDDDDDDDDDCCCCCCBBBBBBBA?>>=IIIIIIIIIIIIIHHHHHHHHHHHHHHHHHHHGGGGGGGGGGGGGGGGGGGGFFFFFFFFFFFFFFFHHHHHHHHHHHHHGGGGGGGGGFFFEEEEDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFFFFFFEEEEDDDDDDDDCCCCCCBBBBBBB@@>=HIIIIIIIIIIIIIHHHHHHHHHHHHHHHHHHGGGGGGGGGGGGGGGGGGGFFFFFFF2FFFFFFFFFIIIIHHHHHHHHHHHHGGGGGGGGGGFFEEEEDDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFFFFEEEEDDDDDDDCCCCCCCCBBBBBBBAA?>HIIIIIIIIIIIIIIIIIHHHHHHHHHHHHHHHGGGGGGGGGGGGGGGGGFFFFFF2HFFFFFFFHHIHHHHHHHHHHHHHGGGGGGGGGGGFFFEEEDDDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEEFFFFFFEEEEEEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFEEEEEDDDDDDCCCCCCCCCCBBBBBBBBBA@IIIIIIIIIIIIIIIIIHHHHHHHHHHHHHHGGGGGGGGGGGGGFFFF2FHFFFFFFFFGGHHHHHHHHHHHHHHGGGGGGGGGGGGFFFEEEDDDDDDDDDDDDDDDDDEEEEEEEEEEEFFFFFFFFFEEEEEEEEEEEEEEEEEEEEEEFFFFFFFFFFEEEEEEDDDDDCCCCCCCCCCCBBBBBBBBA@?<IIIIIIIIIIIIIIIIIIIIIIHHHHHHHHHHHHHHGGGGGGGGGGGGGGFFFFFFFFFFFFFFFFGGGGHHHHHHHHHHHHHHGGGGGGGGGGFFFEEEEDDDDDDEEEEEEFFFFFFFFFFFEEEEEEEEEEEEEEEEEEEEEEEEFFFFFEEEEEEEDDDDDDCCCCCCCCCCCCBBBBBBAA@?>IIIIIIIIIIIIIIIIIIIIHHHHHHHHHHHHHHHHGGGGGGGGGGGFFFFFFFFFFFFFFFFFFGHHHHHHHHHHHHGGGGGGGGGGGGGFFFEEDDDEEEEEFFFFFFFFFFFFEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEDDDDDDDCCCCCCCCCCCBBBBBBAA@>=<IIIIIIIIIIIIIIIIIIIHHHHHHHHHHHGGGGGGFFFFFFFFFFFFFFFFFFGGIIHHHHHHHHHHHGGGGGGGGGGGGGGFFFEEEDDEEFFFFFFFFFFFFFEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEDDDDDDDDDDCCCCCCCCCCCBBBBBBBA@@=HHIIIIIIIIIIIIIIIIIIIIIHHHHHHHHHHHHHHGGGGGGFFFFFFFFFFFFFFFIIIIHGHHHHHHHHHHHHHHHGGGGGGGGGGGGGGFFFFEEEEEEFFFFFFFFFFFFFEEEEEEEEEEEEEEEEEEEEEEEEEEDDDDDDDDDDDCCCCCCCCCBBBBBBBBBBAA@><IIIIIIIIIIIIIIIIIIIIIIIIIHHHHHHHHHHHHHGGGGGHFFFFFFFFFFFFFHGGGHHHHHHHHHHHHHHHHHHHHHHHGGGGGGGGGGGGGGFFEEFFFFFFFFFFFFFFEEEEEEEEEEEEEEEEEEEEEEEDDDDDDDDDDDDDDDCCCCCCBBBBBBBBBBBBBBA@?<IIIIIIIIIIIIIIIIIIIIIIIIIIHHHHHHHHHHHHHHHHHHGGGGGGGGGFFFFFFFFFFFFFFGGGHHHHHHHHHHHHHHHHHHHHHHHHHHHHGGGGGGGGGGGGGGFDEEEEEFFFFFFFFFFFFFFFFEEEEEEEEEEEEEEEEEEDDDDDDDDDDDDDDCCCCCCCCBBBBBBBBBBBBBBBBAA?=<IIIIIIIIIIIIIIIIIIIIIIIIIIIIHHHHHHHHHHHHHHHHGGGGGGGGGGFFFF22FHFFFFFFFGGHHHHHHHHHHHHHHHHHHHHHHHHHHHHHGGGGGGGGGGGGGEEEEEEEFFFFFFFFGGGGFFFFEEEEEEEEEEEEEEEEEEDDDDDDDDDDDDDDDDCCCCCCBBBBBBBBBBBBBBBBBBA@?=<IIIIIIIIIIIIIIIIIIIIIIIIIIIIHHHHHHHHHHGGGGGGGFFF2FFFFFFFFGGGGHHHHHHHHHHHHIIHHHHHHHHHHHHHGGGGGGGGGGGGGDDDEEEEEEEEEEEEFFFFFGGGGGGGFFFFEEEEEEEEEEEEEEEEEEDDDDDDDDDDDDDDCCCCCCCBBBBBBBBBBBBBBBBBBBA@@>=;IIIIIIIIIIIIIIIIIIIIIIIIIIIIHHHHHHHHHHHHGGGGGFFFFFFFFFFFFFGGGGGGGHHHHHHHHIIIIIIHHHHHHHHHHHHGGGGGGGGGGGDDDDDDDDEEEEEFFFFFEEEFFFFGGGGGGGGFFFFEEEEEEEEEEEEEEEEEDDDDDDDDDDDDCCCCCCBBBBBBBBBBBBBBBBBBBBA@@>=<;IIIIIIIIIIIIIIIIIIIIIIIIIIIIHHHHHHHHHHGGGGGFFFFFFFFFFFGGGGGGGGHHHHHIIIIIIIIHHHHHHHHHHHHGGGGGGGEDDDDDDDEEEEEFFFFFFFEEEEEFFFFGGGGGGGGGFFFEEEEEEEEEEEEEEEEEEDDDDDDDDDDDDDCCCCCBBBBBBBBBBBBBBBBBBBBBA@?>=<;JJJIIIIIIIIIIIIIIIIIIIIHHHHHHHHGGGGGGGFFFFFFFFFFFFFFFGGGGGGGHHHIIIIIIIIIIHHHHHHHHHHGGGGEDDDDDDEEEEFFFGGFFFEEFFFFGGGGGGGGGGGGFFFEEEEEEEEEEEEEEEEEDDDDDDDDDDDDDCCCCCBBBBBBBBBBBBBBBBBBBBAA@?>=<;JJJJJJJJIIIIIIIIIIIIIIIIIHHHHHHHGGGGGGGGGFFFFFFFFFFFFFFFFFFGGGGGGHHHHIIIIIIIIIIIHHHHHHHHGGGGEDDDDDDDDDDDDEEEEEFFGGGGFFFFEEEEFFGGGGGGGGGGFFFFEEEEEEEEEEEEEEEDDDDDDDDDDDDDDDDCCCCBBBBBBBBBBBBBBBBBBBBBAA@?>>=<;JJJJJKJJJJIIIIIIIIIIIIHHHHHGGGGGGGGGGFFFFFFFFFFFFFFFFFFFGGGGGGHHHHIIIIIIIIIIIIHHHHHHGGFFFEEDDDDDDDDDDDEEEEFFGGGGGGFFFFEEEFFGGGGGGGGGGGGGGFFFEEEEEEEEEEEEEEEEDDDDDDDDDDDDDDDCCBBBBBBBBBBBBBBBBBBBBBBBA@?>>=<JJJJKKKJJJIIIIIIIIIIHHHGGGGGGGGGGGGGGFFFFFFFFFFFFFFFFGGGGHHHHIIIIIIIIIIIIIIHHHHHHGGGGGGFFFFFEEDDDDDDDDDEEEEEFFGGGGGGFFFFEEFGGGGGGGGGGGGGFFFEEEEEEEEEEEEEEEEDDDDDDDDDDDDDDDDCCCBBBBBBBBBBBBBBBBBBBBA@??>>==<JJJJJKKKKKIIIIIIIIIIIHGGGGGGGGGGGGGGGGGFFFFFFFFFFFFFFGGGGHHHHIIIIIIIIIIIIIIHHHHHHGGGGGGGGFFFFFEEDDDDDDDDDDDDEEEEFFGGGGGGGGFFFFFEFGGGGGGGGGGGGGGFFEEEEEEEEEEEEEEDDDDDDDDDDDDDCCCCCCCBBBBBBBBBBBBBBBBBBBBBBA@??>>==<JJJJKKKKKKIIIIIIIIIIIIIIGGGGGGGGGGGGGGGGGGGGGGFFFFFFFFFFGGGHHHIIIIIIIIIIIIIIIIHHHHGGGGGGGGFFFFFEEDDDDDDDDDDDEEEEFFGGGGGGGGGGFFFGGGGGGGGGGGGGGFFFEEEEEEEEEEEDDDDDDDDDDDDDDCCCCCCCCBBBBBBBBBBBBBBBBBBBBBBB@@??>>=<<JJJKKKKKKKKIIIIIIIIIIIIIIIGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGFFFFFFFFFGGGHHHIIIIIIIIIIIIIIIHHHHGGGGGGGGGFFFFFEEEDDDDDDDDDDDDEEEFFGGGGGGGGGGGGGFFFGGGHHHGGGGGGGGFFFFEEEEEEEDDDDDDDDDDDDDCCCCCCCCCBBBBBBBBBBBBBBBBBBBBBBB@@??>>=;;GHIJJKKKKKKKKIIIIIIIIIIIIIIIHGGGGGGGGGGGGGGGGGGGGFFFFFFFGGGHHHIIIIIIIIIIIIIIIIHHHGGGGGGGGGGGFFFFFEEEDDDDDDDDDDDDDDEEFFGGGGGGGGGGGGGGGHHHHHHHHGGGGGGGFFEEEEDDDDDDDDDDDCCCCCCCCCCBBBBBBBBBBBBBBBBBBA@?>>==<<;GHHIJJKKKKKKKIIIIIIIIIIIIIIHHHGGGGGGGGGGGGGGGGGGGFFFFFFFGGGHHHHIIIIIIIIIIIIIIIHHHGGGGGGGGGGFFFFFFEEEEDDDDDDDDDDDDDDDEEFFGGGGGGGGGGGGGGHHHHHHHGGGGEEEEEEDDDDDDDDDCCCCCCCCCCCBBBBBBBBBBBBBBBBBBA@@??>==<;;FGHIIJJKKKKKKIIIIIIIIIIIIIIIHHHHGGGGGGGGGGGGGGGGGFFFFFFFFFFFFGGHHHIIIIIIIIIIIIIIHHGGGGGGGGFFFFFFFEEEEDDDDDDDDDDDDDDDDEEFGGGGGGGGGGGGGGHHHHHHEEEEEEEDDDDDDDDCCCCCCCCCCCBBBBBBBBBBBBBBBBBBBBA@@@?>>==<<;FGHHIJJKKKKKKKJIIIIIIIIIIIIHHHHHIGGGGGGGGGGGGGGGGGFFFFFFFFFFFFFFGGGHHHIIIIIIIIIIIIIIHHHGGGGGGFFFFFFFFFFEEEEEDDDDDDDDDDDDDDDDEEGGGGGGGGGGGGGGHHHHEEEEEDDDDDCCCCCCCCCCCBBBBBBBBBBBBBBBBBBBBAA@@??>>==<;GHIIJJKKKKKKJJIIIIIIIIHHHIGGGGGGGGGGGGGGGGGGGGFFFFFFFFFFFFFGGGHHHHIIIIIIIIIIIIIIHHGGGGGFFFFFFFFFFFFFEEEEEEDDDDDDDDDDDDDDDFFFGGGGGGGGGGHHHHEEEEEDDDDDCCCCCCCCCCCCBBBBBBBBBBBBBBBBBBBB@@?>>==<GHIIJJJKKKKKKJJIIIIIIIIHHHGGGGGGGGGGGGGGGGGGGGGGGGGFFFFFFFFFFFFFGGHHHHHIIIIIIIIIIIIIIHHHGGGFFFFFFFFFFFFFFEEEEEEEDDDDDDDDDDDDDDDDDDDEEEFFGGGGGGGGGHHHHHEEEEEDDDCCCCCCCCCCCCBBBBBBBBBBBBBBBBBBBBAA@@??>=<GHHIIJJKKKKKKJJJIIIIIIIIHHHHGGGGGGGGGGGGGGGGGGGGGGGGGGGFFFFFFFFFFFFGGHHHHHHHIIIIIIIIIIIHHHGFFFFFFFFFFFFEEEEEEEEEEEEDDDDDDDDDDDDDDDDDDDDDDEEFGGGGGGGGGHHHHHHEEEEDDDCCCCCCCCCCBBBBBBBBBBBBBBBBBBBBAA@@?>>=<<FGHIIJJJKKKKKKKJJJIIIIIIIIIIHGGGGGGGGGGGGGGGGGGGGGGGGGGFFFFFFFFFFFFFFGHHHHHHHHHHIIIIIIIHHHHFFFFFFFFFEEEEEEEEEEEEEEEEDDDDDDDDDDDDDDDDDDDDDDDDEFFGGGGGGGHHHHHHHHHGGGGGFFFFFFEEEEEEDDCCCCCCCCCCCCBBBBBBBBBBBBBBBBBBBBBBBA@??>GHIIJJKKKKKKKLLKKJJIIIIIIIIHHHGGGGGGGGGGGGGGGGGGGGGGGGGGGGFFFFFFFFFFFFFFGHHHHHHHHHHHHHIIHHHFFFFFFFEEEEEEEEEEEEEEDDDDDDDDDDDDDDDDDDDDDDDDDEEFGGGGGGGHHHHHHHHHHHHHHHGGGGFFFFFFFFEEEEEEDDCCCCCCCCCCCCBBBBBBBBBBBBBBBBBBBBBBBBBA@?>=<5GHHIIJJKKKKKLLLKKJJJIIIIIIIHHHHHHHGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGFFFFFFFFFFFGGGHHHHHHHHHHHHHFFFFFEEEEEEEEEEEEEEEEEEEDDDDDDDDDDDDDDDDDDDDDDDDDDDEFFGGGGGHHHHHHHIIIIIIIIIIHHHHHHHGGGGGGFFFFFFFFEEEEEEDDCCCCCCCCCCCCBBBBBBBBBBBBBBBBBBBBBBBBA@?>EFGHIJKKLLLLLLLLKKKKJJIIIIIIIIHHHHHHHHGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGFFFFFFFFFFFFFFFFGGGGGHHHHHHHHHHGFFFFEEEEEEEEEEEEEEEDDDDDDDDDDDDDDDDDDDDDDDDDDDDEEFGGGGGGHHHHHIIIIIJJJJIIIIIIIIHHHHHHGGGGGGGFFFFFFEEEEEDDCCCCCCCCCCCCBBBBBBBBBBBBBBBBBBBBBBBBBA@?>FGGHIIJJKKKLLLLLLLLLLLLKKKKKJJIIIIIIIIIIHHHHHHHHGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGFFFFFFFFFFFFFFFFFFGGGGGGGGHHHHHHHHGFFFFEEEEEEEEEEEEEEEEDDDDDDDDDDDDDDDDDDDDDDDDDDDDDEFFGGGGGHHHHIIIIJJJJJJJJJIIIIIIIHHHHGGGGGGGFFFFFEEEEEDDCCCCCCCCCCCCBBBBBBBBBBBBBBBBBBBBBBA=FGHHIIJKKKLLLLLLLLLLLLLKKKKKJJIIIIIIIHHHHHHHGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGFFFFFFFFFFFFFFFFFFFFFFFGGGGGGGGGGGHHHGGFFFFEEEEEEEEEDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDEEFGGGGGHHIIIIJJJJJJJJJJJJJIIIIIIHHHHGGGGGGGFFFFFEEEEEEDDCCCCCCCCCCCBBBBBBBBBBBBBBBBBBBBBBBBAA@EFHIJJKKLLLLMMMMLLLLLLLLLLKKKKJJIIIIIIIIHHHHHHHGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGFFFFFFFFFFFFFFFFFFFFFFFFFFFFFGGGGGGGGGGGGGHHHGGGFFFEEEEEEEEEEDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDEFFGGGHHIIIJJJJJJJJJJJJJJIIIIHHHHHGGGGGGFFFFFEEEEEDDCCCCCCCCCCCBBBBBBBBBBBBBBBBBBBBBBBBBAA@8EFGHIIJJKLLLLMMMMMMMLLLLLLLLLLKKKJIIIHHHHGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFGGGGGGGGGGGGGGGFFFFEEEEEEDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDEFGGGHHHIJJJJJJJJJJJJJJJJJIIIIHHHHHGGGGGFFFFFFEEEEEDDDCCCCCCCCCCCBBBBBBBBBBBBBBBBBBBBBBBBBAA@:EGHHIJJKKLLLMMMMMMMMMMLLLLLLLLLKIIIHHHHGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGFFFFFFFFFFFFFFFFFFFFFFFFFFFFGGGGGGGGGGGGGGFFFEEEEDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDEEFFGGHHIIIJJJJJJJJKKKKKKKJJJIIIIHHHHHGGGGGFFFFFEEEDDDDCCCCCCCCCCCBBBBBBBBCCCCCCCBBBBBBBBAA@EGHIIJJKLLLMMMMMMMMMMMLLLLLLLLLIIHHHHGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGFFFFFFFFFFFFFFFFFFFFFFFFFFFGGGGGGGGGGGGGFEEEDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDEEEFFGGHHIIJJJJKKKKKKKKKKKKJJJJJIIIHHHHGGGGGFFFFFEEDDDDCCCCCCCCCCCCCCBBBBBCCCCCCCCCCCCCCBBBBBBAADEFHIJJKKLLLMMMMMMMMMMMMMMMLLLLIIIHHHGGGGGGGGGGGGGGGGGGGGGGGGGFFFFFFFFFFFFFFFFFFFFFFFFFFFFGGGGGGGGFFFEEEEDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDEEEFFGGHIIIJKKKLLLLLLLKKKKKKKJJJJJIIHHHHGGGGFFFEEEDDDDDDCCCCCCCCCCCCCCCCCCCCCCCCCCCBBBBBAAEFHIJJKKLLMMMMMMMMMMMMMMMMLLLLIIIIHHGGGGGGGGGGGGGGGGGGGFFFFFFFFFFFFFFFFFFFFFFFFFFFFGGGGGGGGGGGGGFFFEEEDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDEEEFFGGHHIJJKKKLLLLLLLLLLLLLKKKKKJJJJIIHHHGGGGFFFFEDDDDDDDDCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCBBBEFIJJKKLLMMMMMMMMMMMMMMMMMLLIIIIIGGGGGGGGGGGFFFFFFFFFFFFFFFFFFFFFFFFFFFGGGGGGGGGGGGFFFEEDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDEEEEFFHIIJJKKKKKLLMMMMMMMMLLLLKKKKKJJJIIHHHGGFFEDDDDDDDDDDCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCBBBAEFHIJKKLLMMMMMMMMMMMMMMMMMMMIIIIGGGGGGGGGGGGGFFFFFFFFFFFFFFFFFFFFFFFFFGGGGGGGGGGGGFFFFEEDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDEEEEEGHHIJJKKKLMMMMMMMMMMMLLLLKKKJJJIHGFEEDDDDDDDDDDCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCBBBEGHIJKKLLMMMMMMMMMMMMMMMMMMMMIIIIGGGGGGGGGGGGGGGGFFFFFFFFFFFFFFFFFFFFFFFFFGGGGGGGGGFFFFFEEDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDEEEEEEFGGHIJKKKLLLNNNNNNNNNNNNMMMLLKKKJJJHHGGGFEEDDDDDDDDDDDCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCFGHIJKKLLMMMMMMMMMMMMMMMMMMMJJIIHGGGGGGGGGGGGGGGGGGFFFFFFFFFFFFFFFFFFFFGGGGGFFFFFFEEDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDEEEEEFGGHIJJLLLOOOOOOOOONNMMMLLKKJJJIGGDDDDDDDDDDDDDCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCFGIIIKKLLMMMMMMMMMMMMMMMMJJJHHHGGGGGGGGGGGGGGGGFFFFFFFFFFFFFFFFFFFFFGGGFFFFFFFFFEEDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDEEEEFFFFGGHIJJKLLPPPOOOOOOONNMMLLKKJIDDDDDDDDDDDDDDCCCCCCCCCCCCCCCCCCCCCCCCCCCCCFFIIIKKLLMMMMMMMMMMNNNMMMMMKJIHHGGGGGGGGGGGGGFFFFFFFFFFFFFFFFFFFFFFFGFFFFFFFFFFFEEDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDEEFFFFFGHIIJLQQPPPPPPPPPOONNMMLLKKJJIHEEDDDDDDDDDDDDDDDCCCCCCCCCCCCCCCCCCCCCCCCCBFFGKKLMMMMMMMMNNNNNNMMLKKIIIHGGGGGGGGGGGGHHGGFFFFFFFFFEEEFFFFFFFFFFFFFFFFFFFFFFEDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDEEFFFFFFGHHIJSSRQQQQQQQPPPOONNMLLKJFFEEEDDDDDDDDDDDDDDDCCCCCCCCCCCCCCCCCCCCCCCCGGKKMMMMMMMNNNNNNNNNLKKKIIIIHGGGGGGGGGGGGGHGFFFFFFFFEEEEEEEFFFFFFFFFFFFFFFFFFEEDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDEFFFFFFGHIJJRSSTTTTSRRRQQQQQPPOONMMLLKJIFFFFEDDDDDDDDDDDDDDDDDCCCCCCCCCCCCCCCCCCCKLMMMMNNNNNNNNONNNLLLKKKJJIIIHHGGGGGGGGGGFFFFFEEEEEEEEFFFFFFFFFFFFFFFFFFFFEDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDEEFFFFFGGHIJSSSTTUUUUUTSSRQQQQQPPONNMLKJJIFFFFEDDDDDDDDDDDDDDDDDDDCCCCCCCCCCCCCCCGKLMMMNNNNNNOOOONNNNLLLLLLLKKJJIIIIHGGGGGGGGGGFFFFFEEEEEEEFFFFFFFFFFFFFFFFFFFFFFFFEEDDDDDDDDDDDDDDDEEEEEEEEDDDDDDDDDDEFFFFFGHIJRSSSTTUUVVVVUTSSRQQQQPPOONMLLKKJGGFFFFFFFEEEEEEEEEDDDDDDDDDDDDDDDDCCCCCCCCCCCKLLMMNNNNNOOOOOOONNNNMMLLLLLLLJJJIIIHGGGGGGGGGGGGFFFEEEEEEEEEEEEEEFFFFFFFFFFFFFFFFFFFFFFFEDDDDDDDDDDDDDDDEEEEEEEEEEEEDDDDDDDEFFFFFFGHRSSSTTUUVVVVVVUUSRQQQPPPONMMLLKJGGGGFFFFFFFFFEEEEEEEEEEDDDDDDDDDDDDDDDDCCCCCCCCCCCGHHKKLMMMNNNOOOOOPPPPOONNNMMLLLLLLLLJJJIIIHHGGGGGGGGEEEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFFFFFFFFFEEDDDDDDDDDDDDDEEEEEEEFFFFFFFDDDDDEEFFFFFFGGSSSTTUUVVWWWWVVUTSRRQQPPONMMLLKHGGGGGFFFFFFFFFFFFEEEEEEEDDDDDDDDDDDDDDDDDCCCCCCCCCCGHHIJKLLMNNOOOOPPPPPPOOONNMMMMLLLLLLLJJIIIHGGGGGGGGHFFEEEEEEEEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFFFFFFFFEEEDDDDDDDDDDEEEEEFFFFFFFFFFDDDDEEFFFFFFGGSSTTUUVVWWXXXWVVUTRPONMMLLKHHGGGGFFFFFFFFFFFFFFEEEEEEEDDDDDDDDDDDDDDDDDCCCCCCCCCHIIJJKKLMNNNOOOPPPPPPPPPPOOONNMMMMLLLLLLLJIIIIHGGGGGGGGGFFFEEEEEEEEEEEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFFFFFFFFFEEEEDDDDDDDEEEEEFFFFGGGGGFFFFDDDDDEEFFFFGGGSTTUUVVWWXXXXXWVUTSPONMMLKKIGGGGGGGGGFFFFFFFFFFFFFEEEEEEEEDDDDDDDDDDDDDDDDCCCCCCHHIJKKKLLMMNNOOPPPPPPPPPPPPPPOONMMMLLLLLLLKJIIIIHGGGGGGGGHHHGGGGGGFFFEEEEEEEEEEEEEEEEEEEEEEEEEFFFFFFFFFFFFGGGGGGFFFFEEEEEEEEEEEEFFFFGGGGGGGGFFFDDDDEEFFGGGGTUUVVWWXXYYXXTPOMLLKIHGGGGGGGGGGGFFFFFFFFFFEEEEEEEEEEEEDDDDDDDDDDDDDDDCCCCCHIIJKLLLMMNNOOPPPPPPPPPPPPPPOOONNMMMLLLLLLLLKJJIIIHHGGGGGGGGGGHHHHHHHHHHHGGGGGGFFFEEEEEEEEEEEEEEEEEEEFFFFFFFFFFGGGGGGGGFFFFFEEEEEEEEFFFFFGGHHHGGGGGGFFFDDDDGGGGGUUVVWXXXYYYYYMLLKIIHGGGGGGGGGGGGGGGFFFFFFFEEEEEEEEEEEEEEDDDDDDDDDDDDDCCCCHIJMNNOPPPPPPPPPPPPPPPPPOONNMMMMLLLLLLLLKJJIIIIHGGGGGGGGGGGGHHHHHHHHHHGGGGGFEEEEEEEEEEFFFFFFFFFGGGGGGGGGGGFFFFFEEEFFFFGGGGHHHHHGGGGGFFDDDGGGHWWXXXYYZMLKKGGGGGGGGGGGGGGGGFFFFFEEEEEEEEEEEEEEEDDDDDDDDDDDCCCCIJNNOPPPPPPPPPPPPPPPPOOONNMMMLLLLLLLKJIIIIGGGGGGGGGGHHHHHHHHHHGGGEEEEEEEFFFFFFFFGGGGGGGGGGGFFFFFFFFFGGGGHHHHHHHGGGGGGGDDDHHHWXXYYYZLLKKKKKGGGGGGGGGGGGGGGGGFFFFFEEEEEEEEEEEEEEEEEEEDDDDDDDDCCCCIOPPPPPPPPPPPPPPPPPOOONNMMMMLLLLLLKKJJIIHHGGGGGGGGGHHHHHHHHHHEEEEEEEFFFFFFGGGGGGGGGGGGGGGGGFFFFFGGGGHHHHHHHHHGGGGGDDDDDHHHHXYYZLKKKKKKGGGGGGGGGGGGGGGGGGGGFFFFEEEEEEEEEEEEEEEEEEEEEDDDDDDDCCCCCIOOPPPPPPPPPPPPPPPPPPOOONNNMMMMLLLLLLLKKJIIIIHGGGGGGGGGGHHHHEEEHEEEEEEEEFFFFFFFGGGGGGGHHHHHGGGGGGGGGGGGGHHHHHHHHHHHHGGGGGDDDDHHHHHHXYYKKKKKKKGGGGGGGGGGGGGGGGGGGGGFFFFEEEEEEEEEEEEEEEEEEEEEEDDDDDCCCCCBIOPPPPPPPPPPPPPPPPPPOOOONNNNMMMMMLLLKJIIIHGGGGGGGGGGGHEEEEEFFFFFFFGGGGGHHHHHHHHHGGGGGGGGGHHHHHHHHHHHHHGGGDDDDHHHHHYYYZZKKKKKGGGGGGGGHHHHHGGGGGGGFFFFEEEEEEEEEEEEEEEEEEEEEEEDDDDDCCCCIPPPPPPPPPPPPPPPPPOOONNNNNNNNMMMLJJIIIHHGGGGGGGGGGGGEEEEEEFFFFFFFFGGGGHHHHHHHHHHHHHHGGGGHHHHHHHHHIIHHHGGGDDHHHHHZZZZZZKKKKKKKKGGGGGGHHHHHHHHHGGGGGGFFFFFFEEEEEEEEEEEEEEEEEEEEEEEEDDDDDCCJPPPPPPPPPPPPPPPOOONNNNNNNNMLKJIIIHGGGGGGGGGGGGGGHEEEEFFFFFFFFFGGGGHHHHHHHHHHHHHHHHHHHHHHHHIIIIIIIIHHGGDHHHHH[[ZZZZKKKKKKKKGGGHHHHHHHIIIIHHHGGGGGFFFFFFFEEEEEEEEFFFFFEEEEEEEEEDDDDDCPPPPPPPPPPPPPPPOOOOONNNNNNNNNNKJIIIHHGGGGGGGGGGGHHFEEEEEEFFFFFFFFGGGGHHHHHHHHHHIIIIHHHHHHIIIIIJJJIIIIHHEDDDHHHHH[[[ZZKKKKKKKHHHHHHHIIIJJIIHHHGGGGFFFFFFFFEEEFFFFFFFFFFFEEEEEEEDDDDPPPPPPPPPPPPPOOOOONNNNNNNNNLLLKKKJJIIIHGGGGGGGGGGGHHHHHFFEEEEEEEEFFFFFFFFFGGGHHHHHHHHIIIIIIIIIIIIIIJJJJJJJIIFEDDDHHH\\[[[KKKKKKKKIIHHIIJJJJJJIIIHHGGGGFFFFFFFFFFFFFFFFFFFFFFFFFEEEEEEEDDOOPPPPPPPPPPOOOOONNNNNNNLLLLKKJJIHGGGGGGGHHHHHHFFFEEEEEFFFFFFFFGGGHHHHHHHIIIIIIIIIIJJJJJJJJKKJJJJIEDDDDDHHHH\\\\\IIIJJKKKKJJJIIIHHGGGFFFFFFFFFFFFFFFFFFFFFFFFFFFFEEEEEEEMMNNOOOOPPPPPPOOOOOOONNNMLLKKKKJIGGGGGGGGHHHHHFFFFFFEEEEEEEEEEEEFFFFFFFFGGHHHHHHHIIIIIIIIJJJJJJJKKKKKKKKKJJJDDDDDDHHHHH]\\KKKKKKKKKJJJIIIHGGGGFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEEEEELLLMNNOOOOPPPOOOOOOOOONLLLLLKKGGGGGGHHHHHFFFFEEEEEEEEEEEEEEEEEEFFFFFFGGGHHHHHHIIIIIJJJJJJJKKKLLLLLLKKKJJDDDDDDHHHG^]]]]MLLLLLKKKJJJIIHHGGGGFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEEEELLMMMNNNOOOOOOOOOOOOOOOOOLLLLLKKGGGGGGHHHHHEEEEEEFFFFFGGGHHHHHIIIIIIJJJJJJKKLLLLLLKKDDDDDDHHGG^^^^]NNNMMMLLKKJJJIIIHGGGGGFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEEEELMMMNNOOOOOOOOOOONNMMMMLLLLLKGGGGGGGGHHHHHHEEEEEFFFFFFGGGGGHHHIIIIIIJJJJJJJKKKLLLLKDDDDDDHHG__^^PPPONNMMLLKKJJJIIHHGGGGGFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEEEELLMMMNNOOOOOOOOOONNNNNNMMMMMMMMGGHHHHHEFFFFGGGGGHHHIIIIIJJJJJJKKKLLLDDDDDDDG```__QQQQPONNMMLLKKJJIIIHGGGGGGGFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEEELMMMNNOOOOOOONNNNNNNNNNMMNNNNOGGGHHHEEEEEEEFFFFGGGGGGHHIIIIIJJJJJJJKKKDDDDDDDaa```GGRRRRQPONNMMLLKJJIIIHHGGGGGGGGFFFFFFFFFFFFFFFFFFFFFFFFFFFEEEKLMMMNNOOOOOOONNNNNNNNNNNNNNNNNNOOPGGHHEEEEEFFFFFGGGGGGHHIIIIJJJJJJKKKKDDDDDDaaa`GRNNMLLKJJJIIIHGGGGGGGGGGFFFFFFFFFFFFFFFFFFFFFFFFFEEEKMMNNNNOOOOOOOOOONNNNNNNNNNNNNNNOOOPLLGGGGGGHHHEFFFFGGGGGGHHHIIIJJJJJKKKKLDDDDDDaGa`GGONNMLKKJJIIIHHGGGGGGGGGGGGFFFFFFFFFFFFFFFFFFFFFFFEEEEJKMMMNNNNOOOOOOOOOOOOOONNNNNNNNNNNNNNOOOPPQQRRRRLLGGGGGGHHHEFFFFFGGGGGGHHHIKKKKLLDDDDDDDGGONNMLLKKJIIIHHGGGGGGGGGGGGGGFFFFFFFFFFFFFFFFFFFFFEEEEJKMMNNNNNOOOOOOOOOOOOONNNNNNNNNNNOOOQQQRRRRRRLLGGGGGGGHHEEFFFGGGGGGGHHIJKKKKLLLMDDDDDDDGGPPONMMLLKJIIIHHHGGGGGGGGGGGGGGGFFFFFFFFFFFFFFFFFFFFEEEJKMMNNNNOOOOOOOOOOOOOOOOOOOOONNNNNNNNNNOOOORRRRRRRGGGGGGHEEFFFFFGGGGGGGHHKKLLLLLDDDDDDDGFFFQPOONMMLKJIIIIHHHHGGGGGGGGGGGGGGGFFFFFFFFFFFFFFFFFFEEEEIJMMNOOOOOOOOOOOOOOOOOOOONNNNNNNNNNOOOOOPQRRRRRRGGGGGGHEEEEEFFFFGGGGGGHKLLLLLMDDDDDDDGFFEFQQPOONMLLKIIIIHHHHHHGGGGGGGGGGGGGGFFFFFFFFFFFFFFFFFFEEEEELNNNOOOOOOOOOOOOOOOOOONNNNNNNNNNNOOOOOPPQQRRRRRRRQQGGGGGGHEEEEEFFFFFGGGGGGHLLLMMDDDDDDDFEQQPOONMLKJIIIIHHHHHHHGGGGGGGGGGGGGFFFFFFFFFFFFFFFFEEEEENNOOOOOOOOOOOOOOOOONNNNNNNNNNNOOOOOQQQRRRRRQQQQGGGGGEEEEEEFFFFFGGGGGHHLLMMMMDDDDDDDDEEQONMLKJJIIIIHHHHHHHHHGGGGGGGGGGGFFFFFFFFFFFFFFFFFEEEEMNOOOOOOOOOOOOOOOOOOOOONNNNNNNNNNNOOOOPPQQQQRRRQQQQQGGGGGEEEEFFFFGGGGHHMMMMMMNNDDDDDDDDDEEONJIIIIIIHHHHHHHHHHGGGGGGGGFFFFFFFFFFFFFFFFFEEEEELMNOOOOOOOOOOOOOOOOOOOOOONNNNNNNNNNNOOOOPPQQQQQQQQPQQQGGGGGEEEEEEEEEEEEEEFFFFGGGHHMMNNNNNEDDDDDDDDDDEEJJIIIIIIIHHHHHHHHHGGGGGGGFFFFFFFFFFFFFFFFFEEEELMNOOOOOOOOOOOOOOOOOOOONNNNNNNNNNNNNOOOPPQQQQQPPPQQGGGGGEEEEEEEEEEEFFFFFFGGHHINNNNNNOEEEEDDDDDDDEEKJJIIIIIIIIHHHHHHHHGGGGGGFFFFFFFFFFFFFFFFFFFFEEEHLMOOOOOOOOOOOOOOOOOOOONNNNNNNNNNNNNOOOOPPQQPPPPPOPPGGGGEEEEEEEEEEEEEEFFFGGHIIIKLMNNNNNOOOOEEEEDDDDDEEEKKJJIIIIIIIIHHHHHHHHGGGGGFFFFFFFFFFFFFFFFFFFEEDCLOOOOOOOOOOOOOOOOOONNNNNNNNNNNNNOOOOPPPPPPOOOOOOPPPGGGGEEEEEEEEEEEEEEEEEEFGGHIIIKLMNNNOOOOOOPFEEDDDDDDEEEEEKKJJJJIIIIIIIIHHHHHHHGGGGGFFFFFFFFFFFFFFFFFFFEEDOOOOOOOOOOOOOOOOONNNNNNNNNNNNNOOOOPPPOOOOOOOOOPPPPOGGGGEEEEEEEEEEEEEEEFGGHIIIJKLMNOOOOOPPPQFFFDDDDDDEEEEDJKKJJJJIIIIIIIIIHHHHHHGGGGFFFFFFFFFFFFFFFFFFFFEEDCOOOOOOOOOOOPPOOOOONNNNNNNNNNNNNNOOOOOOOOOOOOOOOOOOGGGEEEEEEEEEFFHIIJJLLOOPPPPQQQQRSFFDDDDDEEEDDKKKKJJJJIIIIIIIIIHHHHHGGGGGGGFFFFFFFFFFFFFFFFFFEEDOOOOOOOOOOOPPOOOOONNNNNNNNNNNNNOOOOOOOOOOOOOOOOOGGGGEEEEEEEEEEEFFGHIIJKLPPPQQQQRRTFFFFDDDDDDDDDDDDKLMLLKKJJJJIIIIIIIIIHHHHHGGGGGGGGFFFFFFFFFFFFFFFFEEOOOOOOOOOOOPPPOOOOOONNNNNNNNNNNNNNNNOOOOOOOOOOOOOEEEEEEEEEEEFFGHIIJQQQRRRSSTFFFFFDDDDDDDDDKKLMMLLKKKJJJIIIIIIIIIHHHHHGGGGGGGGGGFFFFFFFFFFFFFFEEOOOOOOOOOPPPPOOOOOOOOOONNNNNNNNNMMMMMMMMNOOOOOOOONLKEEEEEEEEEFFGHIIQRRRRSSSTFFFFDDDDDDDDDLLNNMMLLKKKJJJIIIIIIIIIHHHHGGGGGGGGGGGGGGFFFFFFFFFFFFEENNOOOOOOPPPOOOOOOOOOONNNNNNNMMMMMMMMMMMNNOOOOOONMEEEEEEEEEFIIRRRSSSTTFFFFEEDDDDDDDDLLNNNMMLLKKKJJJIIIIIIIIHHHHHGGGGGGGGGGGGGGGGGFFFFFFFFEEDDMMNNNOOOOPPPPPOOOOOOOOOOOOONNNNNNMMMMMMMMMMMNNOOOOONNNMMEEEEEEEEEEEFFFFGITTTFFFFFFEEEDDDDDDDKKKNNNMMLLKKKJJJIIIIIIIIHHHHGGGGGGGGGGGGGGGGGGGGGFFFFFEEDDMMMNNOOOOPPPPPPOOOOOOOOOOOOONNNNMMMMMMMMMMMMMNNOOONNNMMEEEEEEEEEEEFFFGHHSSTFFFFEEEEEEDDDDDJJJNNMMLLLKKKJJJIIIIIIIHHHGGGGGGGGGGGGGGGGGGGGGGGFFFEEEDDIKLLLLMMNOOPPPPPPPPPOOOOOOOOOOOOONNNMMMMMMMMMMMMMNNNNNNNMMMLEEEEEEEEEEEEFFGHSSFFFEEEEEDDDDDDDDEIJJNNMMMLLKKJJJIIIIIIIIHHGGGGGGGGGGGGGGGGGGGGGGGGGFFFEEEEDDDHJJKKKLLMNNOOPPPPPPPPPPOOOOOOOOOOOOONNNMMMMMMMMMMMMMNNNNNNMMMMLEEEEEEEEEEEEEEFFGGHSEEEEDDDDDDDFFHHOONNMMLLKKKJJJIIIIIIIHHHGGGGGGGGGGGGGGGGGGGGGGGGGFFEEEEEDDDHHIJJKKLLMMNOPPPPPPPPPPPPPPPPPPPOOOOOOOOOONNNMMMMMMMMMMMMMMNNNNMMMMLKEEEEEEEEEEEEEEEFFFGHHEDDCCCDDEFGPOONNMMMLKKKKJJIIIIIIIIHHHGGGGGGGGGGGGGGGGGGGGGGGGFFEEEEEEDDDCHIJKKKLMMNOPPPPPPPPPPPPPPPPPPPPPPOOOOOOOONNNNMMMMMMMMMMMMMMMMMMMLLKEEEEEEEEEEEEFFFGGHHCCCCCDDPONNMMMLLKKKJJJIIIIIIIHHHGGGGGGGGGGGGGGGGGGGGGGGGFFFEEEEEEDDCBGHHJJJKKLLNPPPPPPPPPPPPPPPPPPPPOOOOOOONNNNMMMMMMMMMMMMMMMMMLLKKEEEEEEEEEFFFFGGGHCCCCDQPOONNMMLLLKKKJJIIIIIIIIHHHGGGGGGGGGGGGGGGGGGGGGGGFFFEEEEEEEDDCFHHIJJJKLPPPPPPPPPPPPPPPPPPPPOOOOONNNNNNMMMMMMMMMMMMMMMLKKEEEEEEEDDFFFFFGGGHCIIIDSSQPOONNMMMLLKKKKJJIIIIIIIHHGGGGGGGGGGGGGGGGGGGGGGGFFFFEEEEEEDDCEFGGHHIIJJJKLPPPPPPPPPPPPPPPPOOOONNNNNNNMMMMMMMMMMMLLKEEEEEEDDDDEEFFFFFFFFHHIIJCIIJJISSRQPOONNMMMLLLKKKKJJIIIIIIIHHGGGGGGGGGGGGGGGGGGGGGGGFFFFEEEEEEDDCDFFGHHHHHIIJKPPPPPPPPPPPPPPPOOONNNNNNNNNNNMMMMLLEEEEEDDDDEEFFFFFFGHHIJKDIIJSRRPPONNNMMMLLLKKKKJJJIIIIIGGGGGGGGGGGGGGGGGGGGGGFFFFFFEEEEEEDCBEEFGGHHHHIIJJPPPPPPPPPPPPOONMMMLLFFEEEEEDDDCEEEEEFFFFFGGHHIJIIASRQPPONNNMMMLLLLKKKKJJJIIIIGGGGGGGGGGGGGGGGGGGGFFFFFFFEEEEEEDCCBFFGGGHHHHHHIJPPPPPPPPPPPPOONMLLKFFFEEEDDCCEEEEFFFFFGGGGHHIFCHAASSQPPOONNMMMLLLLLKKKKJJJIIIGGGGGGGGGGGGGGGFFFFFFFFFEEEEEEDDCBFFGGHHHHHHHIPPPPPPPPPPLKFFEDCCCEEFFFFEEDCCAAAPPPOONNNMMMLLLLLKKKKJJJJIGGGGGGGGGGGFFFFFFFFFFFEEEEEEEDCAFFGGGHHHHHHHPPPPPPPPPPNCCCDMFFFFEEDDAAAOOOONNMMMMLLLLLKKKKKJJJGGGGGGFFFFFFFFFFFFFEEEEEEECCBAFFGGHHHHHPPPPPPPPFCCCMMFFFEEDDCCAAAANNNNNMMMLLLLLLLKKKKKGGGFFFFFFFFFFFFFFFEEEEEEDCCGGGHHHHHHIPPPPPPPPPPCCCMMMFFEECCEAAWUNNNMMMMMLLLLLLKKKKKKFFFFFFFFFFFFFFFFEEEEEEDFGGGHHHHHHIIIMPPPPPPPPPCMMMMFFFEEDDEEAAAUSSPNNMMMMMLLLLLLKKKKKKHHHHFFFFFFFFFFFFFEEEEEDFFGGHHHHHHIIIPPPPPPQPPPPPPPCCEEMMMMEEEEc`RQPPPOONNMMMLLLLLLKKKKKKIIHHHHGGFFFFFFFFFFFEEEEFGGHHHHHIIIIPPPPPPPPQQQQPPPPPPPLCELLLMMEEEEEOLLLKKKKKIIIHHHGGGGGGGFFFFFFFFEEEEFGHHHHHIIIIIIIPPPQPPPPPEEEFLLLLEEEEEEEJIIIIIIHHHHGGGGGGGGGFFFFFFFFEEEEHHHIIIIIIIIJPPPPPPPPFCEEEFFFFFFFFGHIJJKKLLEEEEJJIIIIIIHHHHGGGGGGGGGGGFFFFFEEEEEHIIIIIIIIJJJPPPPPPPPPEECEEDEEEEFFFFFFFFGGHIJJKKJJJIIIIIIIHHHGGGGGGGGGGGFFFFFFFEEEEHHIIIIIIIJJJJPPPPPPPPPPECEDEEFFFFFFFGHHIIJEJJJJJIIIIIIIHHHGGGGGGGGGGGFFFFFFFFEEEEEHHIIIIIJJJJJJPPPPPPPPPPDEDDEFFFFFFFFFGGHEEEEE^]UUTTJJJJJJIIIIIIIHHHGGGGGGGGGGGFFFFFFFFFFFEEEEEHIIIIJJJJJPPPPPPPPPPPCFFFFFFFFFEEEF__^^]TTSSJJJJJJJJIIIIIIIIHHHGGGGGGGGGGFFFFFFFFFFFEEEEEEEBA@GHIIJJJJJJPPPPPPPPPPPPPPPECEEFFFFFGGGGGEEEEFTTSSPPOONMMLLLKKKKJJJJJJJJJJIIIIIIIIIIIHHHHGGGGGGGGGFFFFFFFFFFFFEEEECAHHIIJJJJPPPPPPPOOOPPPPPPPPPCEEEEEEFFFFGGGHHGGFDEFFTTSQPPOOMMMLLLKKKKJJJJJJJIIIIIIIIIIIIIHHHHGGGGGGGGGFFFFFFFFFFFFFFEEEEEBBA@?GHHIJJJJPPPPPPPPOOOOPPPOOPPPPCCCEEEEEEFFFFGHHDDFTTRQPPOMMLLLLKKKJJJJJJIIIIIIIIIIIIIHHHHHHGGGGGGGGFFFFFFFFFFFFFFFFEEEECBBA@EFGHIJJKPPPPPPPPPPPOOOOOOOOOOOPPCEEEEEEFFGGHHHFFTTRQLLLLLKKKJJJJJJIIIIIIIIIIIHHHHHHHHHGGGGGGGGFFFFFFFFFFFFFFFFFEEECCBAA@EFHIJJJKKOOPPPPPPPPOOOOOOOONNOOOOPCEEEEFFFGGHHHFFTTSRRLLKKKKKJJJJJJIIIIIIIIIIHHHHHHHHHHGGGGGGGGFFFFFFFFFFFFFFFFFFEEEECCBBFGIIJJJKKLMMNOOOPPPPPPOOOOONNNNNNNOOCEEEEFFGGGHFEDDpppSRKKKKKKJJJJJJIIIIIIIIIHHHHHHHHHHHGGGGGGGGFFFFFFFFFFFFFFFFFFFEEEDCCBBAA@FFHIJJKKKLLMNNOOOOOOOOOOOOONNNNNNNNNNEEFGFEDDppppSSKKKKKJJJJJJJIIIIIIIIIHHHHHHHHHHHHGGGGGGGGFFFFFFFFFFFFFFFFFFFFEEEDCCCBAFGHIJJKKKKLLMMMNNOOOOOOOOOOOOOONNNMMMNNNNEEEFFGFDDDDpppppUTTSKKKKJJJJJJJJJIIIIIIIIIHHHHHHHHHHHHGGGGGGGFFFFFFFFFFFFFFFFFFFFFEEEEEEEEEDDDCBGGHIJJKKKLLLMMMNNNOOOOOOOOOOONMMMMMMMNCCEEFGFDDDDDDEppppppVTTTKKKJJJJJJJJJIIIIIIIIIHHHHHHHHHHHHHGGGGGGGFFFFFFFFFFFFFFFFFFFFEEEEEEEEEEEEDDDCGGHIJKKKKLLMMMNNNNOOOOOOOOONNMMMMMMMMMCEEEEFGEEDDDDEEAApppppppAVVUUTKKKJJJJJJJIIIIIIIIIIIHHHHHHHHHHHHHGGGGGGGFFFFFFFFFFFFFFFFFFFEEEEEEEEEEEEEDDC?GHHJJKKKKLLLLMMNNNNNNNOOOOOONNNNNMMMMMMMMEEEEEFFFGEDDDDDEAAppppppppAYWVVUUUKKJJJJJJIIIIIIIIIIIIIHHHHHHHHHHHHHGGGGGGFFFFFFFFFFFFFFFFFFFFEEEEEEEEEEEEEEEEGHIJJKKKLLLLLMMMNNNNNNNNNNNNNNNNMMMMMMLEEEEFFGGEEEDDDDDEAAppppppppYYYYXXXWWVVVKKJJJJJIIIIIIIIIIIIIHHHHHHHHHHHHHHHGGGGGFFFFFFFFFFFFFFFFFFFEEEEEEEEEEEEEGHIJJJKKLLLLMMMNNNNNNNNNNNNNNNMMMMMMLLFEEEEEEFEEEDDDDDDDDDpppppppYYYYXXXXVVVKKJJJJJIIIIIIIIIIIIIHHHHHHHHHHHHHHHGGGGGFFFFFFFFFFFFFFFFFFFEEEEEEEEEEEEEEEEEGHIIJJKKLLLLLMMMMNNNNNNNMMMMMMLLKFFEEEFEDDDDDDDDDDppppppppYYYXXXXXXXXWWWKKJJJJIIIIIIIIIIIIIIHHHHHHHHHHHHHHHHGGGGFFFFFFFFFFFFFFFFFFFEEEEEEEEEEEEEEEEGHIIJJKKLLLLMMMMMMNNNNNMMMMLLLLKKJEEEEFEDDDDDDDDppppppppYXXXXXXXXXXWWWKKKJJJIIIIIIIIIIIIIIHHHHHHHHHHHHHHHHGGGGGFFFFFFFFFFFFFFFFFFEEEEEEEEEEEEEEEGHIJJKKLLLLMMMMMMNMMMLLLLLLKJJFEFDDDDDDDDqpppppppYYXXXXXXXXXWWXXLKKKJJJIIIIIIIIIIIIIHHHHHHHHHHHHHHHHHGGGGGFFFFFFFFFFFFFFFFFEEEEEEEEEEEEEEEHIJJKKLLLMMMMMLLKKKKJJIIEDDDDqqqqpppppYXXXXXXXXXXXXXXLLKKKJJJIIIIIIIIIIIIIHHHHHHHHHHHHHHHHHHGGGGGFFFFFFFFFFFFFFFFEEEEEEEEEEEHHIJJKKKLLLMMMMMLLLKKKKJJIIHEEJqqqqqqpppXXXXXXXXXXXXXXXLLLKKJJJIIIIIIIIIIIIIHHHHHHHHHHHHHHHHHHGGGGGFFFFFFFFFFFFFFFFEEEEEEEEEEHIJJKKLLMMMMMMMLLLKJJJJJIIHHHGGGFFKDqqqqqqXXXXXXXXXXXXXXXXLLLKKKJJIIIIIIIIIIIIIHHHHHHHHHHHHHHHHHHGGGGGGFFFFFFFFFFFFFFFEEEEEEEEEEIJJJKKKLLMMMMMMLLKKKJJJJJIIHHGGGJDDDqqqqqXXXXXXXXXXXXXXXXLLLKKKJJIIIIIIIIIIIIIHHHHHHHHHHHHHHHHHHHGGGGGFFFFFFFFFFFFFFFEEEEEEEEEEIJKKLLMMMMMMMMMKKKKJJJIIHHGGFFEFJDDDDDqqqrXXXXXXXXXXXXYYYLLLKKKJJJIIIIIIIIIIIIIHHHHHHHHHHHHHHHHHHGGGGGFFFFFFFFFFFFFFFFEEEEEEEEEHJJKLLLMMMMMMMNNNNMKJJIIHHGFFFFEEFDDDDqqrXXXXXXXXXXXXYYYLLLKKKJJIIIIIIIIIIIIIHHHHHHHHHHHHHHHHHHGGGGGFFFFFFFFFFFFFFFFEEEEEEEEGIJJKKLMMMMMMMMNNNNNNNMMJJJIIIHHGGFFFFFEEEDDDrrXXXXXXXXXXXXYYYYLLLKKKJJJIIIIIIIIIIIIIHHHHHHHHHHHHHHHHHGGGGGGFFFFFFFFFFFFFFFFEEEEEEEEGIJJKKLMMMMMNNNNNNNNMMLLKKJJJIIHHGGFFFFFFFFFFEEEGGGEDDDrrrXXXXXXXXXXXYYYYLLLLKKKJJIIIIIIIIIIIIIHHHHHHHHHHHHHHHHHGGGGGGFFFFFFFFFFFFFFFFFEEEEEEEEHIJKKKLLMMMNNNNNNNNNNMMMLKKJJJIHHHGGFFFFFFFFFFECEEEEEEEEEEFFFDDrXXXXXXXXXXXYYYLLLKKKJJJIIIIIIIIIIIIIHHHHHHHHHHHHHHHHHGGGGGFFFFFFFFFFFFFFFFFEEEEEEEFIJKKLLLMMNNNNNNNNNNNMMLLKJJIHHHGGFFFFFFFFFFEEDCCEEEEEEEEFFFEEEEDDssXXXXXXXXXXXYYYLLLKKKJJJJJIIIIIIIIIIIIHHHHHHHHHHHHHHHHGGGGGFFFFFFFFFFFFFFFFFFEEEEEEFHIJKKLLMMMNNNNNNONNNNNMMMLKKKJJJIIHHGGGFFFFFFFFFFFEDDCCCCEEEEEEEsssYYXXXXXXXXXXXYYYYMLLLKKKKJJJJJJJIIIIIIIIIIIHHHHHHHHHHHHHHGGGGGFFFFFFFFFFFFFFFFFFEEEEEEEEEEE=DFGIJKKLLMMNNNNOOOONNNNMMLLKJJJJIIIHHHGGGGFFFFFFFFFFFEEDDDCCCCCCIsssYYYYXXXXXXXXXXXYYMLLLLKKKKJJJJJJJJJIIIIIIIIIIHHHHHHHHHHHHGGGGGFFFFFFFFFFFFFFFFFFFEEEEEEEEEEEEDCAFGJJKLLMNNNNOOOOOOOONNMMMLKKJJJIIIIIHGGGGFFFFFFFFFFFFFFEEDDDCCCCCCCJJIttZYYYYXXXXXXXXXXXXMMLLLKKKKKKJJJJJJJJJJIIIIIIIIIHHHHHHHHHHHGGGGFFFFFFFFFFFFFFFFFFEEEEEEEEEEDDDCCCBGHJJHLMNNNNOOOOOOPPPOOONNMMLLKJIIIHHGGGGGFFFFFFFFFFFFFFFEEEDDCCCCCCCCJJJJtttttZZZYYYXXXXXXXXXXXXMMLLLLKKKKKKKKKKKJJJJJIIIIIIIIIHHHHHHHHHHGGGGFFFFFFFFFFFFFFFFFEEEEEEEEEEEDDDDCCCCEHIJKHLMNNNOOOOOPPPPPPPOONNNMLKJIIIHHHGGFFFFFFFFFFFFFFFEEEDDDCCCCCCCJKKJttttZZZYYXXXXXXXXXXXXMLLLLLLKKKKKKKKKKKJJJJIIIIIIIIIHHHHHHHHHGGGGFFFFFFFFFFFFFFFEEEEEDDDDCCCCEFHIJLLMNNOOOOOPPQQQPPPOOONNMLKJIIHHHHGFFFFFFFFFFFFFFFFEEDDDCCCCCCCCKKuuttZZZZZXXXXXXXXXXXXXNMMLLLLLLLLLLLLKKKKKKJJJIIIIIIIIIHHHHHHHHGGGFFFFFFFFFFFFFFFEEEEEDDDDCCCBDFGKLMNNOOOOPQQQQQQPPOOOOMMKJIIHHHHFFFFFFFFFFFFFFFFFEEEDDDCCCCCCEEEFuttt[ZZZXXXXXXXXXXXXXNNNMMMLLLLLLLLLLLLKKKKKJJIIIIIIIIIHHHHHHHHGGGFFFFFFFFFFFFFEEEEEEEDDDDCCCHIJKLMMNOOOQQQPPOONNLKIHHHHHGFFFFFFFFFFFFFFFFFFFEEEDDDDCCCCCCCCEEEEFFFtt[[[YYXXXXXXXXXXXXNNMMMMLLLLLLLLLLLLLKKKKJJIIIIIIIIIHHHHHHHGGGFFFFFFFFFFFFEEEEEEEDDDDDCCEFGHIJJLLMNOOOQIHHHHGGFFFFFFFFFFFFFFFFFFFFFFEEEEDDDDDCCCCCEFFFFut[[[[YYYXXXXXXXXXXXXNNNMMMMMLLLLLLLLLLLLKKKJJJIIIIIIIIHHHHHHHGGGFFFFFFFFFFFEEEEEEDDDDDCCCDFGHIIJKLMNNOOHHGGGFFFFFFFFFFFFFFFFFFFFFFFFFFFFEEEEEDDDDDDCCCCEFFFFFFwwuu[[[YYYYYXXXXXXXXXXXOONNNMMMMMMLLLLLLLLLLLKKKJJIIIIIIIIHHHHHHHGGGFFFFFFFFFFEEEEEDDDDDDDCCCFGHHIJKLMNNNOGGGGFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEEEEEEDDDDDCCCEFFFFFGGxxuu[[[[ZYYYYYXXXXXXXXXWPOOONNNNMMMMMLLLLLLLLLLKKKJJJIIIIIIIHHHHHHGGGFFFFFFFFFFFEEEEDDDDDDDDCCCCCFGGHIIJLMMNNOOGGFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEEEEDDFFGGxvu[[ZZZZYYYYYXXXPPPOOONNNNNMMMLLLLLLLLLLLKKKJJIIIIIIIHHHHHHGGGFFFFFFFFFFEEEEDDDDDDDDDDDDDCCCFGHHIJLLMMNGFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEEEEEDDDEEFFFGGv[[ZZZZYYPPPOOOONNNNNMMMLLLLLLLLLLKKKJJJIIIIIHHHHHHHGGGFFFFFFFFFFFFEEEDDDDDDDDDDDDDDBGHIIKLMMGFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEEEEDDDEEEEEEFFGGGww[[[[ZZZPPPPOOOOONNNNMMMLLLLLLLLLLKKKJJJIIIIIHHHHHHHGGFFFFFFFFFFFFEEEEDDDDDDDDDDDDDAEFGGIKLLLMMGGFFFFFFFFFFFFFFFFFFFFFFFFFEEEEEEEDEEEGGGGGGGxww[[[[[ZZPPPPOOOOOONNNNMMMLLLLLLLLLLKKKJJJIIIIHHHHHHHGGGFFFFFFFFFFFFEEEDDDDDDDDDDDDDDDAAGHIJLLLLGFFFFFFFFFFFFFFFFFFFFFFFEEEEEEEGGGGGGxxx[[[[[[PPPOOOOOONNNNNMMMLLLLLLLLLLKKKJJJIIIIHHHHHHHGGGFFFFFFFFFFEEEEDDDDDDDDDDDDDDD@>GHIKKLKGGFFFFFFFFFFFFFFFFFFFEEEEEEEEHGGGGxZ[[[OOOOOOONNNNNNMMMMLLLLLLLLLKKKJJJJIIIIHHHHHHHGGFFFFFFFFFFFEEEEDDDDDDDDDDDDDDDHJKKKKKGGFFFFFFFFFFFFFFFFEEEEEEEEIIIIIHHHZOOONNNNNNNNNNNNNMMMMLLLLLLLLLLKKKJJJJIIIHHHHHHHHGGFFFFFFFFFEEEEEEDDDDDDDDDDDDDDDJJJKKKGGGFFFFFFFFFFFFFFFFFEEEEEHIIIHHHHZNNNNNNNNNNNNNMMMMMMLLLLLLLLLLKKKJJJJJIIIHHHHHHHGGFFFFFFFFEEEEEEEEDDDDDDDDDDDDDDJJJJJJJGGGGFFFFFFFFFFFFFFFFFEEHHHHZMMMMMMMMMMMMMMMMMLLLLLLLLLLLLKKKJJJJIIIIHHHHHHHGGFFFFFFFFEEEEEEEEDDDDDDDDDDDDDDDDIIJJJJJJGGGGGGGGFFFFFFFFFFFFFFFFFFEEHHHHZZLLMMMMLLLLLLLLLLLLLLLLLLLLLKKKJJJJIIIIHHHHHHHGGFFFFFFFFFEEEEEEEEDDDDDDDDDDDCCCDDDHHIIIJJJIGGGGGGGGFFFFFFFFFFFFFFFFFFFEEHZLLLLLLLLLLLLLLLLLLLLLLLLLLKKKKJJJJIIIIHHHHHHHGGFFFFFFFEEEEEEEEEDDDDDDDDDDCCCCCCCCBHHHIIIIIIIHGGGGGGGGGGGGGFFFFFGGGGGGFFFFFEGZKLLLLLLKKKKKLLLLLLLLLLLLKKKKKJJJJJIIIHHHHHHHHGFFFFFFEEEEEEEEEEEDDDDDDDDCCCCCCCCCCBBBBBBBGGHHHIIIIIHHHGGGGGGGGGGGGFFFFGGGGGGGGGGFFFEEKKKKKKKKKKKKKKKKLLLLLLKKKKKKJJJJJIIIIHHHHHHHGGFFFFFEEEEEEEEEEEDDDDDDDDCCCCCCCCBBBBBBBBBBAFGGGHHIIIIIHHHGGGGGGGGGGGGGFFGGGGGGGGGGGGGFEEEEEDDGFFFFFFKKKKJJJJJJJKKKKKKLLKKKKKKJJJJJJIIIIHHHHHHHHGFFFFFEEEEEEEEEEEEDDDDDDDCCCCCCCBBBBBBBBBBBAAEFFGGHIIIIHHHGGGGGGGGGGGGGGGFFFGGGGGGGGGGGGGGGFFEEEEDeFFFFFFFFFFFFJJJIIIJJJJJKKKKKKKKKKJJJJJJIIIIIIHHHHHHHGGFFFFFEEEEEEEEEEEEDDDDDDDCCCCCCBBBBBBBBBBBAAA@?>EFFGHHIIHHHGGGGGGGGGGGGGGFFFGGGGGGGGGGGGGGGGGFEEEEFFFdFFFFFFFFFFFJIIIIIIIJJJJJJKKKKKJJJJJJIIIIIIIHHHHHHHGGFFFFFEEEEEEEEEEEEDDDDDDDCCCCCBBBBBBBBBAAAA@@?>DEFFGGHIIHHHGGGGGGGGGGGGGGGGFFFFFFFFGGGGEEEEEEFFFFFFDddFFFFFFFFFFIIIIIIIIIIIJJJJJJJJJJJIIIIIIIIHHHHHHGGGFFFFFFEEEEEEEEEEDDDDDDDCCCCCBBBBBBBBBAAAA@@?>=DEFFGHHIHHHGGGGGGGGGGGGGGGGGFFFFFFFFFFGGGGGEEEEEEFFFFFFFEEedFFFFFFFIIIIIIIIIIIJJJJJJJIIIIIIIIIHHHHHGGGGFFFFFFEEEEEEEEEEDDDDDDDDCCCCCBBBBBBBBBAAA@@??=EFGGHHHGGGGGGGGGGGGFFFFFFFFGGGGGEEEEEEEFFFFFFFEEddcFFFFFFHHIIIIIIIIIIIIIIIIIIIIIHHHHGGGGGFFFFFFFEEEEEEEEEEDDDDDDDDCCCCCBBBBBBBBBAAA@@??>DEFFFGHHHHGGGGGGGGGGGFFFFFGGGGGGEEEEEEEFFFFFEEBddcccbFFFFFHHHHHIIIIIIIIIIIIIIIHHHHHGGGGGFFFFFFFFEEEEEEEEEDDDDDDDDDCCCCCCBBBBBBBBBAA@@??>DFFFGGHGGGGGGGGGGFFFGGGGGGGGFEEEEEEEEFFFFFFBcccbbbbZZZFFFFFFHHHHHHHHHIIIIIIIIIHHHHHHGGGGGFFFFFFFFEEEEEEEDDDDDDDDDDDDCCCCCCBBBBBBBBBAA@@??>DEFFGGGGGGGGGGGGGGGGGGGGGGGGGFEEEEEEEECcccbbbbbbbZZFFFFFFHHHHHHHHHHHHHHHHHHHGGGGGGFFFFFFFFEEEEEEDDDDDDDDDDDDDDCCCCCCCBBBBBBBBBA@@??>>CDEFFGGGGGGGGGGGGGGGGGGGGGGGGFEEEEEEECDDCPbbbbbbbbbbbbbZZZZQFFFFFFFFFHHHHHHHHHHHHHHHHGGGGGGGGFFFFFFFFEEEEEEDDDDDDDDDDDDDDCCCCCBBBBBBBBBAA@???>CDEFGGGGGGGGGGGGGGGGGGGGGGFFFFFFFFEEEEDCCCCQQQaaaabbbbbbbbbbbbaaaaZZZZQFFFFFFFFGHHHHHHHGGGGGGGGGFFFFFFFFEEEEEEEEDDDDDDDDDDDDDDCCCCCCBBBBBBBBBA@@??>BFGGGGGGGGGGGGGGGGGGGGGFFFFFFFFFFFFFEEDCCCCCCQRR``aabbbbbbb`\\\[[ZZZQFFFFFFFGGGGGGFFFFFFFFFFEEEEEEEEDDDDDDDDDDDDCCCCCCCCCBBBBBBBBBA@@@??>CDEGGGGGGGGGGGGGGGGGGGGGGGFFFFFFFFFFECCCCCCCDDPQQQRSS\\]]^^___``_^^]]\\[ZZYYQFFFFFFGGGFFFFFFFFFFEEEEDDDDDDDDDDCCCCCCCCCCBBBBBBBBBA@@??>>GGGGGGGGGGGGGGGGGGGGFFFFFFFFFEECCDPQQQRRSYYZ[\]\\\ZZYYYYFFFFFFEEEDDDDDDDDCCCCCCCCCCCBBBBBBBBBAA??=GGGGGGGGGGGGGFFEEEEEECCCDTUVVWWXXXYYZZbbbbbaaZZYYYYFFFFFEEDDDDDDCCCCCCCCCCCCCBBBBBBBBBA@@?>>=GGGGGGGGGEEEEEECCCCCCCCDPUVVVWXXaabbbbbbbbbbbbb``ZZZYYYYQQQFFFFFFEEDDDDDCCCCCCCCCCCCCCCCCBBBBBAA@@??>GGGGGGGFFEEEEEEECCCCCCDEEE]]]^^__````abbbbbbbb_[[ZZZYYYYFFFFFFFEEEDDDDCCCCCCCCCCCCCCCBBBBB@@?GGGGGGGGFFFEEEEEEEECCCCCCCCDDEEOQRS[[\]]`aaabbbbbbbb][[[ZZZYYYFGDDDDCCCCCCCCCCCCBBBBBBBBAA@@?>>FFGGGGGGGGFFEEEEECCCCEEENRSSTTZZZ[[]bbbb]\\[[[[[ZZZZYYYYDDDDCCCCCCCBBBBBBBA@@??>=FGGGGGGGGGGGGGFFEEEEEEEEEECCCCCCCCDEEENTTUYYYZZ\\[[[ZZZZZYYYYXHGGDDDDDCCCCCBBBBBBBBBA@@???>>FGGGGGGGGGGGGGGGGGGGGGGGGGFFFFFFEEEEEEEEEEECCDENOVWWWXXX[[ZZZZZYYYYXXXXJJIIHHDDDDDDDDDCCCCCCCBBBBBBBBBB@@@@???>FGGGGGGGGGGGGGGGGGGGGGGGGGGGGFFFFFFFEEEEEEEEEEEEEECEEENWWWWXZZZYYYYYXXXXWWUFKJJIIIHHHGGFFDDDDDDCCCCCCCCCBBBBBBBBBBBAA@@@@@@???GGGGGGGGGGGGGGGGGGGGGGGGGGGGGFFFFFFEEEEEEEECCEENWZZYYYXXXXXXWWVUTTRPPOONMLKJJIHHHGFEEEDCCCCCCBBABBBBBBBBAAA@@@@@???GGGGGGGGGGGGGGGGGGGGGGGGGGFFFFFFFFFFFFFEEEEEEEEEFFCCCCEEMMWYYYXXXXXXWWWVUTTRRPPOKJJJIIIHHGGGGFEDCCCCBBBBBABBBBBBAAAA@@@@@@?GGGGGGGGGGGGGGGGGGGGGGGGGFFFFFFFFFFFFEEEEEEEEFFECCEYYXXXXXXWWWWVVUTTTTSRRRQPPPLLKKJJJIIIIHHGGGGFFEEEDDCCCBBBBBABBAAAAAAA@@@@@=GGGGGGGGGGGGGGFFFFFFFFFFFFFFFEEEEEEEEECDEEEKLMYXXXWWWWWWWVVVUUTTTTSSRRRQPPPLLKKJJIIIHHGGGFFFEDCCBBBBAAAA@@@@GGGGGGGGGGGGGFFFFFFFFFFFFFFFFEEEEEEEEEEEEEMYYXXWWWWWWWVVVVUUUTSRRQQPPPOOONLLLLKJJJIIHHGGGGFFEEDDBA@@@@@@>GGGGGGGGGGGFFFFFFFFFFFFFFFFFEEEEDEEFXXWWWWWWVVVVVVVUUUUSRRQQQPPOOONMLLLLKKJIIHHGGGGFEEDDDCB??GGGGGGGGGFFFFFFFFFFFFFFFDDDDDDDDDEFKMWWWVVVVVVVVVVUUUUTTRQQQPPOOOMLKJJJIHHHGGEDDCCBA??>GGGGGGGGGFFFFFFFFFFFFCDDDDDDDDDEEFFLMWWVVVVVVVVVVVVVUUUUTTSRRQQOOMMLLLKKJJJIHHGGEEEEDDDCBBB??>GGGGGGGGGGGGGGGGGGGGFFFFFFFFFFCCCEDDDDDDEEEFFKKLMWWVVUUUUVVVVVVVUUUUUTTTSSRQQQMLLKKJJIHHFFEEEDDDDBBA@AAAAAA@@@@??>>GGGGGGGGGGGGGGGGGFFFFFFFFFFFFCCCEFEDDDDDDEEEEFHKLVUUUUUUUUVVVUUUTTTTTSSSRRMMMLLKKJJIIHFEEEEDDDDCBBBBAAA@?AAAAAAAAA@@@@@???GGGGGGGGGGGGGGFFFFFFFFFFECEDDDDEEEEEFFHIJKLLVVUUUTTUUUUUUUUTTTTSSSSRRRNMMMLKKJJJHFFEEEEEDDDCCBBBAAAAA@AAAAAAAAAAAAAAAAA@?@@@@@@?>>GGGGGGGGGGGGGGFFFFFFFFFFEDDFFEDDEEEEEEEFFGIJKLUUUTTTTTTTUUUTTTTTTSSSSSRRRMMLLKKJHEEEEEEEDDDDBBBBAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@@@@@?=GGGGGGGGGGFFFFFFFFFEEDDEEEEEEEEFFFGHJLUTTTTSTTTTTTTTTTSSSSSRRRRRMMLLKJJJGFEEEEEEEDDDDCCBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@@@@@@@??>>GGGGGGGFFFFFFFFFFFFEDDEEEEEEEFFFGGFJUTTTSSSSSSSSTTTSSSSSSRSRRRRRRMLLKJJIGEEEEEEEEDDCBBBAAAAAAAAAAAAAAAAAAAAAAAAAAABBA@@@@@??>>=GGGGGGGGGFFFFFFFFFFFDDEEEEEEFFFGFIHTTSSSSSSSSSSSSSSRRRRRRRRRRRQMLKJJIEEEEEEEEDDDCCCBAAAAAAAAAAAAAAAAAAAAAAABA@@@@@@??>>>>GGGGGGGGGGGGFFFFFFFFFFFEDEEEEEEFFFGGHKJIHHSSSSSSSSSSSSSRQQQRRQQMLKJIEEEEEEEEECCBBBAAAAAAAAAAAAAABBBBAAAACCA@@@??>>=GGGGGGGGGGGGFFFFFFFFFFFIEEEEEEFFFGGFFKKKKJIIHHHHHSRRRSSSRRQQQQPLLKJJJIFEEEEEEEEECCBBBAAAAAAAAAABBBBBBABBCCCCCAA@??>>=GGGGGGGGGGFFFFFFFFFFFEEEEEEEFFFIIJJJJJJJIIHHHHGHIRRRRRRRRRRQPPPNLKJIIFEEEEEEEBBBAAAAAAAAAAAAAABBBBBBBBBBBBCC?>>==FGGGGGGGGGGFFFFFFFFFFEEEEEEFHIIIIIIIIIIHHHHHHIILRQQQRRRRRRQQQQPPPPOOMLJJIIIGEEEEEDBBBAAAAAAAAABBBBBBBBBBBBBCCCBB>>==GGGGGGGGGGGGGGFFFFFFFFFFEEEHHHHIIIIIIHHHHHHGHHHIIKLLQQQQQQQQQQRRQPPPOOONMLJIIIFFEEBBAAAAAAAABBBBBBBBBBBBBBBCCCC==GGGGGGGGGGFFFFFFFFFFFFEEFFGGGHHHHHIIHHHHHHHGGGHHHIIIJKKLLQPPPPPQQQQQQQQOONLKJIIFFEEEEBAAAABBBBBBBBBBBBBBBBBCD==GGGGGGGFFFFFFFFFFFFFFFFFFFFFFFFEEEFGGGGHHHHHHHHHHHHHHHHHHHIIIJJLLPPPPPPPPPPPPPQQOONNNMKJJIGFFEEEBBABBBBBBBBBBBBCCCCDD=GGGGGGFFFFFFFFFFFFFFFFFFFFFFFFFFFEEGGGGGGHHHHHHHHHGHHHHHHHIIIIJJKKOOOOOOOPPPPPPPNNNNNNMLKIHGFFEEEDDBBBBBBBBBBBCCCCCCCDDEEGGGGGGGGGGFFFFFFFFFFFFFFFFFFFFFFFFEEEGGGGGHHHHHHHHHHHGHHHHHHHIIIIJJNNOOOOOOOOOOONNMMMJJHGGFFEEDBAABBBBBBBBBBBBBCCCCCCCCDDDFFGGGGGGFFFFFFFFFFFFFFFFFFFFFFFFFEEHHHHHHHHHGGGHHHHHHHIIIIJMNNNNNNOOOOOMMMMMLKJIHGFFFEEDDAAABBBBBBBCCCCDDDDEFFFFFGGGGGGGFFFFFFFFFFFFFFFFFFFFFFFFGFFFFEEHHHHHHHHHHHHHIIIJJJJJKLMMMMNNNNNNMLMMMMMMMMLKJIGFFFEEEDDDDDCCCCCCCDDDEFFFFFF<GGGGFFFFFFFFFFFFFFFFFFFFFFFGFFFEEEEEEHHHHHHHHHIIIIJJJIJJJKLMMMMMMLMMMMMMMMMLLJIHGFFFEEEDDDDCCCCBDDEEFFFFFGGGGGG;;GGGGGGFFFFFFFFFFFFFFFFFFFFFEEEEEEEEEEEEHHHHHHHHIIIJJIIJJJKLLMMMMLKMMMMMMMLLJGFFEEEDDDDCCCDEEFFFFFGGG<;OGGGGGGGGGGFFFFFFFFFFFFFFFFFFFFFFEEEEEEEEEEEEEEEEEHHHHHHHIIIIJIIIIIJJJKLLKLLLMMMMMLLJGFFEEDDDDCCCBBBEFFFGHHHG<<;OGGGGGGGGGGGGGGFFFFFFFFFFFFFFFFFFFFEEEEEEEEEEEEEEEEEEEHHHHHHIIJJJJIIIIIIIIIJJKKKKKLLLLLLLLLLJJIGFEEEEDDDDDCCCBBBEEEFFFGHHHHHHHH;;;GGGGGGGGGGGGGGGGGGFFFFFFFFFFFFFFFFEEEEEEEEEEEEEEEEEEEEEEEEEFFFFFGHHHHIIIIJJJJIIIIHHIIIIIJJKKKKKKKLLLLLLJJJIIHFFEEEDDDCCBBBBBBBBBFFFFHHHHHHHHH;;FGGGGGGGGGGGGGGGGFFFFFFFFFFFFFFFFEEEEEEEEEEEEEEEEEEEEEEEFFFFFFFFGGGGGHHHHHIIIJJJJJIIIHHHHHIIIIJJKKKKKLLKKJJIHEEEEEDDDCCBBBBBBBABBBCCFFFGHHHHHH<;;GGGGGGGGGGGGGGGGGGFFFFFFFFFFFFFEEEEEEEEFFFEEEEEEEEEEEEFFFFFFFFGGGGGHHHHHHGFHHIIJJJIIIHHHHHHHHHIIJJKKKKKFFJKKKKKKKJJJJIIHHGFEEEEBBBBBBBBBABBBBBCCFFFHHIIIIIIH<<;;GGGGGGGGGGGGGGGGGGGGFFFFFFFFFFFFEEEEEEFFFFFFFFEEEEEEEEEGGFGGGGGGHHHHHHHHHHHHGGGHHHHHIIIJJIIIIHHHHHHHHHHIIJKKKKKFFKKKKKKKKKJJJIHGEEEEBBBBBBBBBBABBBBBCCCCDDFGGHIIIHFE<<;;FFGGGGGGGGGGGGGGGGGGGGGGGGGGFFFFFFFFFFFFFEEEFFFFFFFFEEEEEEGGGGGHHHHHHHHHHHHHHHHGGHHHHHHHHHIIIIIIIHHHGGHHHHHHIIJJJKKKJFFKKKKKJJJIHHHGGFEEEDBBBBBBBBAAABBCCDGIIIHGE<<FFFGGGGGGGGGGGGGGGGGGGGGGGGGGFFFFFFFFFFFEFFFFFFFEEEEEEEEGGHHHGHHHHHHHHHHHHHHHHHGHHHHHHHHIIHHHGGGGGGHHHHHIIJJJJJJFIKKKJJJJIHHGGGFFCCBBBBBBABBBCCCDDGF;FFFFFGGGGGGGGGGGGGGGGGGGGGGGFFFFFFFFFFFFFEEFFGGGGFFFEEEEEFFHHHHHHHHHHHHHHHHHHHHHHHHHGGHHHHHHHIIHHGGGGGGGHHHIIIJJJJIIJJJIIHHGGFDCBBBBBBAAAAABBCCDDEHEFFFFGGGGGGGGGGGGGGGGGGGGGGGFFFFFFFFFFEEEGGGGGGGFEEEEEHIIIIIHHHHHHHHHHHHHHHHHHGHHHHHHHHIHHGGGFFGHHHIIIIIIIIHGHIJJJJJJJJJIHHGGGGGFFDDCCBBBBBAAABBBCCDEEHHFCEEFFFFFGGGGGGGGGGGGGGGGGGGGGFFFFFFFFFGGGGEEEGGGGGGGGGFEDEEFFGIIIIIIIIHHHHHHHHHHHHHHHHHHGHHHHHHHGGFFFGGGHHHHIIIIHHGGIIJJJJJJJIHHGGGGGFFDDDCBBBBAAAABBCCDDEEIHDEEFFFFFGGGGGGGGGGGGGGGGGGGGFFFFFFFFFFGGGGGGFEEGGHHHGGFDDDFGIJJJJIIIHHHHHHHHHHHHHHHHHHHHHHHHHHHGFFFFFGGGHHHHHHHHHGGGHIJJJJJJIHGGGGGGFFDDDCBBBBBAAAAABCCCDDEEGICDFFFFFFFGGGGGGGGGGGGGGGGGFFFFFFGGGGGGGGGGFFEEEEEGGHHHHHHHHDDDEJJJJJJJIIIHHHHHHHHHHHHHHHHHHHHHHHHHHHGFFFFFFFFGGGGHHHHHHHHHGGGGHHJJJJIHHGGGGGFEEDDCCBBBBAACDEEFICDEFFFFFFFGGGGGGGGGGGGGGGGFFFGGGGGGGGGGGFFGHHHHHHHDDDJJJJJJJIIHHHHHHHHHHHHHGHHHHHHHHHHHHHGFFFFFFFFGGGGHHHHHGGGGGDHHIGGGGGGGFFEDDDDCCBBBAEEGHBCDEFFFFFFFFGGGGGGGGGGGGGGGGGGGGGGGGGGGFFFFFFHHHHIIDDDJJJJJJJJIIIHHHHHHHHHHHHHHHHHHHHHHHHHHHHFFFFFFFFFGGGGGHHHGGGGGGGFEFHHHHHGGGGGGGFEDDDCCBBBBAAAAEEFFHJCEEFFFFFFFFGGGGGGGGGGGGHHHGGGGGGGGGGGGGFFFFFFGHIICDDKJJJJJIIIHHHHHHHHHHHHHHHHHHHHHFFFFFFFFFFFGGGGGGGGGGGGGGGFHHIIIIHHHGGGGGGFDDDDDCBAFG<BCDEEFFFFFFFGGGGGGGGGGHHHHHHGGGGGGGGGGGGGGFFFFFFFHFFFFGHHIIIICCCKKJJJJIIIIHHHHHHHHHHGGHHHHHHHHHHHHFFFFEEFFFFFFGGGGGGGGGGGFFEDIIIIIHHGGGGGGFFDDDDDDBBBAAFFGGII=ABBCDEEEFFFFFFGGGGGGGGHHHHIIIHHHHHHHHGGGGGGGJGGGGGGFFFGGGGGFGGHIIIIICCCKJJIIIIHHHHHHHHHHHHGGGHHHHHHHHHHHFFEEEEEEFFFFGGGGGGGGFEDEHHIIIHHGGGGFEDDDDDDDBAAAGHIG?=<ABCDEEEFFFFFGGGGGGGHHIIIIIIIIIHHHHHGGGGGGGGGGGGGGGGGGHHHHHIIIIJJCCKJJJIIIIIHHHHHHHHHHHGGGHHHHHHHHHHGEEEEEEEFFFFFGGGGFEEEEHHHHHHGGGGGGFFDDDDDCAAGGGGGHIIJJJJJJJJJJJJGF?><ABCCDEEEEFFFGGGGGGHHIIIIIIIIIIIIIHHHGGGGGGGGGGGGGGGGHHHHHHHIIIIJJJJCKJJJJJIIIIHHHHHHHHHHHGGGGGGHHHHHHHHHHHHGGEEEEEEEEFFFFFFGGGGGGFEHHHHHGGGGGGGGGFFDDDDCCCCCBAAAAGGGGIIIJJJJJJJJIGE>==@ABCDDDEEEFFGGGHHIIIIIIIIIIIIIIIHHHHHHGGGGGGGGGGGGHHHHHIIIJJJKKKKJJJJIIIIHHHHHHHHHGGGGGGHHHHHHHHHHGGGEEEEEEEEFFFFFGGFEEEGHHHHHHGGGGGGGGGGFFDDDDDCCCCCCCCCBBAAAAAAAAAGGGGGHHHIIIIJJJJJJJJJHEC==@ABCDDDEEFFGGGHHIIIIIIIIIIIIIIIIIIHHHHGGGGGGGGGGGGGHHHHIIIIIIJJJCCLKKKJJIIIIIIHHHHHHHGGGGGHHHHHHHHHGGGFEEEEEEEEEFFFFFFEEDEGHHHHHHHGGGGGGGGFDDDCCCCCCCCCBAAAAAAAAAAAAAGGGGGHHHIIIIIIIJJJJIIHCC=<@@ABCCDDDEFFGGGHHIIIIIIIIIIIIIIIIIIIIHHHHGGGGGGGGGGHHHHHIIIIIIJJJECCCCCKKJJJIIIIIIHHHHHHHGGGGGGGGGHHHHHHHHGGGGFEEEDDEEEEEFFFFEEEFGGHHHHHHGGGGGGGGFDDDDCCCCCCCCCBAAAAAAAAAAAAAAADDGGGGHHHIIIIIIIIIHHDC=<?@@ABCCDDEEFFGGHHIIIIIIJJJJJJJIIIIIIIHHHGGGGGGGGGGGHHHHHHIIIIIIJJJDCCCCCCCDKKJJJIIIIHHHHHGGGGGGGHHHHHHHGGGGFFEDDDDDDEEEEEEEFFFFFFEEFGGHHGGGGGGGGGGFFEDDCCCCCCCBAAAAAAAAAAAAAADDEFFGHHHIIIIIIIIIIIHHHHED?@@BCCDEFFGHHIIIIIIJJJJJJJJJJIIIIIHHHHGGGGGGGGGHHHHHHIIIIJJJJLDDCCCCCDKKJJJJIIIIHHHHGGGGGGGGFFFHHHHHGGGGGFFFDDDDDEEEEEFFFEFFGGGGGGGGGGGGGFDDDCCCCCCCCCBAAAAAAAAAACDDEFFFGHHIIIIIIIIHHHHFEE?@AABCCDEEFGGGHHIIIIJJJJJJJJJJIIIIIHHHGGGGGGGGGHHHHHHIIIIIJJJJDCCCLKKJJJJIIIHHHHHGGGGGGGGFFFGHHGGGGGGFFFFEDDDDDDDDDEEEEEEEEEEDFGGGGGGGGGGGGGGGGGFFEDDCCCAAAAAAAAAACCCDDEEFGGGHHHIIIHHHHHHHFEE@@AABCDDEFFGGHHIIIJJJJJJJJJJJJJJJIIIIHHHGGGGGGGGHHHHHIIIIIIJJJJJJDCCCJJJJJJIIIHHHGGGGGGGGGGFFGGGGGGGGGFFFFEDDDDDDDDDEEEEECDFGGGGGGGGGGFEDDDDCCCCAAAAAABCCDDEFGHHHHHHHHHHHHGE>ABDEEGGGHHIIJJJJJJJJJJJJJIIIHHHGGGGGGHHHHHIIIIJJJJJJJJCCCJJIIIHHHHGGGGGGGGGGGGGGGGGGFFFEEEDDDDDDDDDEEEDDDDEFFGGGGDDCCCCCBAAAAACCDDEFGHHHHHHHHHHFE<>?@@ABCCDEFFGGHHIIJJJJJJJJJJJJIIIIHHHGGGGGGHHHIIIIIIIIJJJIICCCIIIIIIIHHHHGGGGGGGGGGGFGGGGGGGGFFFEEEDDDDDDDDDDDDDEEEEFFGGFFDDDDDCCCCBBAAAAACCDEFFFGGGHHHHHHHGF<?@AABCCDEFFGGHHIJJJJJJJJJJJJIIIHHGGGGGGHHHIIIIIIIIIJJIIIDCCCCCCIIHIIIIIHHGGGGGGGGGGGFFFGGGGGGGFFEEEDDDDDDDDDDDDDDDDEEEFFGGDDDCCCCCBAAACCDDFFFGGGGHHHGGGFEE<<?@AABCCDEFGGHHJJJJJJJJJIIIHHHGGGGGGGHHIIIIIIIIIJJJKIHHHFEDDCCCCCCCIHHHHIIIIHHHHGGGGGGGGGGGFFGGGGGGFFEEEEDDDDDDDDDDCDDDDEEFFDCCCAAAACDDFFFFFFGGGGGGGEED<<@ABCDEFGGHIIJJJJJJJJJJJJJIIIHHHGGGGGGHHIIIIIIIJJJJHHGGFEDCCCCCCCCCHHHHHHIIIIIIIIIHHHHGGGGGGGGFFFFGGGGGGGFFEEEEDDDDDDDDDDDEFFCCAAADDDEFFFFFFGGGEED<<?@ABCCEFFGGHIIIJJJJJJJJJJJJIIIHHGGGGGGGGGHHHIIIIIIJJJKGFFEEDCCCCBBCCCCCCCDHHHHHHHHIIIIIIIIIHHHGGGGGGGGGFFFFGGGGGFFEEEEDDDDDDDFAADEFFFFFFFFFFGFEEDBA<<?@AABCCDDFFGHHIIJJJJJJJIIIHHGGGGGHHHIIIIJJJKKKKGFFFEDDDCCCBBBBBCCCCCDDGGGHHHHHHIIIIIIIIIHHHGGGGGGGGFFFGGGGGFFFEEEDDDDDEAAAEFFFFFFGGFFEEDC=<<@BCCDFGGHHHIIJJJJJJJIIIHHGGGGGGGGHHHHIIJJJKKLFEEDCCCBBBBBBBBCCCCCDDFGGGHHHHIIIIIIIHHHHGGGGGGGFFFFFFGFFEEEDDDDDAAAADEFFFFFFFFFFEEEDDCBB<<@AACFGFFGHIIJJJJJJIIIHHHGGGGGHHHHIIIJJJKKKEEDDCCCBBBBBBBBBBBBBCCCCCDDDEEEFFFGGGHHHHIIIIIIIIHHHHGGGGGFFFFFFFFEEEEDDDDDEDDDAAAAAAEEFFFFEEEDDC@@?<@GIJJJJJJIIIIHHGGGGGGGGGHHHHIIIJJJKKLLLEEDDCCBBBBBBBBBBBBBBBCCCCDDDEEEFFFGGHHHHIIIIIIIIHHHGGGGGFFFFFFEEEDDDDDDDAAAAAAAEFFFEEDCA=<EHIIJJIIIIIHHHGGGGGGGGGGGGGHHHHIIIJJJKKKLLLLEDDCCBBBBABBBBBBBBBCCCDDDDEEEFFHHHHHIIIIIIIIHHHGGGGGFFEEFFFFFEEEEDDDDDDDDFEEEDDDCCAAAADDEEFFEEEDDCBB@?<<EHIIIIIIIIHHHHGGGGGGGGGGGGGGGHHHHIIJJJKKKKLLLEDDCCBBBAAAABBBBBBBBCCCCDDDDEEFFGGHHIIIIIIIIIHHHGGGGGGFFEFFFFFFEEEDDDDDDDDDDDDDDDDDDFFFFFFEDDDDDDCCCCCAAAACFEDDDCB@=<<DGIIIIIHHHHHGGGGGGGGGGGGGGGGGGHHHHIIJJKKKKLLLEEDDCBBBBBAAABBBBBBBBCCCCDDDDDEEFFFGGGHHHHHIIIIIIIHHHHGGGGFFFFFFFFEEEEDDDDDDDDDDDDDDDDDDDDDDDDCFFFFFFFFEEDDDDDCCCCBAAAAAAABCDEFEDDDCBA@>=<<<DFHHHHHHHHHGGGGGGGGGGGGGGGGGGGGGHHHIIJJJKKKKKLDDCCBBBBAAAABBBBBBBCCCCDDDDEEFFGGGHHHHIIIIIIHHGGGGGGFFFFFFFEEEEDDDDDDDDDDDDDDDDDDDDDDDCDFFFFFFEEEEDDDDDDCCCCBAAAAAAAACDDDCCB?>=<<<<<EFGGGGHHHGGGGGGGGGGGGGGGGGGGGGGGGGHHHIIJJKKKKKDDDCBBBBBAAAAABBBBBBCCCCCDDDDEEFFGHHHIIIIIHHHGGGGGFFEFFFFFFFEEEDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDEFFFFFFEEDDDDDDCCCAAAAAAAAAACDCBA<<<FFFGGGGGFFGGGGGGGGGGGGGGGGGGGGHHHHIIJJKKLLLDDDCCBBBAAAAAABBBBBCCCCCDDDDEEFFFGGHHIIIHHHGGGGGGFEDFFFFFFFEEEEDDDDDDDDDDDDDDDDDDDDDDDDDDCDDDEFFFFEEEEDDDDDDDDCCCAAAAABBBCCCB>=<<BCDEFFFFFFFFFFFGGGGGGGGGGGGGGGGGGHHHHIJJJKDCCBBBBBAAAAABBBBBBCCCCCCDDDDEEGGGGHIHHGGGGGFFFFFFFFEEEDDDDDDDDDDDDDDDDDDDDDDDDDDDCDDDDDEFGGGGEEEDDDDDCBAAAABBBBCCCCA><<BCDEEEEEFFFFFFFFGGGGGGGGGGGGGGGGGGHHHHIJJKKLLDDDCCBBBBAAAAABBBBCCCCCDDGGGGGHHHGGGGGGGFFFFFFFFFEEEEEDDDDDDDDDDDDDDDDDDDDDDDDCCCDDDDEEFGGGGFEEDDDDDCBAAAAAAAAABBBBBBBCCCCCBBA@<BCDDEEEEEEEFFFFFFFGGGGGGGGGGGGGGGGHHHIJJKKKLLDDDCCBBBBAAABBBBBCCCCCCGFGGGIHHHGGGGGGFFFFFFFFFFEEEEEDDDDDDDDDDDDDDDDDDDDDCCCDDDEEEGGGGFFEDDDDDDCCBAAAAAAABBBBBBBCCCCBA??<BCDDDEEEEEEEEFFFFFFGGGGGGGGGGGGGGHHHIIIJJKLLDDCCBBBAAAAABBBBCCCCCCCDFFFIHHGGGGGGFFEFFFFFFFFFFFEEEEEDDDDDDDDDDDDDDDDDDDCCCCCDDDEEGGFFFFDDDDDDCBBAABBBBBBBCCCBA??>=<@ACCDDDDEEEEEEEFFFFFGGGGGGGGGGGGGHHIIJJKKLLDDCCBBBBAAAAAABBBCCCCCCDEFFFFIHGGGGGGFFFEFFFFFFFFFFFEEEEEEDDDDDDDDDDDCCCCCCDEEEGGGGGGGFFFDDDDDCCBBAABBBBBBBBBBBBBCCA?>=<;ABCCDDDDDEEEEEEEEFFFFGGGGGGGGGGGGHIJJJJKKLLDCCCCBBAAAAAABBBCCCCCCCDFFFFFHGGGGGFFFFFFFFFFFFFFFEEEEEDDDDDDDDDDDDBBBCDDEEGGGGGGFDDDDDDDCCBAABBBBBBBBBBBCC?>><<?@AABCCDDDDDDEEEEEEEFFGGGGGGGGGGGHHHIIIJJJKCCCBBBAAAABBBBCCCCCCCDDEEFFFFFHHGGGGGGFFFEFFFFFFFFFFFFFEEEEEEEDDDDDDDDDDCCDEEGGGGGGDDDDDDDCCCAAAABBBBBBBBBBBBCC?>=<;>??@AABCCCDDDDDDEEEEEFFGGGGGGGHHHHIJJJJJKLCCCBBAAAAAABBBBCCCCCCDDDEEEFFFFGHHHHHGGGGGGFFEFFFFFFFFFFFFFEEEEEEDDDDDDDDDDDCCEEFGGGGGGFDDDDDDCCCCAAAAAABBBBBBBBBBBBBBBBBCCCCCCCCC@@>=;>?@@AABBCCCDDDDDEEEEEFFGGGGGGHHHHHHHHJJJJJCCAAAAAABBBCCCCCCDDDEFFFFGGHHHHHGGGGGFFEEFFFFFFFFFFEEEEEEEEEDDDDDDDDDCCBFGGGGFFDDDDDDDCCCAAAAAAAAABBBBBBBBBBBBBBBBBBCCCCCCCCCCC@??==<<?@@@AABCCCCDDDDDEEEEFGGGGHHHHHHHIIJJJJICCCAAAAAAAABBBBCCCCCDDEFFFGGGHHHHHGGGGGGFFEFFFFFFFFFFEEEEEEEDDDDDDDDDDCCBGGGGGFFEDDDDDDDCCCBAAAAAAAAABBBBBBBBBBBBBBBBBBCCCCCCCCCCCCCBA@?>=<<?@@@AABBCCCCDDDDEEEFFGGGHHHHHHHIIIIJJJICAAAABBBBBCCCCCCDEFFFGHHHHHHHGGGGGFEEFFFFFFFFFFEEEEEEEEDDDDDDDDDCCGGFEDDDDDDCCCCBAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBCCCCCCCCCCCBBA@=<??@@@AABBBCCCCCDDEEFFFGGHHHHIIIIIIIJJJJHCCCCBBAAAAAAAABBBBBCCCCCCDGGGHHHHGGGGFFFFFFFFFFEEEEEEEEDDDDDDDDDDCCFGFFEEDDDDDDDCCCBAAAAAAAAABBBBBBBBBBBBBBBBBBBBCCCCCCCCCCCCBAA@<;;?@@@AAABBBCCCCCDDEEFFGHHHIIIIIIIIIJJCCCCCCBBBBAAAAAAAABBBBBCCCCCCCCDGGHHHGGGGGGFFEEFFFFFFFFFEEEEEEEEDDDDDDDCCDFFFFFEEDDDDDDCCCAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBCCCCCCCCBBA@;;??@@@AAAABBBCCCCDDEFFGGHHHIIIIIIIIIJJJJJKDCCCCCBBBBAAAAAABBBBBCCCCCGGGHIHHHGGGGGFFFFFFFFFFEEEEEEEEDDDDDDDDCCBFFFFFFEDDDDDDDCCAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBCCCCCCCBBA@=:??@@@AAAAABCCCCDEFFGHHIIIIIIIIIJJJJKKDDDCCCCCBBBBAAAAAABBBBCCCCCCGGGHHHIHHHGGGGGGFEEFFFFFFFFFEEEEEEEEEEEEDDDDDDDCCBBBFFFFFFDDDDDDDDDDDAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB@<:>??@@@@AAABBCCCDDEFFGGHHIIIIIIIIIIJKDDCCCCCCBBBBAAAAAABCCCCCCCGHHHIIHHGGGGGGFEEFFFFFFFEEEEEEEEEEEEDDDDDBDDEFFFFEEDDDDDDDDDDDDBAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBA?>=>>??@@@@AAABBBCCDEFFGGHHIIIIIIIIIJKKKDEECCCCCBBBBAAAAAABCCCCCCCCDHHIIHHGGGGGGFEFFEEEEEEEEEEEEEEDDDDDDDDEFFEEEDDDDDDDDDDDCAAAAAAAABBBBBBBBBBBBBBBBBCCBBBBBBBA=>>??@@@AABBBCDDEFFGHHIIIIIIIIJJKLEEEDCCCCCBBAAAAAAAABBCCCCCCCCDIHHHGGGGEEEEEEEEEEEEEEEEEDDDDDDCBDDEEEEEEDDDDDDDDCCCAAAAABBBBBBBBBBCCCBBBB=>>??@@@@AABBCDEFFGGHHIIIIIIIIJJKKLLKEECCCCCBBBAAAAAAAAACCCCCCCCDJJJJIIHHHGGGGFDEEEEEEEEEEEEEEEDDDDDCCDDDDEEEEEEEDDDDDDDCCCCAAAAAAABBBBBBBBBBBBCCCDCCB>>???@@@AABCCDEEFFGGHIIIIIIIJJJJKLLKEEEEDDDCCCBBBBAAAAAAAAAAABBBBBCCCCCCDJJJJJHHHHGGGGGGFDEEEEEEEEEEEDDDDCBBCDDDDDEEEDDDDDDCCCCAAAAAABBBBBBBBBBBBCDC>>????@@AABCDDEEFFGGIIIIIIIJJJJKLLLLJEEEDDDCCCCCBBBBAAAAAAAAAABBBBCCCCCCDDDKKKKJKKJIIHHHGGGGGFFDDEEEEEEEEEDBBCCDDDDDDDDDCDDCCCCCAAAAABBBBBBBBBBCDD>>>?@@@AABBCDEFFGHIIIIIJJJJJKLLLKKJFEEEEDDCCCCCBBBAAAAAAAAAAAABBBBBCCCCCDDDDDKKKKKKJIHHHHGGGGGGGFEEDEEEEEEEDDCCCDDDDDCCCCDDCCCCCBBAABBBBBBBBBBBBCDD==>>>??@@@AABCDEGHIIIIIJJJJJKKLLLLKEEDDCCCCCBBAAAAAAAAAAABBBBBBBCCCDDDDKKKHHHHHGGGGGGDDEEEEDCCCCCCBDDDDDDDDCCCCBBBBBBBBB==>>>??@@AABCEFHHIIIJJJJJJKKLLLLFFEEDDCCCCCBAAAAAAAAAABBBBBBBBCCCDDKKLLHHHGGGGGGGFDDECBBCCBDDDDDDDCCCCCBBBBBBBBBBC<==>>>>??@ABDFGHIIJJJJJJJKKLLLLLKGFFDCCCCAAAAAAAABBBBBBBBBCCCDDDEKKLLLHHHGGGGGGGGFDBBDDDDDDCCCBBBBBBBBBBBCCGHIJJJJJJKKKLLLHGDCCCCBBBAAAAABBBBBBBBBBBCCDDDDKLHGGGGGGGGGFFDDDDDDDCCCCCCBBBBBBBBBBBBBCCDEGIJJJJJJKKKKKLLHHCCCBBBBBBBBBBBBBBBBCCCCDDEKKLJJJIHHHGGGGGGGGGFFDDDDDDDDDCCCCCBBBBBBBBBBBBBBBCCEGJJJKKKKKKLLIIICCCCCBBBBBBBBBBBBCCCCCDDDEEFKKJJJIIHHGGGGGGGGGGFFFFDDDDDDDDDDDCCCCCBBBBBBBBBBBBBCCCDHJJJKKKKKKKKKJJICCCCCCBBBBBBBBBCCCCDDDEEFKJJIIIIHGGGGGGGGGGGFFFFFEEEDDDDDDDDDDDDCCCCBBBBBBBBBBBBCCDDGHHJKKKKKKKKKJJCCCCCCBBBBBCCCCDDEEEJIIIIHHHGGGGGGGGGGFFFFFFFFEEEDDBDDDDDDCCBBBBBBBBBDDDDDDDFGIJJKKKKKKKKKKKKJJCCCCCCCCCCCCDEIIHHHHGGGGGGGGGGFFFFFFFFFEEEEEBBCCEEEEEEDDDDDDCCCBDDDDDDDDDHIJJJKKKKKKKKKKKKJKCCCCCCCCCCCDIHHHHGGHGGGGGGGGGFFFFFFFFEEEEBCCDEEEEEEDDDDCCCCDDDDDDDDDDDDDDGIIJJKKKKKKKKKLJJCCCCCCDDHHHHGGGGFFFFGGGGGFFFFFFFFFFFFEEBBDEEFFFEEDDDDCCDDDDDDDDDDDDHHIJJJKKKKKKKKKKLLKJJJCCCDDHHGGGGGFFFFFFFFFFFFFFFFFFFFFFFFFEEDEFFFFFFFEEDDDDDDDDDDDDDDDDDDDDDGHHIJJJKKKKKKKKKKKKKJJKCDCCFGGGGGGFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCCCFFFFFFFFEDDDDDDDDDDDDDDDDDDDDDDDDGHIJJJKKKKKKKKKKKKKKKKKKKKDDDFGGGGGFFF7FFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBCDGDDDDDDDDDDDDDDDDDDDDDDDDDHIIJJJKKKKKKKKKKKKKKKKKKKKDDLCEFGGGGGGGFFFFFFFFEEEEEFFFFFFFFFFFFFFFFFFFFFFFFFFFECEGGGFEDDDDDDDDDDDDDDDDDDDDDIIIJJKKKKKKKKKKKKKKKKKKKKEDDC7LL7GGGGGGFFFFFFEEEEEEEEEEEFFFFFFFFFFFFFFFFFFGGGFFFFEEHHHFEEDDDDDDDDDDDDDDDDDDHIIIJKKKKKKKKKKKKKKKKKKDDCL7777GGGGGGGFFFFEEEEEEEEEEEEEEEFFFFFFFFFFFFFFGGGGGGGFFFDCFGHHHHHHGFEDDDDDDDDDDDDDDHIIJJJKKKKKKKKKKKKKKKKKFEEDDDHHGGGGGGGFFFFEEEEEEEEEEEEEEEEEFFFFFFFFFFFFGGGGGGGFFFFDDCGHGFEDDDDDDDDDDDDDDJJJKKKKKKKKKKKKKFFEEEEDHGGGGGFFFEEEEEEEEEEEEEEEEEEFFFFFFFFFFFGGGGGGGGFFDCFHHGFEDDDDDDDDDDDCCDJJJJJKKKKKKKKKKLLLFFFEEE77HHHHGFFEEEEEEEEEEEEEEEEEEEEFFFFFFFFFFGGGGGGGGGFFFEGHHHHGFDDDDDDDDCCDJJJJKKKKKKKKLLLLFFFFFF777HHGGFFEEEEEEEEEEEEEEEEEEEEEFFFFFFFFGGGGGGGGGGFFEBGHHHHGDDDDDDDDDDDDDDDDCJJIKKKKKKKKLLGFFGGG77HHFEEEEEEEEEEEEEEEEEEEEEEFFFFFFFFGGGGGGGGGGFECCBGHHGDDDDDDDDDDDDDDCCIIKKKKKKKKKKLGGGGHHI77HHG77EEEEEEEEEEEEEEEEEEEEEEFFFFFFFGGGHHGGGGGBEDCBGGGDDDDDDDDDDDDDDDDDCCDIIKKKKKKKKKKIHGGGHI7HH77777EEEEEEEEEEEEEEEEEEEEEEEFFFFFFFGGHHHHHHGGFBCEECBGGGFDDDDDDDDCHHKKKKKKKKKKKIIHHHGHHIIJKKKK7HHH77EEEEEEEEEEDEEEEEEEEEEEEFFFFFGGHHIIHHHHGFCDEFEDCBDDDCCCDDDDDDDCGHJJJKKKKKKKKKHHHHIJJJ77HH7GGFFFEEEEEEEEEDDDDDEEEEEEEEEEEEFFFFGGGHIIIIHHHBDEFEDDCBDCDDDDDDDDDCGIIJJJKKKKKKJHHIIIJJJ7HHG7GFFFFFEEEEEEEDDDDDDDDEEEEEEEEEEFFFGGGHHIIIIIHHFEDBDCDDDDDDDDCGGGGGGHIIIJJKKKKJJJHHIIIIJJIIHHHGGGFFFFEEEEEEDDDDDDDDDEEEEEEEFFFGGHHHIIJJJIIHFCCCCCCCDDDDDCFGGGGGHHIIIJJJJJJHHHHIIIIIHHHGGGFFFFFFEEEEEDDDDDDDDDDEEEEEEEFFHHHHIIJJJJIIFFCCCCCDDDFFFFGHHIIJJJIIHHHIIIIHHGGGFFFFFFFEEEEEDDDDDDDDDDDDEEEEEEFGGHHHIJJJJIIFCCCCCCCEEFFFGGIIIIHHHIHHGGFFFFFFFFFEEEEDDDDDDDDDDDDEEEEEFFHIIIJJJJJJIHEFCCCCCCEFGIIHHHHHHHHHHHHHHHGGFFFFFFFFFFEEEEDDDDDDDDDDDEEEEEEEFFFGIIJJJJJJJIBDEEEFHHHHGGGGGGGGHHHHHHHHHGGGGGGFFFFFFFFFFFFFEEEEDDDEDEEEEEEEFFFGGHHIIJJJJJJJIDEGGGGGGGGGGGGFFGGGGGGGGHHHHHGGGGFFFFFFFFFFFFFFFFFFEEEEEEEEEEEEEFFFFGGHIIJJJJJIHGGGGGGGGGGGFFFFFFFFFFGGGGGGGGGGGFFFFFFFFFFFFFFFFFFFFFFFFEFFFEEEEEEFFFFFFGGHHIIIJJIIIHGG@GGGGGGFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEEFFFFFFFFGGHHIIIIIIHGFC?FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFGGHHHIIIIHHGFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFHHHHGEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEEHGFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEEFFFFFFFFFFFFFFFFFFEEEEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEEEEFFFFFFFFFFEEEDEEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDEEEEEFFFFFFEEEDEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEEEEEEEFFFFFEEDEEFFFFFFFFFGGGGFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEEEEEFFFFEEEEDEFFFFFFFGGGGGGGFFFFFFFFFFFFFFFFFFFFFFFFFDDEEEEEEEEFFFFFGGGGGGGGGFFFFFFFFFFFFFFFFFFFFEEEEDEFFFFGGGGGGGGGFFEFFFFFFFFFFFFFFEEEEFFFGGGGGGGGGGGFEFFFFFFFFEEEFFGGGGGGGGGFEFFFFFFEEFFGGGGGGGGGGFFFFFEFFGGGGGGGGGFEGGGGGGGFEFGGGGGG \ No newline at end of file From 19649529d85db29cb6ec7b2c035973dc5e6da2b5 Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Mon, 13 May 2024 18:41:16 -0400 Subject: [PATCH 04/46] feat: use states on GriffinBurrows Diggable --- .../features/impl/events/GriffinBurrows.kt | 103 ++++++++---------- 1 file changed, 47 insertions(+), 56 deletions(-) diff --git a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt index 4488710e7..08fb6347a 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt @@ -18,6 +18,8 @@ package gg.skytils.skytilsmod.features.impl.events import com.google.common.collect.EvictingQueue +import gg.essential.elementa.state.BasicState +import gg.essential.elementa.state.State import gg.essential.universal.UMatrixStack import gg.essential.universal.UMinecraft import gg.essential.universal.wrappers.UPlayer @@ -36,11 +38,7 @@ import net.minecraft.network.play.client.C08PacketPlayerBlockPlacement import net.minecraft.network.play.server.S04PacketEntityEquipment import net.minecraft.network.play.server.S29PacketSoundEffect import net.minecraft.network.play.server.S2APacketParticles -import net.minecraft.util.AxisAlignedBB -import net.minecraft.util.BlockPos -import net.minecraft.util.EnumParticleTypes -import net.minecraft.util.Vec3 -import net.minecraft.util.Vec3i +import net.minecraft.util.* import net.minecraftforge.client.event.ClientChatReceivedEvent import net.minecraftforge.client.event.RenderWorldLastEvent import net.minecraftforge.event.world.WorldEvent @@ -133,7 +131,7 @@ object GriffinBurrows { if (Skytils.config.showGriffinBurrows) { val matrixStack = UMatrixStack() for (pb in particleBurrows.values) { - if (pb.hasEnchant && pb.hasFootstep && pb.type != -1) { + if (pb.hasEnchant && pb.hasFootstep && pb.type.get() != -1) { pb.drawWaypoint(event.partialTicks, matrixStack) } } @@ -187,14 +185,14 @@ object GriffinBurrows { } } val burrow = particleBurrows.getOrPut(pos) { - ParticleBurrow(pos, hasFootstep = false, hasEnchant = false, type = -1) + ParticleBurrow(pos, hasFootstep = false, hasEnchant = false) } when (type) { ParticleType.FOOTSTEP -> burrow.hasFootstep = true ParticleType.ENCHANT -> burrow.hasEnchant = true - ParticleType.EMPTY -> burrow.type = 0 - ParticleType.MOB -> burrow.type = 1 - ParticleType.TREASURE -> burrow.type = 2 + ParticleType.EMPTY -> burrow.type.set(0) + ParticleType.MOB -> burrow.type.set(1) + ParticleType.TREASURE -> burrow.type.set(2) } } } @@ -235,7 +233,7 @@ object GriffinBurrows { // x ranges from 195 to -281 // z ranges from 207 to -233 do { - y = BurrowEstimation.grassData.get((x++ % 507) * 507 + (z++ % 495)).toInt() + y = BurrowEstimation.grassData[(x++ % 507) * 507 + (z++ % 495)].toInt() } while (y < 2) val guess = BurrowGuess(guessPos.x.toInt(), y, guessPos.z.toInt()) BurrowEstimation.arrows.remove(arrow) @@ -248,35 +246,33 @@ object GriffinBurrows { abstract val x: Int abstract val y: Int abstract val z: Int - abstract var type: Int val blockPos: BlockPos by lazy { BlockPos(x, y, z) } - protected abstract val waypointText: String - protected abstract val color: Color + protected abstract val waypointText: State + protected abstract val color: State fun drawWaypoint(partialTicks: Float, matrixStack: UMatrixStack) { val (viewerX, viewerY, viewerZ) = RenderUtil.getViewerPos(partialTicks) - val pos = blockPos - val x = pos.x - viewerX - val y = pos.y - viewerY - val z = pos.z - viewerZ - val distSq = x * x + y * y + z * z + val renderX = this.x - viewerX + val renderY = this.y - viewerY + val renderZ = this.z - viewerZ + val distSq = renderX * renderX + renderY * renderY + renderZ * renderZ GlStateManager.disableDepth() GlStateManager.disableCull() RenderUtil.drawFilledBoundingBox( matrixStack, - AxisAlignedBB(x, y, z, x + 1, y + 1, z + 1).expandBlock(), - this.color, + AxisAlignedBB(renderX, renderY, renderZ, renderX + 1, renderY + 1, renderZ + 1).expandBlock(), + this.color.get(), (0.1f + 0.005f * distSq.toFloat()).coerceAtLeast(0.2f) ) GlStateManager.disableTexture2D() - if (distSq > 5 * 5) RenderUtil.renderBeaconBeam(x, y + 1, z, this.color.rgb, 1.0f, partialTicks) + if (distSq > 5 * 5) RenderUtil.renderBeaconBeam(renderX, renderY + 1, renderZ, this.color.get().rgb, 1.0f, partialTicks) RenderUtil.renderWaypointText( - waypointText, - blockPos.x + 0.5, - blockPos.y + 5.0, - blockPos.z + 0.5, + waypointText.get(), + x + 0.5, + y + 5.0, + z + 0.5, partialTicks, matrixStack ) @@ -292,9 +288,8 @@ object GriffinBurrows { override val y: Int, override val z: Int ) : Diggable() { - override var type = 0 - override val waypointText = "§aBurrow §6(Guess)" - override val color = Color.ORANGE + override val waypointText = BasicState("§aBurrow §6(Guess)") + override val color = BasicState(Color.ORANGE) } data class ParticleBurrow( @@ -302,37 +297,36 @@ object GriffinBurrows { override val y: Int, override val z: Int, var hasFootstep: Boolean, - var hasEnchant: Boolean, - override var type: Int + var hasEnchant: Boolean ) : Diggable() { - constructor(vec3: Vec3i, hasFootstep: Boolean, hasEnchant: Boolean, type: Int) : this( + constructor(vec3: Vec3i, hasFootstep: Boolean, hasEnchant: Boolean) : this( vec3.x, vec3.y, vec3.z, hasFootstep, - hasEnchant, - type + hasEnchant ) - override val waypointText: String - get() { - var type = "Burrow" - when (this.type) { - 0 -> type = "§aStart" - 1 -> type = "§cMob" - 2 -> type = "§6Treasure" - } - return "$type §a(Particle)" - } - override val color: Color - get() { - return when (this.type) { - 0 -> Skytils.config.emptyBurrowColor - 1 -> Skytils.config.mobBurrowColor - 2 -> Skytils.config.treasureBurrowColor - else -> Color.WHITE + val type = BasicState(-1) + + override val waypointText = type.map { + "${ + when (it) { + 0 -> "§aStart" + 1 -> "§cMob" + 2 -> "§6Treasure" + else -> "§7Unknown" } + } §a(Particle)" + } + override val color = type.map { + when (it) { + 0 -> Skytils.config.emptyBurrowColor + 1 -> Skytils.config.mobBurrowColor + 2 -> Skytils.config.treasureBurrowColor + else -> Color.WHITE } + } } private val ItemStack?.isSpade @@ -359,12 +353,9 @@ object GriffinBurrows { companion object { fun getParticleType(packet: S2APacketParticles): ParticleType? { if (!packet.isLongDistance) return null - for (type in entries) { - if (type.check(packet)) { - return type - } + return entries.find { + it.check(packet) } - return null } } } From 4dfa90a4556d16ed81135ed5bf158c4f9f2696af Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Mon, 13 May 2024 18:54:28 -0400 Subject: [PATCH 05/46] fix: NPE on griffin burrow estimate --- .../skytilsmod/features/impl/events/GriffinBurrows.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt index 08fb6347a..9e2d57ba8 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt @@ -198,10 +198,10 @@ object GriffinBurrows { } } is S04PacketEntityEquipment -> { - val entity = UMinecraft.getMinecraft().theWorld.getEntityByID(event.packet.entityID) + val entity = mc.theWorld?.getEntityByID(event.packet.entityID) (entity as? EntityArmorStand)?.let { armorStand -> - if (event.packet.itemStack.item != Items.arrow) return - if (armorStand.getDistanceSq(UPlayer.getPlayer()?.position) >= 27) return + if (event.packet.itemStack?.item != Items.arrow) return + if (armorStand.getDistanceSq(mc.thePlayer?.position) >= 27) return printDevMessage("Found armor stand with arrow", "griffin", "griffinguess") val yaw = Math.toRadians(armorStand.rotationYaw.toDouble()) val lookVec = Vec3( From 6a3613d72e3cf7e58c67706dbc3e8cbdd14eef03 Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Mon, 13 May 2024 18:57:42 -0400 Subject: [PATCH 06/46] feat: Show New Year Cake Year --- src/main/kotlin/gg/skytils/skytilsmod/core/Config.kt | 10 ++++++++++ .../skytilsmod/features/impl/misc/ItemFeatures.kt | 3 +++ src/main/resources/assets/skytils/lang/en_US.lang | 1 + src/main/resources/assets/skytils/lang/zh_CN.lang | 1 + src/main/resources/assets/skytils/lang/zh_TW.lang | 1 + 5 files changed, 16 insertions(+) diff --git a/src/main/kotlin/gg/skytils/skytilsmod/core/Config.kt b/src/main/kotlin/gg/skytils/skytilsmod/core/Config.kt index db62a95ca..7c2c113b7 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/core/Config.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/core/Config.kt @@ -2300,6 +2300,16 @@ object Config : Vigilant( ) var showOrigin = false + @Property( + type = PropertyType.SWITCH, name = "Show New Year Cake Year", + description = "Shows the year of a New Year Cake as the stack size.", + category = "Miscellaneous", subcategory = "Items", + i18nName = "skytils.config.miscellaneous.items.show_new_year_cake_year", + i18nCategory = "skytils.config.miscellaneous", + i18nSubcategory = "skytils.config.miscellaneous.items" + ) + var showNYCakeYear = false + @Property( type = PropertyType.SWITCH, name = "Show NPC Sell Price", description = "Shows the NPC Sell Price on certain items.", diff --git a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/misc/ItemFeatures.kt b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/misc/ItemFeatures.kt index e969ac383..0e4c90fc8 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/misc/ItemFeatures.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/misc/ItemFeatures.kt @@ -687,6 +687,9 @@ object ItemFeatures { UGraphics.enableDepth() } } + if (Skytils.config.showNYCakeYear && extraAttributes.hasKey("new_years_cake")) { + stackTip = extraAttributes.getInteger("new_years_cake").toString() + } } if (Skytils.config.showPetCandies && item.item === Items.skull) { val petInfoString = getExtraAttributes(item)?.getString("petInfo") diff --git a/src/main/resources/assets/skytils/lang/en_US.lang b/src/main/resources/assets/skytils/lang/en_US.lang index 4e7da1f59..a992c140c 100644 --- a/src/main/resources/assets/skytils/lang/en_US.lang +++ b/src/main/resources/assets/skytils/lang/en_US.lang @@ -217,6 +217,7 @@ skytils.config.miscellaneous.items.etherwarp_teleport_position_color=Etherwarp T skytils.config.miscellaneous.items.show_gemstones=Show Gemstones skytils.config.miscellaneous.items.show_head_floor_number=Show Head Floor Number skytils.config.miscellaneous.items.show_item_origin=Show Item Origin +skytils.config.miscellaneous.items.show_new_year_cake_year=Show New Year Cake Year skytils.config.miscellaneous.items.show_npc_sell_price=Show NPC Sell Price skytils.config.miscellaneous.items.show_potion_tier=Show Potion Tier skytils.config.miscellaneous.items.show_pet_candies=Show Pet Candies diff --git a/src/main/resources/assets/skytils/lang/zh_CN.lang b/src/main/resources/assets/skytils/lang/zh_CN.lang index 2e97ccdca..e1f2be2bb 100644 --- a/src/main/resources/assets/skytils/lang/zh_CN.lang +++ b/src/main/resources/assets/skytils/lang/zh_CN.lang @@ -217,6 +217,7 @@ skytils.config.miscellaneous.items.etherwarp_teleport_position_color=Etherwarp skytils.config.miscellaneous.items.show_gemstones=显示宝石 skytils.config.miscellaneous.items.show_head_floor_number=显示金头/钻头所属地牢层数 skytils.config.miscellaneous.items.show_item_origin=显示物品来源 +skytils.config.miscellaneous.items.show_new_year_cake_year=显示新年蛋糕年份 skytils.config.miscellaneous.items.show_npc_sell_price=显示NPC出售价格 skytils.config.miscellaneous.items.show_potion_tier=显示药水等级 skytils.config.miscellaneous.items.show_pet_candies=显示宠物使用糖果数 diff --git a/src/main/resources/assets/skytils/lang/zh_TW.lang b/src/main/resources/assets/skytils/lang/zh_TW.lang index f497dc34c..37515d7c1 100644 --- a/src/main/resources/assets/skytils/lang/zh_TW.lang +++ b/src/main/resources/assets/skytils/lang/zh_TW.lang @@ -217,6 +217,7 @@ skytils.config.miscellaneous.items.etherwarp_teleport_position_color=Etherwarp skytils.config.miscellaneous.items.show_gemstones=顯示寶石 skytils.config.miscellaneous.items.show_head_floor_number=顯示金頭/鑽頭所屬地牢層數 skytils.config.miscellaneous.items.show_item_origin=顯示物品來源 +skytils.config.miscellaneous.items.show_new_year_cake_year=顯示新年蛋糕年份 skytils.config.miscellaneous.items.show_npc_sell_price=顯示NPC出售價格 skytils.config.miscellaneous.items.show_potion_tier=顯示藥水等級 skytils.config.miscellaneous.items.show_pet_candies=顯示寵物使用糖果數 From 1f24fd6ef7e798d06d603a801e22dd1a24fe65cb Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Tue, 14 May 2024 18:48:46 -0400 Subject: [PATCH 07/46] chore: add Fann level gui to confirmable signs --- .../gg/skytils/skytilsmod/mixins/hooks/gui/GuiEditSignHook.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/gg/skytils/skytilsmod/mixins/hooks/gui/GuiEditSignHook.kt b/src/main/kotlin/gg/skytils/skytilsmod/mixins/hooks/gui/GuiEditSignHook.kt index 5526176ad..de8432078 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/mixins/hooks/gui/GuiEditSignHook.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/mixins/hooks/gui/GuiEditSignHook.kt @@ -28,6 +28,7 @@ val prompts = listOf( "Your auction", "Enter the amount", "Auction", // Auction time + "Please enter", // Fann level selection ) fun isConfirmableSign(gui: AccessorGuiEditSign): Boolean = From 6397c064eb1e075ec2bd31e32fde348936fdea39 Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Tue, 14 May 2024 19:14:59 -0400 Subject: [PATCH 08/46] fix: add toggle for burrow estimation --- .../gg/skytils/skytilsmod/core/Config.kt | 8 ++++ .../features/impl/events/GriffinBurrows.kt | 39 +++++++++++-------- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/main/kotlin/gg/skytils/skytilsmod/core/Config.kt b/src/main/kotlin/gg/skytils/skytilsmod/core/Config.kt index 7c2c113b7..832cf3c98 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/core/Config.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/core/Config.kt @@ -1562,6 +1562,14 @@ object Config : Vigilant( ) var treasureBurrowColor = Color(173, 216, 230) + // TODO: Add translations + @Property( + type = PropertyType.SWITCH, name = "Griffin Burrow Estimation", + description = "Estimates griffin burrow position after using spade near the previous burrow.", + category = "Events", subcategory = "Mythological" + ) + var burrowEstimation = false + @Property( type = PropertyType.SWITCH, name = "Broadcast Rare Drop Notifications", description = "Sends rare drop notification when you obtain a rare drop from a Mythological Creature.", diff --git a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt index 9e2d57ba8..8e9b36d7f 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt @@ -80,6 +80,7 @@ object GriffinBurrows { hasSpadeInHotbar = mc.thePlayer != null && Utils.inSkyblock && (0..7).any { mc.thePlayer.inventory.getStackInSlot(it).isSpade } + if (!Skytils.config.burrowEstimation) return BurrowEstimation.guesses.entries.removeIf { (_, instant) -> Duration.between(instant, Instant.now()).toMinutes() > 30 } @@ -135,22 +136,24 @@ object GriffinBurrows { pb.drawWaypoint(event.partialTicks, matrixStack) } } - for (bg in BurrowEstimation.guesses.keys) { - bg.drawWaypoint(event.partialTicks, matrixStack) - } - for (arrow in BurrowEstimation.arrows.keys) { - RenderUtil.drawCircle( - matrixStack, - arrow.pos.x, - arrow.pos.y + 0.2, - arrow.pos.z, - event.partialTicks, - 5.0, - 100, - 255, - 128, - 0, - ) + if (Skytils.config.burrowEstimation) { + for (bg in BurrowEstimation.guesses.keys) { + bg.drawWaypoint(event.partialTicks, matrixStack) + } + for (arrow in BurrowEstimation.arrows.keys) { + RenderUtil.drawCircle( + matrixStack, + arrow.pos.x, + arrow.pos.y + 0.2, + arrow.pos.z, + event.partialTicks, + 5.0, + 100, + 255, + 128, + 0, + ) + } } } } @@ -198,6 +201,7 @@ object GriffinBurrows { } } is S04PacketEntityEquipment -> { + if (!Skytils.config.burrowEstimation) return val entity = mc.theWorld?.getEntityByID(event.packet.entityID) (entity as? EntityArmorStand)?.let { armorStand -> if (event.packet.itemStack?.item != Items.arrow) return @@ -215,6 +219,7 @@ object GriffinBurrows { } } is S29PacketSoundEffect -> { + if (!Skytils.config.burrowEstimation) return if (event.packet.soundName != "note.harp") return val (arrow, distance) = BurrowEstimation.arrows.keys .associateWith { arrow -> @@ -233,7 +238,7 @@ object GriffinBurrows { // x ranges from 195 to -281 // z ranges from 207 to -233 do { - y = BurrowEstimation.grassData[(x++ % 507) * 507 + (z++ % 495)].toInt() + y = BurrowEstimation.grassData.getOrNull((x++ % 507) * 507 + (z++ % 495))?.toInt() ?: 0 } while (y < 2) val guess = BurrowGuess(guessPos.x.toInt(), y, guessPos.z.toInt()) BurrowEstimation.arrows.remove(arrow) From a3b2a298e3a6a5b124351c209493e98c45c75b92 Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Tue, 14 May 2024 19:23:21 -0400 Subject: [PATCH 09/46] fix: burrow distance formula --- .../skytils/skytilsmod/features/impl/events/GriffinBurrows.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt index 8e9b36d7f..d69713d1d 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt @@ -64,7 +64,7 @@ object GriffinBurrows { val arrows = mutableMapOf() val guesses = mutableMapOf() fun getDistanceFromPitch(pitch: Double) = - 2805 * pitch + 1375 + 2805 * pitch - 1375 val grassData by lazy { this::class.java.getResource("/assets/skytils/grassdata.txt")!!.readBytes() From f717722c8578b563c5d703fd2b8aaf71f38e1484 Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Tue, 14 May 2024 19:41:14 -0400 Subject: [PATCH 10/46] fix: adjust burrow estimation timeout to be shorter --- .../skytils/skytilsmod/features/impl/events/GriffinBurrows.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt index d69713d1d..b77414202 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt @@ -82,10 +82,10 @@ object GriffinBurrows { } if (!Skytils.config.burrowEstimation) return BurrowEstimation.guesses.entries.removeIf { (_, instant) -> - Duration.between(instant, Instant.now()).toMinutes() > 30 + Duration.between(instant, Instant.now()).toMinutes() > 2 } BurrowEstimation.arrows.entries.removeIf { (_, instant) -> - Duration.between(instant, Instant.now()).toMinutes() > 5 + Duration.between(instant, Instant.now()).toMillis() > 30_000L } } From dd9113c6aa8eb632662530161138228e02dcf979 Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Tue, 14 May 2024 19:25:47 -0400 Subject: [PATCH 11/46] version: 1.9.5 --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index b23cd863b..b61e6eb87 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,7 +34,7 @@ plugins { signing } -version = "1.9.4" +version = "1.9.5" group = "gg.skytils" repositories { From 227f6cf9eb865658d351fe58f3ff34f3155c1e8c Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Tue, 14 May 2024 22:42:50 -0400 Subject: [PATCH 12/46] perf: use setters instead of states for griffin burrows --- .../features/impl/events/GriffinBurrows.kt | 66 ++++++++++--------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt index b77414202..e864101ec 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt @@ -132,7 +132,7 @@ object GriffinBurrows { if (Skytils.config.showGriffinBurrows) { val matrixStack = UMatrixStack() for (pb in particleBurrows.values) { - if (pb.hasEnchant && pb.hasFootstep && pb.type.get() != -1) { + if (pb.hasEnchant && pb.hasFootstep && pb.type != -1) { pb.drawWaypoint(event.partialTicks, matrixStack) } } @@ -193,15 +193,15 @@ object GriffinBurrows { when (type) { ParticleType.FOOTSTEP -> burrow.hasFootstep = true ParticleType.ENCHANT -> burrow.hasEnchant = true - ParticleType.EMPTY -> burrow.type.set(0) - ParticleType.MOB -> burrow.type.set(1) - ParticleType.TREASURE -> burrow.type.set(2) + ParticleType.EMPTY -> burrow.type = 0 + ParticleType.MOB -> burrow.type = 1 + ParticleType.TREASURE -> burrow.type = 2 } } } } is S04PacketEntityEquipment -> { - if (!Skytils.config.burrowEstimation) return + if (!Skytils.config.burrowEstimation || SBInfo.mode != SkyblockIsland.Hub.mode) return val entity = mc.theWorld?.getEntityByID(event.packet.entityID) (entity as? EntityArmorStand)?.let { armorStand -> if (event.packet.itemStack?.item != Items.arrow) return @@ -219,7 +219,7 @@ object GriffinBurrows { } } is S29PacketSoundEffect -> { - if (!Skytils.config.burrowEstimation) return + if (!Skytils.config.burrowEstimation || SBInfo.mode != SkyblockIsland.Hub.mode) return if (event.packet.soundName != "note.harp") return val (arrow, distance) = BurrowEstimation.arrows.keys .associateWith { arrow -> @@ -255,8 +255,8 @@ object GriffinBurrows { BlockPos(x, y, z) } - protected abstract val waypointText: State - protected abstract val color: State + protected abstract val waypointText: String + protected abstract val color: Color fun drawWaypoint(partialTicks: Float, matrixStack: UMatrixStack) { val (viewerX, viewerY, viewerZ) = RenderUtil.getViewerPos(partialTicks) val renderX = this.x - viewerX @@ -268,13 +268,13 @@ object GriffinBurrows { RenderUtil.drawFilledBoundingBox( matrixStack, AxisAlignedBB(renderX, renderY, renderZ, renderX + 1, renderY + 1, renderZ + 1).expandBlock(), - this.color.get(), + this.color, (0.1f + 0.005f * distSq.toFloat()).coerceAtLeast(0.2f) ) GlStateManager.disableTexture2D() - if (distSq > 5 * 5) RenderUtil.renderBeaconBeam(renderX, renderY + 1, renderZ, this.color.get().rgb, 1.0f, partialTicks) + if (distSq > 5 * 5) RenderUtil.renderBeaconBeam(renderX, renderY + 1, renderZ, this.color.rgb, 1.0f, partialTicks) RenderUtil.renderWaypointText( - waypointText.get(), + waypointText, x + 0.5, y + 5.0, z + 0.5, @@ -293,8 +293,8 @@ object GriffinBurrows { override val y: Int, override val z: Int ) : Diggable() { - override val waypointText = BasicState("§aBurrow §6(Guess)") - override val color = BasicState(Color.ORANGE) + override val waypointText = "§aBurrow §6(Guess)" + override val color = Color.ORANGE } data class ParticleBurrow( @@ -312,26 +312,30 @@ object GriffinBurrows { hasEnchant ) - val type = BasicState(-1) - - override val waypointText = type.map { - "${ - when (it) { - 0 -> "§aStart" - 1 -> "§cMob" - 2 -> "§6Treasure" - else -> "§7Unknown" + var type = -1 + set(value) { + field = value + when (value) { + 0 -> { + waypointText = "§aStart §a(Particle)" + color = Skytils.config.emptyBurrowColor + } + 1 -> { + waypointText = "§cMob §a(Particle)" + color = Skytils.config.mobBurrowColor + } + 2 -> { + waypointText = "§6Treasure §a(Particle)" + color = Skytils.config.treasureBurrowColor + } } - } §a(Particle)" - } - override val color = type.map { - when (it) { - 0 -> Skytils.config.emptyBurrowColor - 1 -> Skytils.config.mobBurrowColor - 2 -> Skytils.config.treasureBurrowColor - else -> Color.WHITE } - } + + override var waypointText = "§7Unknown §a(Particle)" + private set + + override var color = Color.WHITE + private set } private val ItemStack?.isSpade From 4109d95ec0955484f3c660d38d61ca062516e6d3 Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Wed, 15 May 2024 12:19:03 -0400 Subject: [PATCH 13/46] feat: ping when particle burrow nearby --- .../gg/skytils/skytilsmod/core/Config.kt | 11 +++++++++- .../features/impl/events/GriffinBurrows.kt | 22 ++++++++++--------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/gg/skytils/skytilsmod/core/Config.kt b/src/main/kotlin/gg/skytils/skytilsmod/core/Config.kt index 832cf3c98..a183968b8 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/core/Config.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/core/Config.kt @@ -1563,6 +1563,13 @@ object Config : Vigilant( var treasureBurrowColor = Color(173, 216, 230) // TODO: Add translations + @Property( + type = PropertyType.SWITCH, name = "Ping when Burrow is Nearby", + description = "Pings when a burrow is nearby.", + category = "Events", subcategory = "Mythological" + ) + var pingNearbyBurrow = false + @Property( type = PropertyType.SWITCH, name = "Griffin Burrow Estimation", description = "Estimates griffin burrow position after using spade near the previous burrow.", @@ -4376,7 +4383,9 @@ object Config : Vigilant( arrayOf( "emptyBurrowColor", "mobBurrowColor", - "treasureBurrowColor" + "treasureBurrowColor", + "burrowEstimation", + "pingNearbyBurrow" ).forEach { propertyName -> addDependency(propertyName, "showGriffinBurrows") } addDependency("activePetColor", "highlightActivePet") diff --git a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt index e864101ec..a2fd5d02e 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt @@ -18,13 +18,10 @@ package gg.skytils.skytilsmod.features.impl.events import com.google.common.collect.EvictingQueue -import gg.essential.elementa.state.BasicState -import gg.essential.elementa.state.State import gg.essential.universal.UMatrixStack -import gg.essential.universal.UMinecraft -import gg.essential.universal.wrappers.UPlayer import gg.skytils.skytilsmod.Skytils import gg.skytils.skytilsmod.Skytils.Companion.mc +import gg.skytils.skytilsmod.core.SoundQueue import gg.skytils.skytilsmod.events.impl.MainReceivePacketEvent import gg.skytils.skytilsmod.events.impl.PacketEvent import gg.skytils.skytilsmod.utils.* @@ -56,7 +53,7 @@ import kotlin.math.sin object GriffinBurrows { val particleBurrows = hashMapOf() var lastDugParticleBurrow: BlockPos? = null - val recentlyDugParticleBurrows: EvictingQueue = EvictingQueue.create(5) + val recentlyDugParticleBurrows = EvictingQueue.create(5) var hasSpadeInHotbar = false @@ -190,6 +187,11 @@ object GriffinBurrows { val burrow = particleBurrows.getOrPut(pos) { ParticleBurrow(pos, hasFootstep = false, hasEnchant = false) } + if (burrow.type == -1 && type.isBurrowType) { + if (Skytils.config.pingNearbyBurrow) { + SoundQueue.addToQueue("random.orb", 0.8f, 1f, 0, true) + } + } when (type) { ParticleType.FOOTSTEP -> burrow.hasFootstep = true ParticleType.ENCHANT -> burrow.hasEnchant = true @@ -255,8 +257,8 @@ object GriffinBurrows { BlockPos(x, y, z) } - protected abstract val waypointText: String - protected abstract val color: Color + abstract val waypointText: String + abstract val color: Color fun drawWaypoint(partialTicks: Float, matrixStack: UMatrixStack) { val (viewerX, viewerY, viewerZ) = RenderUtil.getViewerPos(partialTicks) val renderX = this.x - viewerX @@ -341,7 +343,7 @@ object GriffinBurrows { private val ItemStack?.isSpade get() = ItemUtil.getSkyBlockItemID(this) == "ANCESTRAL_SPADE" - private enum class ParticleType(val check: S2APacketParticles.() -> Boolean) { + private enum class ParticleType(val check: S2APacketParticles.() -> Boolean, val isBurrowType: Boolean = true) { EMPTY({ type == EnumParticleTypes.CRIT_MAGIC && count == 4 && speed == 0.01f && xOffset == 0.5f && yOffset == 0.1f && zOffset == 0.5f }), @@ -354,10 +356,10 @@ object GriffinBurrows { }), FOOTSTEP({ type == EnumParticleTypes.FOOTSTEP && count == 1 && speed == 0.0f && xOffset == 0.05f && yOffset == 0.0f && zOffset == 0.05f - }), + }, false), ENCHANT({ type == EnumParticleTypes.ENCHANTMENT_TABLE && count == 5 && speed == 0.05f && xOffset == 0.5f && yOffset == 0.4f && zOffset == 0.5f - }); + }, false); companion object { fun getParticleType(packet: S2APacketParticles): ParticleType? { From 9cd31ab934f9bf703595f2c2bfcc248f2a05332e Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Wed, 15 May 2024 12:23:53 -0400 Subject: [PATCH 14/46] feat: draw client player head on Catlas even when dead --- .../features/impl/dungeons/catlas/core/CatlasElement.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/dungeons/catlas/core/CatlasElement.kt b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/dungeons/catlas/core/CatlasElement.kt index a75270cc7..9e882c8fa 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/dungeons/catlas/core/CatlasElement.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/dungeons/catlas/core/CatlasElement.kt @@ -243,7 +243,7 @@ object CatlasElement : GuiElement(name = "Dungeon Map", x = 0, y = 0) { private fun renderPlayerHeads() { if (DungeonTimer.bossEntryTime != -1L) return DungeonListener.team.forEach { (name, teammate) -> - if (!teammate.dead) { + if (!teammate.dead && !teammate.mapPlayer.isOurMarker) { RenderUtils.drawPlayerHead(name, teammate.mapPlayer) } } From 205edae690a32bf015dce4a2371525ac17032724 Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Wed, 15 May 2024 12:35:51 -0400 Subject: [PATCH 15/46] feat: reset burrow data when server resets it --- .../skytilsmod/features/impl/events/GriffinBurrows.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt index a2fd5d02e..58abaea6a 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt @@ -104,6 +104,13 @@ object GriffinBurrows { lastDugParticleBurrow = null } } + if (event.message.formattedText == "§r§6Poof! §r§eYou have cleared your griffin burrows!§r") { + particleBurrows.clear() + recentlyDugParticleBurrows.clear() + lastDugParticleBurrow = null + BurrowEstimation.guesses.clear() + BurrowEstimation.arrows.clear() + } } @SubscribeEvent From 26ed9bb0467c57cf216aaf2aebfd473a5d9760d2 Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Wed, 15 May 2024 22:47:08 -0400 Subject: [PATCH 16/46] fix: make sure DataFetcher only runs once at a time --- .../skytilsmod/commands/impl/SkytilsCommand.kt | 3 ++- .../kotlin/gg/skytils/skytilsmod/core/DataFetcher.kt | 11 +++++++++-- .../skytilsmod/features/impl/misc/SummonSkins.kt | 3 ++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/gg/skytils/skytilsmod/commands/impl/SkytilsCommand.kt b/src/main/kotlin/gg/skytils/skytilsmod/commands/impl/SkytilsCommand.kt index 510f0635b..54ca1c900 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/commands/impl/SkytilsCommand.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/commands/impl/SkytilsCommand.kt @@ -134,7 +134,8 @@ object SkytilsCommand : BaseCommand("skytils", listOf("st")) { } else { when (args[1].lowercase()) { "data" -> { - DataFetcher.reloadData().invokeOnCompletion { + DataFetcher.reloadData() + DataFetcher.job?.invokeOnCompletion { it?.run { UChat.chat("$failPrefix §cFailed to reload repository data due to a ${it::class.simpleName ?: "error"}: ${it.message}!") }.ifNull { diff --git a/src/main/kotlin/gg/skytils/skytilsmod/core/DataFetcher.kt b/src/main/kotlin/gg/skytils/skytilsmod/core/DataFetcher.kt index a2a7189ae..44215d956 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/core/DataFetcher.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/core/DataFetcher.kt @@ -51,6 +51,8 @@ import kotlin.concurrent.fixedRateTimer import kotlin.reflect.jvm.jvmName object DataFetcher { + var job: Job? = null + private fun loadData(): Job { return Skytils.IO.launch { try { @@ -213,8 +215,13 @@ object DataFetcher { } @JvmStatic - fun reloadData() = - loadData() + fun reloadData() { + if (job?.isActive != true) { + job = loadData() + } else { + UChat.chat("$failPrefix §cData fetch requested while already fetching!") + } + } internal fun preload() {} diff --git a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/misc/SummonSkins.kt b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/misc/SummonSkins.kt index c33fed7eb..c4034cab3 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/misc/SummonSkins.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/misc/SummonSkins.kt @@ -26,11 +26,12 @@ import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.utils.io.jvm.javaio.* import kotlinx.coroutines.launch +import java.util.concurrent.ConcurrentHashMap import javax.imageio.ImageIO object SummonSkins { // maps name to url - val skinMap = HashMap() + val skinMap = ConcurrentHashMap() // maps name to dynamic resource val skintextureMap = HashMap() From e0bc8cc46c6a475676731ade11440631d41ab865 Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Thu, 16 May 2024 18:51:12 -0400 Subject: [PATCH 17/46] feat: add new Griffin Burrow guess method based on spade --- .../commands/impl/SkytilsCommand.kt | 3 + .../gg/skytils/skytilsmod/core/Config.kt | 10 +- .../features/impl/events/GriffinBurrows.kt | 178 +++++++++++++----- 3 files changed, 146 insertions(+), 45 deletions(-) diff --git a/src/main/kotlin/gg/skytils/skytilsmod/commands/impl/SkytilsCommand.kt b/src/main/kotlin/gg/skytils/skytilsmod/commands/impl/SkytilsCommand.kt index 54ca1c900..c4e527b97 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/commands/impl/SkytilsCommand.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/commands/impl/SkytilsCommand.kt @@ -110,6 +110,9 @@ object SkytilsCommand : BaseCommand("skytils", listOf("st")) { "refresh" -> { GriffinBurrows.particleBurrows.clear() } + "clearguess" -> { + GriffinBurrows.BurrowEstimation.guesses.clear() + } else -> UChat.chat("$prefix §b/skytils griffin ") } diff --git a/src/main/kotlin/gg/skytils/skytilsmod/core/Config.kt b/src/main/kotlin/gg/skytils/skytilsmod/core/Config.kt index a183968b8..f7bd0699b 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/core/Config.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/core/Config.kt @@ -1577,6 +1577,13 @@ object Config : Vigilant( ) var burrowEstimation = false + @Property( + type = PropertyType.SWITCH, name = "Griffin Burrow Estimation (MEOW)", + description = "Estimates griffin burrow position after using spade ANYWHERE. Use of this mode will disable the other mode.", + category = "Events", subcategory = "Mythological" + ) + var experimentBurrowEstimation = false + @Property( type = PropertyType.SWITCH, name = "Broadcast Rare Drop Notifications", description = "Sends rare drop notification when you obtain a rare drop from a Mythological Creature.", @@ -4385,7 +4392,8 @@ object Config : Vigilant( "mobBurrowColor", "treasureBurrowColor", "burrowEstimation", - "pingNearbyBurrow" + "pingNearbyBurrow", + "experimentBurrowEstimation" ).forEach { propertyName -> addDependency(propertyName, "showGriffinBurrows") } addDependency("activePetColor", "highlightActivePet") diff --git a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt index 58abaea6a..8d13f3c8c 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt @@ -46,9 +46,7 @@ import net.minecraftforge.fml.common.gameevent.TickEvent.ClientTickEvent import java.awt.Color import java.time.Duration import java.time.Instant -import kotlin.math.PI -import kotlin.math.cos -import kotlin.math.sin +import kotlin.math.* object GriffinBurrows { val particleBurrows = hashMapOf() @@ -56,10 +54,15 @@ object GriffinBurrows { val recentlyDugParticleBurrows = EvictingQueue.create(5) var hasSpadeInHotbar = false + var lastSpadeUse = -1L object BurrowEstimation { val arrows = mutableMapOf() val guesses = mutableMapOf() + val lastTrail = mutableListOf() + var lastTrailCreated = -1L + var firstDistanceGuess = 0.0 + fun getDistanceFromPitch(pitch: Double) = 2805 * pitch - 1375 @@ -70,7 +73,6 @@ object GriffinBurrows { class Arrow(val directionVector: Vec3, val pos: Vec3) } - @SubscribeEvent fun onTick(event: ClientTickEvent) { if (event.phase != TickEvent.Phase.START) return @@ -84,6 +86,69 @@ object GriffinBurrows { BurrowEstimation.arrows.entries.removeIf { (_, instant) -> Duration.between(instant, Instant.now()).toMillis() > 30_000L } + if (!Skytils.config.experimentBurrowEstimation) return + if (BurrowEstimation.firstDistanceGuess != -1.0 && BurrowEstimation.lastTrail.size >= 2 && System.currentTimeMillis() - BurrowEstimation.lastTrailCreated > 1000) { + printDevMessage("Trail found ${BurrowEstimation.lastTrail}", "griffinguess") + printDevMessage("Pitch ${BurrowEstimation.firstDistanceGuess}", "griffinguess") + + val pitch = BurrowEstimation.firstDistanceGuess + + fun cubicFunc(x: Double): Double { + return -355504.3762671333 * x.pow(3) + 573210.5260410917 * x.pow(2) - 304839.7095941929 * x + 53581.7430868503 + } + + fun logFunc(x: Double): Double { + return 1018.3988994912 + 3301.6178248206 * log10(x) + } + + fun linearFunc(x: Double): Double { + return 2760.6568614981 * x - 1355.0749724848 + } + + val distanceGuessLog = logFunc(pitch) + val distanceGuessC = cubicFunc(pitch) + val distanceGuessL = linearFunc(pitch) + + val distanceGuess = if (pitch < 0.5) distanceGuessC else if (pitch > .6) (distanceGuessLog + .1 * distanceGuessL) else { + .45 * distanceGuessC + .5 * distanceGuessLog + (if (pitch > .53) .1 else .05) * distanceGuessL + } + + printDevMessage("Distance guess cubic $distanceGuessC log $distanceGuessLog, weighted $distanceGuess", "griffinguess") + + val trail = BurrowEstimation.lastTrail.asReversed() + + val directionVector = trail[0].subtract(trail[1]).normalize() + printDevMessage("Direction vector $directionVector", "griffinguess") + + val guessPos = trail.last().add( + directionVector * distanceGuess + ) + printDevMessage("Guess pos $guessPos", "griffinguess") + + println("Pitch ${pitch}, Distance: ${ + particleBurrows.keys.minOfOrNull { + trail.last().distanceTo(it.toVec3()) + } + }") + + var y: Int + var x = guessPos.x.toInt() + var z = guessPos.z.toInt() + // offset of 300 blocks for both x and z + // x ranges from 195 to -281 + // z ranges from 207 to -233 + + // TODO: this y thing is wrong and puts them in the air sometimes + do { + y = BurrowEstimation.grassData.getOrNull((x++ % 507) * 507 + (z++ % 495))?.toInt() ?: 0 + } while (y < 2) + val guess = BurrowGuess(guessPos.x.toInt(), y, guessPos.z.toInt()) + BurrowEstimation.guesses[guess] = Instant.now() + + BurrowEstimation.lastTrail.clear() + BurrowEstimation.lastTrailCreated = -1 + BurrowEstimation.firstDistanceGuess = -1.0 + } } @SubscribeEvent(receiveCanceled = true, priority = EventPriority.HIGHEST) @@ -110,24 +175,38 @@ object GriffinBurrows { lastDugParticleBurrow = null BurrowEstimation.guesses.clear() BurrowEstimation.arrows.clear() + BurrowEstimation.lastTrail.clear() + BurrowEstimation.lastTrailCreated = -1 + lastSpadeUse = -1 + BurrowEstimation.firstDistanceGuess = -1.0 } } @SubscribeEvent fun onSendPacket(event: PacketEvent.SendEvent) { - if (!Utils.inSkyblock || !Skytils.config.showGriffinBurrows || mc.theWorld == null || mc.thePlayer == null) return - val pos = - when { - event.packet is C07PacketPlayerDigging && event.packet.status == C07PacketPlayerDigging.Action.START_DESTROY_BLOCK -> { - event.packet.position + if (!Utils.inSkyblock || !Skytils.config.showGriffinBurrows || mc.thePlayer == null || SBInfo.mode != SkyblockIsland.Hub.mode) return + if (mc.thePlayer.heldItem?.isSpade != true) return + + if (event.packet is C08PacketPlayerBlockPlacement && event.packet.position.y == -1) { + lastSpadeUse = System.currentTimeMillis() + BurrowEstimation.lastTrail.clear() + BurrowEstimation.lastTrailCreated = -1 + BurrowEstimation.firstDistanceGuess = -1.0 + printDevMessage("Spade used", "griffinguess") + } else { + val pos = + when { + event.packet is C07PacketPlayerDigging && event.packet.status == C07PacketPlayerDigging.Action.START_DESTROY_BLOCK -> { + event.packet.position + } + event.packet is C08PacketPlayerBlockPlacement && event.packet.stack != null -> event.packet.position + else -> return } - event.packet is C08PacketPlayerBlockPlacement && event.packet.stack != null -> event.packet.position - else -> return + if (mc.theWorld.getBlockState(pos).block !== Blocks.grass) return + particleBurrows[pos]?.blockPos?.let { + printDevMessage("Clicked on $it", "griffin") + lastDugParticleBurrow = it } - if (mc.thePlayer.heldItem?.isSpade != true || mc.theWorld.getBlockState(pos).block !== Blocks.grass) return - particleBurrows[pos]?.blockPos?.let { - printDevMessage("Clicked on $it", "griffin") - lastDugParticleBurrow = it } } @@ -176,41 +255,47 @@ object GriffinBurrows { if (Skytils.config.showGriffinBurrows && hasSpadeInHotbar) { if (SBInfo.mode != SkyblockIsland.Hub.mode) return event.packet.apply { - val type = ParticleType.getParticleType(this) ?: return - val pos = BlockPos(x, y, z).down() - if (recentlyDugParticleBurrows.contains(pos)) return - BurrowEstimation.guesses.keys.associateWith { guess -> - pos.distanceSq( - guess.x.toDouble(), - guess.y.toDouble(), - guess.z.toDouble() - ) - }.minByOrNull { it.value }?.let { (guess, distance) -> - printDevMessage("Nearest guess is $distance blocks away", "griffin", "griffinguess") - if (distance <= 625) { - BurrowEstimation.guesses.remove(guess) + if (type == EnumParticleTypes.DRIP_LAVA && count == 2 && speed == -.5f && xOffset == 0f && yOffset == 0f && zOffset == 0f && isLongDistance) { + BurrowEstimation.lastTrail.add(vec3) + BurrowEstimation.lastTrailCreated = System.currentTimeMillis() + printDevMessage("Found trail point $x $y $z", "griffinguess") + } else { + val type = ParticleType.getParticleType(this) ?: return + val pos = BlockPos(x, y, z).down() + if (recentlyDugParticleBurrows.contains(pos)) return + BurrowEstimation.guesses.keys.associateWith { guess -> + pos.distanceSq( + guess.x.toDouble(), + guess.y.toDouble(), + guess.z.toDouble() + ) + }.minByOrNull { it.value }?.let { (guess, distance) -> + // printDevMessage("Nearest guess is $distance blocks^2 away", "griffin", "griffinguess") + if (distance <= 625) { + BurrowEstimation.guesses.remove(guess) + } } - } - val burrow = particleBurrows.getOrPut(pos) { - ParticleBurrow(pos, hasFootstep = false, hasEnchant = false) - } - if (burrow.type == -1 && type.isBurrowType) { - if (Skytils.config.pingNearbyBurrow) { - SoundQueue.addToQueue("random.orb", 0.8f, 1f, 0, true) + val burrow = particleBurrows.getOrPut(pos) { + ParticleBurrow(pos, hasFootstep = false, hasEnchant = false) + } + if (burrow.type == -1 && type.isBurrowType) { + if (Skytils.config.pingNearbyBurrow) { + SoundQueue.addToQueue("random.orb", 0.8f, 1f, 0, true) + } + } + when (type) { + ParticleType.FOOTSTEP -> burrow.hasFootstep = true + ParticleType.ENCHANT -> burrow.hasEnchant = true + ParticleType.EMPTY -> burrow.type = 0 + ParticleType.MOB -> burrow.type = 1 + ParticleType.TREASURE -> burrow.type = 2 } - } - when (type) { - ParticleType.FOOTSTEP -> burrow.hasFootstep = true - ParticleType.ENCHANT -> burrow.hasEnchant = true - ParticleType.EMPTY -> burrow.type = 0 - ParticleType.MOB -> burrow.type = 1 - ParticleType.TREASURE -> burrow.type = 2 } } } } is S04PacketEntityEquipment -> { - if (!Skytils.config.burrowEstimation || SBInfo.mode != SkyblockIsland.Hub.mode) return + if (!Skytils.config.burrowEstimation || SBInfo.mode != SkyblockIsland.Hub.mode || Skytils.config.experimentBurrowEstimation) return val entity = mc.theWorld?.getEntityByID(event.packet.entityID) (entity as? EntityArmorStand)?.let { armorStand -> if (event.packet.itemStack?.item != Items.arrow) return @@ -229,7 +314,12 @@ object GriffinBurrows { } is S29PacketSoundEffect -> { if (!Skytils.config.burrowEstimation || SBInfo.mode != SkyblockIsland.Hub.mode) return - if (event.packet.soundName != "note.harp") return + if (event.packet.soundName != "note.harp" || event.packet.volume != 1f) return + printDevMessage("Found note harp sound ${event.packet.pitch} ${event.packet.volume} ${event.packet.x} ${event.packet.y} ${event.packet.z}", "griffinguess") + if (BurrowEstimation.firstDistanceGuess == -1.0 && lastSpadeUse != -1L && System.currentTimeMillis() - lastSpadeUse < 1000) { + BurrowEstimation.firstDistanceGuess = event.packet.pitch.toDouble() + } + if (Skytils.config.experimentBurrowEstimation) return val (arrow, distance) = BurrowEstimation.arrows.keys .associateWith { arrow -> arrow.pos.squareDistanceTo(event.packet.x, event.packet.y, event.packet.z) From 720cbea2733d0f6c62a527d38137ffdd8e311a26 Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Thu, 16 May 2024 20:31:32 -0400 Subject: [PATCH 18/46] fix: remove y from guess burrow distance calc --- .../features/impl/events/GriffinBurrows.kt | 24 ++++++++---------- .../assets/skytils/hub_grass_heights.bin | Bin 0 -> 210357 bytes 2 files changed, 10 insertions(+), 14 deletions(-) create mode 100644 src/main/resources/assets/skytils/hub_grass_heights.bin diff --git a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt index 8d13f3c8c..685e044c0 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt @@ -24,6 +24,7 @@ import gg.skytils.skytilsmod.Skytils.Companion.mc import gg.skytils.skytilsmod.core.SoundQueue import gg.skytils.skytilsmod.events.impl.MainReceivePacketEvent import gg.skytils.skytilsmod.events.impl.PacketEvent +import gg.skytils.skytilsmod.features.impl.events.GriffinBurrows.BurrowEstimation.otherGrassData import gg.skytils.skytilsmod.utils.* import net.minecraft.client.renderer.GlStateManager import net.minecraft.entity.item.EntityArmorStand @@ -70,6 +71,10 @@ object GriffinBurrows { this::class.java.getResource("/assets/skytils/grassdata.txt")!!.readBytes() } + val otherGrassData by lazy { + this::class.java.getResource("/assets/skytils/hub_grass_heights.bin")!!.readBytes() + } + class Arrow(val directionVector: Vec3, val pos: Vec3) } @@ -131,18 +136,13 @@ object GriffinBurrows { } }") - var y: Int - var x = guessPos.x.toInt() - var z = guessPos.z.toInt() // offset of 300 blocks for both x and z // x ranges from 195 to -281 // z ranges from 207 to -233 - // TODO: this y thing is wrong and puts them in the air sometimes - do { - y = BurrowEstimation.grassData.getOrNull((x++ % 507) * 507 + (z++ % 495))?.toInt() ?: 0 - } while (y < 2) - val guess = BurrowGuess(guessPos.x.toInt(), y, guessPos.z.toInt()) + fun getIndex(x: Int, z: Int) = (x - -281) + (z - -233) * (195 - -281 + 1) + + val guess = BurrowGuess(guessPos.x.toInt(), otherGrassData.getOrNull(getIndex(guessPos.x.toInt(), guessPos.z.toInt()))?.toInt() ?: 0, guessPos.z.toInt()) BurrowEstimation.guesses[guess] = Instant.now() BurrowEstimation.lastTrail.clear() @@ -264,14 +264,10 @@ object GriffinBurrows { val pos = BlockPos(x, y, z).down() if (recentlyDugParticleBurrows.contains(pos)) return BurrowEstimation.guesses.keys.associateWith { guess -> - pos.distanceSq( - guess.x.toDouble(), - guess.y.toDouble(), - guess.z.toDouble() - ) + (pos.x - guess.x) * (pos.x - guess.x) + (pos.z - guess.z) * (pos.z - guess.z) }.minByOrNull { it.value }?.let { (guess, distance) -> // printDevMessage("Nearest guess is $distance blocks^2 away", "griffin", "griffinguess") - if (distance <= 625) { + if (distance <= 25 * 25) { BurrowEstimation.guesses.remove(guess) } } diff --git a/src/main/resources/assets/skytils/hub_grass_heights.bin b/src/main/resources/assets/skytils/hub_grass_heights.bin new file mode 100644 index 0000000000000000000000000000000000000000..ebc85f8ee31c0c69e31b6d622f712ed1daee8ba4 GIT binary patch literal 210357 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 literal 0 HcmV?d00001 From 1e0a37cc7972fae4f3a4af5f393b6c5c35183443 Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Fri, 17 May 2024 01:56:25 -0400 Subject: [PATCH 19/46] fix: explicitly mark AccessorCommandHandler methods as Mutable --- .../mixins/transformers/accessors/AccessorCommandHandler.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/gg/skytils/skytilsmod/mixins/transformers/accessors/AccessorCommandHandler.java b/src/main/java/gg/skytils/skytilsmod/mixins/transformers/accessors/AccessorCommandHandler.java index 8cbc2482c..8dd6711a3 100644 --- a/src/main/java/gg/skytils/skytilsmod/mixins/transformers/accessors/AccessorCommandHandler.java +++ b/src/main/java/gg/skytils/skytilsmod/mixins/transformers/accessors/AccessorCommandHandler.java @@ -21,6 +21,7 @@ import net.minecraft.command.CommandHandler; import net.minecraft.command.ICommand; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; import org.spongepowered.asm.mixin.gen.Accessor; import java.util.Map; @@ -31,12 +32,14 @@ public interface AccessorCommandHandler { @Accessor Set getCommandSet(); + @Mutable @Accessor void setCommandSet(Set set); @Accessor Map getCommandMap(); + @Mutable @Accessor void setCommandMap(Map map); From 856ded911e61ed8a794b2a2b878fc77eb5fbbfb6 Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Fri, 17 May 2024 04:15:38 -0400 Subject: [PATCH 20/46] feat: add GPG verification to UpdateChecker --- build.gradle.kts | 4 + .../skytilsmod/tweaker/DependencyLoader.java | 14 ++- .../skytilsmod/gui/updater/UpdateGui.kt | 85 ++++++++++++------ .../assets/skytils/my-name-is-jeff.gpg | 64 ++++++++++++++ src/main/resources/assets/skytils/sychic.gpg | 86 +++++++++++++++++++ 5 files changed, 228 insertions(+), 25 deletions(-) create mode 100644 src/main/resources/assets/skytils/my-name-is-jeff.gpg create mode 100644 src/main/resources/assets/skytils/sychic.gpg diff --git a/build.gradle.kts b/build.gradle.kts index b61e6eb87..90680cde0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -147,6 +147,10 @@ dependencies { shadowMe(project(":events")) shadowMe(project(":hypixel-api:types")) + shadowMe("org.bouncycastle:bcpg-jdk18on:1.78.1") { + exclude(module = "bcprov-jdk18on") + } + compileOnly("org.bouncycastle:bcprov-jdk18on:1.78.1") shadowMe(annotationProcessor("io.github.llamalad7:mixinextras-common:0.3.5")!!) annotationProcessor("org.spongepowered:mixin:0.8.5:processor") diff --git a/src/main/java/gg/skytils/skytilsmod/tweaker/DependencyLoader.java b/src/main/java/gg/skytils/skytilsmod/tweaker/DependencyLoader.java index ed8664cb1..272cc4699 100644 --- a/src/main/java/gg/skytils/skytilsmod/tweaker/DependencyLoader.java +++ b/src/main/java/gg/skytils/skytilsmod/tweaker/DependencyLoader.java @@ -26,6 +26,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; +import java.security.Security; import static gg.skytils.skytilsmod.tweaker.TweakerUtil.addToClasspath; @@ -36,6 +37,7 @@ public class DependencyLoader { public static void loadDependencies() { loadBrotli(); + if (Security.getProvider("BC") == null) loadBCProv(); } public static File loadDependency(String path) throws Throwable { @@ -49,13 +51,23 @@ public static File loadDependency(String path) throws Throwable { } } - System.out.println("Brotli size: " + Files.size(downloadPath)); + System.out.printf("Dependency size for %s: %s%n", path.substring(path.lastIndexOf('/') + 1), Files.size(downloadPath)); addToClasspath(downloadLocation.toURI().toURL()); return downloadLocation; } + public static void loadBCProv() { + try { + loadDependency("org/bouncycastle/bcprov-jdk18on/1.78.1/bcprov-jdk18on-1.78.1.jar"); + System.out.println("Bouncy Castle provider loaded"); + } catch (Throwable t) { + System.out.println("Failed to load Bouncy Castle providers"); + t.printStackTrace(); + } + } + public static void loadBrotli() { if (System.getProperty("skytils.noNativeBrotli") != null) { System.out.println("Native Brotli disabled by system property"); diff --git a/src/main/kotlin/gg/skytils/skytilsmod/gui/updater/UpdateGui.kt b/src/main/kotlin/gg/skytils/skytilsmod/gui/updater/UpdateGui.kt index a5b5d75ec..5132c3391 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/gui/updater/UpdateGui.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/gui/updater/UpdateGui.kt @@ -32,8 +32,15 @@ import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.launch import net.minecraft.client.gui.GuiButton import net.minecraft.client.gui.GuiScreen -import net.minecraft.util.EnumChatFormatting +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection +import org.bouncycastle.openpgp.PGPSignatureList +import org.bouncycastle.openpgp.PGPUtil +import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory +import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider import java.io.File +import java.security.Security import kotlin.math.floor /** @@ -46,12 +53,14 @@ class UpdateGui(restartNow: Boolean) : GuiScreen() { companion object { private val DOTS = arrayOf(".", "..", "...", "...", "...") private const val DOT_TIME = 200 // ms between "." -> ".." -> "..." - var failed = false var complete = false } private var backButton: GuiButton? = null private var progress = 0.0 + private var stage = "Downloading" + var failed = false + override fun initGui() { buttonList.add(GuiButton(0, width / 2 - 100, height / 3 * 2, 200, 20, "").also { backButton = it }) updateText() @@ -63,14 +72,48 @@ class UpdateGui(restartNow: Boolean) : GuiScreen() { val url = UpdateChecker.updateDownloadURL val jarName = UpdateChecker.getJarNameFromUrl(url) IO.launch(CoroutineName("Skytils-update-downloader-thread")) { - downloadUpdate(url, directory) + val updateFile = downloadUpdate(url, directory) + val signFile = downloadUpdate("$url.asc", directory) if (!failed) { - UpdateChecker.scheduleCopyUpdateAtShutdown(jarName) - if (restartNow) { - mc.shutdown() + if (updateFile != null && signFile != null) { + stage = "Verifying signature" + val finger = JcaKeyFingerprintCalculator() + + fun getKeyRingCollection(fileName: String): PGPPublicKeyRingCollection = + this::class.java.classLoader.getResourceAsStream("assets/skytils/$fileName.gpg")!!.use { + PGPPublicKeyRingCollection(PGPUtil.getDecoderStream(it), finger) + } + + val keys = listOf( + getKeyRingCollection("my-name-is-jeff"), + getKeyRingCollection("sychic") + ) + + val sig = (JcaPGPObjectFactory(PGPUtil.getDecoderStream(signFile.inputStream())).nextObject() as PGPSignatureList).first() + val key = keys.firstNotNullOfOrNull { it.getPublicKey(sig.keyID) } + if (key != null) { + sig.init(JcaPGPContentVerifierBuilderProvider().setProvider(Security.getProvider("BC") ?: BouncyCastleProvider().also(Security::addProvider)), key) + sig.update(updateFile.readBytes()) + if (sig.verify()) { + signFile.deleteOnExit() + UpdateChecker.scheduleCopyUpdateAtShutdown(jarName) + if (restartNow) { + mc.shutdown() + } + complete = true + updateText() + } else { + failed = true + println("Signature verification failed") + } + } else { + println("Key not found") + failed = true + } + } else { + println("Files are missing") + failed = true } - complete = true - updateText() } } } catch (ex: Exception) { @@ -82,7 +125,7 @@ class UpdateGui(restartNow: Boolean) : GuiScreen() { backButton!!.displayString = if (failed || complete) "Back" else "Cancel" } - private suspend fun downloadUpdate(urlString: String, directory: File) { + private suspend fun downloadUpdate(urlString: String, directory: File): File? { try { val url = Url(urlString) @@ -102,25 +145,27 @@ class UpdateGui(restartNow: Boolean) : GuiScreen() { failed = true updateText() println("$url returned status code ${st.status}") - return + return null } if (!directory.exists() && !directory.mkdirs()) { failed = true updateText() println("Couldn't create update file directory") - return + return null } val fileSaved = File(directory, url.pathSegments.last().decodeURLPart()) if (mc.currentScreen !== this@UpdateGui || st.bodyAsChannel().copyTo(fileSaved.writeChannel()) == 0L) { failed = true - return + return null } println("Downloaded update to $fileSaved") + return fileSaved } catch (ex: Exception) { ex.printStackTrace() failed = true updateText() } + return null } public override fun actionPerformed(button: GuiButton) { @@ -134,14 +179,14 @@ class UpdateGui(restartNow: Boolean) : GuiScreen() { when { failed -> drawCenteredString( mc.fontRendererObj, - EnumChatFormatting.RED.toString() + "Update download failed", + "§cUpdate download failed", width / 2, height / 2, -0x1 ) complete -> drawCenteredString( mc.fontRendererObj, - EnumChatFormatting.GREEN.toString() + "Update download complete", + "§aUpdate download complete", width / 2, height / 2, 0xFFFFFF @@ -162,16 +207,8 @@ class UpdateGui(restartNow: Boolean) : GuiScreen() { top + 3, -0x1000000 ) - val x = (width - mc.fontRendererObj.getStringWidth( - String.format( - "Downloading %s", - DOTS[DOTS.size - 1] - ) - )) / 2 - val title = String.format( - "Downloading %s", - DOTS[(System.currentTimeMillis() % (DOT_TIME * DOTS.size)).toInt() / DOT_TIME] - ) + val x = (width - mc.fontRendererObj.getStringWidth("$stage ${DOTS[DOTS.size - 1]}")) / 2 + val title = "$stage ${DOTS[(System.currentTimeMillis() % (DOT_TIME * DOTS.size)).toInt() / DOT_TIME]}" drawString(mc.fontRendererObj, title, x, top - mc.fontRendererObj.FONT_HEIGHT - 2, -0x1) } } diff --git a/src/main/resources/assets/skytils/my-name-is-jeff.gpg b/src/main/resources/assets/skytils/my-name-is-jeff.gpg new file mode 100644 index 000000000..7c27ea91c --- /dev/null +++ b/src/main/resources/assets/skytils/my-name-is-jeff.gpg @@ -0,0 +1,64 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=Nkmk +-----END PGP PUBLIC KEY BLOCK----- \ No newline at end of file diff --git a/src/main/resources/assets/skytils/sychic.gpg b/src/main/resources/assets/skytils/sychic.gpg new file mode 100644 index 000000000..ed38060dd --- /dev/null +++ b/src/main/resources/assets/skytils/sychic.gpg @@ -0,0 +1,86 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +LBxoCg== +=WLv5 +-----END PGP PUBLIC KEY BLOCK----- \ No newline at end of file From d692ed98da558ea2d7f83ef8f199b522a7bd11f7 Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Fri, 17 May 2024 18:09:34 -0400 Subject: [PATCH 21/46] feat: implement the e/pitch slope method for distance using stat --- .../features/impl/events/GriffinBurrows.kt | 83 ++++++++++--------- 1 file changed, 44 insertions(+), 39 deletions(-) diff --git a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt index 685e044c0..4c3617640 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/events/GriffinBurrows.kt @@ -18,12 +18,15 @@ package gg.skytils.skytilsmod.features.impl.events import com.google.common.collect.EvictingQueue +import gg.essential.universal.UChat import gg.essential.universal.UMatrixStack import gg.skytils.skytilsmod.Skytils import gg.skytils.skytilsmod.Skytils.Companion.mc import gg.skytils.skytilsmod.core.SoundQueue import gg.skytils.skytilsmod.events.impl.MainReceivePacketEvent import gg.skytils.skytilsmod.events.impl.PacketEvent +import gg.skytils.skytilsmod.features.impl.events.GriffinBurrows.BurrowEstimation.lastParticleTrail +import gg.skytils.skytilsmod.features.impl.events.GriffinBurrows.BurrowEstimation.lastSoundTrail import gg.skytils.skytilsmod.features.impl.events.GriffinBurrows.BurrowEstimation.otherGrassData import gg.skytils.skytilsmod.utils.* import net.minecraft.client.renderer.GlStateManager @@ -60,9 +63,9 @@ object GriffinBurrows { object BurrowEstimation { val arrows = mutableMapOf() val guesses = mutableMapOf() - val lastTrail = mutableListOf() + val lastParticleTrail = mutableListOf() + val lastSoundTrail = linkedSetOf>() var lastTrailCreated = -1L - var firstDistanceGuess = 0.0 fun getDistanceFromPitch(pitch: Double) = 2805 * pitch - 1375 @@ -92,35 +95,43 @@ object GriffinBurrows { Duration.between(instant, Instant.now()).toMillis() > 30_000L } if (!Skytils.config.experimentBurrowEstimation) return - if (BurrowEstimation.firstDistanceGuess != -1.0 && BurrowEstimation.lastTrail.size >= 2 && System.currentTimeMillis() - BurrowEstimation.lastTrailCreated > 1000) { - printDevMessage("Trail found ${BurrowEstimation.lastTrail}", "griffinguess") - printDevMessage("Pitch ${BurrowEstimation.firstDistanceGuess}", "griffinguess") - - val pitch = BurrowEstimation.firstDistanceGuess - - fun cubicFunc(x: Double): Double { - return -355504.3762671333 * x.pow(3) + 573210.5260410917 * x.pow(2) - 304839.7095941929 * x + 53581.7430868503 + if (lastSoundTrail.size >= 2 && lastParticleTrail.size >= 2 && System.currentTimeMillis() - BurrowEstimation.lastTrailCreated > 1000) { + printDevMessage("Trail found $lastParticleTrail", "griffinguess") + printDevMessage("Sound trail $lastSoundTrail", "griffinguess") + + // chat did I get a 5 on the exam? + // https://apcentral.collegeboard.org/media/pdf/statistics-formula-sheet-and-tables-2020.pdf + val pitches = lastSoundTrail.map { it.second } + val xMean = (lastSoundTrail.size - 1) / 2.0 + val xStd = sqrt(lastSoundTrail.indices.sumOf { + (it - xMean) * (it - xMean) + }) / (lastSoundTrail.size - 1) + + val yMean = pitches.average() + val yStd = sqrt(pitches.sumOf { + (it - yMean) * (it - yMean) + }) / (pitches.size - 1) + + val numerator = lastSoundTrail.withIndex().sumOf { (i, pair) -> + (i - xMean) * (pair.second - yMean) } - fun logFunc(x: Double): Double { - return 1018.3988994912 + 3301.6178248206 * log10(x) - } + val denominatorX = sqrt(lastSoundTrail.indices.sumOf { (it - xMean) * (it - xMean) }) + val denominatorY = sqrt(pitches.sumOf { (it - yMean) * (it - yMean) }) - fun linearFunc(x: Double): Double { - return 2760.6568614981 * x - 1355.0749724848 - } + val r = numerator / (denominatorX * denominatorY) - val distanceGuessLog = logFunc(pitch) - val distanceGuessC = cubicFunc(pitch) - val distanceGuessL = linearFunc(pitch) + val slope = r * yStd / xStd - val distanceGuess = if (pitch < 0.5) distanceGuessC else if (pitch > .6) (distanceGuessLog + .1 * distanceGuessL) else { - .45 * distanceGuessC + .5 * distanceGuessLog + (if (pitch > .53) .1 else .05) * distanceGuessL - } + if (r < 0.95) UChat.chat("${Skytils.failPrefix} §cWarning: low correlation, r = $r. Burrow guess may be incorrect.") + + printDevMessage("Slope $slope, xbar $xMean, sx $xStd, ybar $yMean, sy $yStd, r $r", "griffinguess") - printDevMessage("Distance guess cubic $distanceGuessC log $distanceGuessLog, weighted $distanceGuess", "griffinguess") + val trail = lastParticleTrail.asReversed() - val trail = BurrowEstimation.lastTrail.asReversed() + // formula for distance guess comes from soopyboo32 + val distanceGuess = E / slope + printDevMessage("Distance guess $distanceGuess", "griffinguess") val directionVector = trail[0].subtract(trail[1]).normalize() printDevMessage("Direction vector $directionVector", "griffinguess") @@ -130,12 +141,6 @@ object GriffinBurrows { ) printDevMessage("Guess pos $guessPos", "griffinguess") - println("Pitch ${pitch}, Distance: ${ - particleBurrows.keys.minOfOrNull { - trail.last().distanceTo(it.toVec3()) - } - }") - // offset of 300 blocks for both x and z // x ranges from 195 to -281 // z ranges from 207 to -233 @@ -145,9 +150,9 @@ object GriffinBurrows { val guess = BurrowGuess(guessPos.x.toInt(), otherGrassData.getOrNull(getIndex(guessPos.x.toInt(), guessPos.z.toInt()))?.toInt() ?: 0, guessPos.z.toInt()) BurrowEstimation.guesses[guess] = Instant.now() - BurrowEstimation.lastTrail.clear() + lastParticleTrail.clear() BurrowEstimation.lastTrailCreated = -1 - BurrowEstimation.firstDistanceGuess = -1.0 + lastSoundTrail.clear() } } @@ -175,10 +180,10 @@ object GriffinBurrows { lastDugParticleBurrow = null BurrowEstimation.guesses.clear() BurrowEstimation.arrows.clear() - BurrowEstimation.lastTrail.clear() + lastParticleTrail.clear() BurrowEstimation.lastTrailCreated = -1 lastSpadeUse = -1 - BurrowEstimation.firstDistanceGuess = -1.0 + lastSoundTrail.clear() } } @@ -189,9 +194,9 @@ object GriffinBurrows { if (event.packet is C08PacketPlayerBlockPlacement && event.packet.position.y == -1) { lastSpadeUse = System.currentTimeMillis() - BurrowEstimation.lastTrail.clear() + lastParticleTrail.clear() BurrowEstimation.lastTrailCreated = -1 - BurrowEstimation.firstDistanceGuess = -1.0 + lastSoundTrail.clear() printDevMessage("Spade used", "griffinguess") } else { val pos = @@ -256,7 +261,7 @@ object GriffinBurrows { if (SBInfo.mode != SkyblockIsland.Hub.mode) return event.packet.apply { if (type == EnumParticleTypes.DRIP_LAVA && count == 2 && speed == -.5f && xOffset == 0f && yOffset == 0f && zOffset == 0f && isLongDistance) { - BurrowEstimation.lastTrail.add(vec3) + lastParticleTrail.add(vec3) BurrowEstimation.lastTrailCreated = System.currentTimeMillis() printDevMessage("Found trail point $x $y $z", "griffinguess") } else { @@ -312,8 +317,8 @@ object GriffinBurrows { if (!Skytils.config.burrowEstimation || SBInfo.mode != SkyblockIsland.Hub.mode) return if (event.packet.soundName != "note.harp" || event.packet.volume != 1f) return printDevMessage("Found note harp sound ${event.packet.pitch} ${event.packet.volume} ${event.packet.x} ${event.packet.y} ${event.packet.z}", "griffinguess") - if (BurrowEstimation.firstDistanceGuess == -1.0 && lastSpadeUse != -1L && System.currentTimeMillis() - lastSpadeUse < 1000) { - BurrowEstimation.firstDistanceGuess = event.packet.pitch.toDouble() + if (lastSpadeUse != -1L && System.currentTimeMillis() - lastSpadeUse < 1000) { + lastSoundTrail.add(Vec3(event.packet.x, event.packet.y, event.packet.z) to event.packet.pitch.toDouble()) } if (Skytils.config.experimentBurrowEstimation) return val (arrow, distance) = BurrowEstimation.arrows.keys From 3c73d6b3f4e9c45db9f3f3fcebf71df5e4eab86a Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Fri, 17 May 2024 18:51:10 -0400 Subject: [PATCH 22/46] fix: move where dungeon classes are cached in DungeonListener --- .../skytilsmod/listeners/DungeonListener.kt | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/gg/skytils/skytilsmod/listeners/DungeonListener.kt b/src/main/kotlin/gg/skytils/skytilsmod/listeners/DungeonListener.kt index 29cf4228d..a0bced94d 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/listeners/DungeonListener.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/listeners/DungeonListener.kt @@ -53,6 +53,7 @@ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent object DungeonListener { val team = hashMapOf() + private val teamCached = hashMapOf>() val deads = hashSetOf() val disconnected = hashSetOf() val missingPuzzles = hashSetOf() @@ -106,6 +107,7 @@ object DungeonListener { disconnected.clear() missingPuzzles.clear() completedPuzzles.clear() + teamCached.clear() } @SubscribeEvent @@ -236,20 +238,22 @@ object DungeonListener { if (Utils.inDungeons && mc.thePlayer != null && (DungeonTimer.scoreShownAt == -1L || System.currentTimeMillis() - DungeonTimer.scoreShownAt < 1500)) { val tabEntries = TabListUtils.tabEntries var partyCount: Int? = null - var oldTeam: Map? = null - if (tabEntries.isNotEmpty() && tabEntries[0].second.contains("§r§b§lParty §r§f(")) { partyCount = partyCountPattern.find(tabEntries[0].second)?.groupValues?.get(1)?.toIntOrNull() if (partyCount != null) { // we can just keep disconnected players here i think if (team.size != partyCount) { println("Recomputing team as party size has changed ${team.size} -> $partyCount") - oldTeam = team.clone() as Map + team.values.filter { it.dungeonClass != DungeonClass.EMPTY }.forEach { + teamCached[it.playerName] = it.dungeonClass to it.classLevel + } team.clear() } else if (team.size > 5) { UChat.chat("$failPrefix §cSomething isn't right! I got more than 5 members. Expected $partyCount members but got ${team.size}") println("Got more than 5 players on the team??") - oldTeam = team.clone() as Map + team.values.filter { it.dungeonClass != DungeonClass.EMPTY }.forEach { + teamCached[it.playerName] = it.dungeonClass to it.classLevel + } team.clear() } } else { @@ -295,10 +299,15 @@ object DungeonListener { ) } else { println("Parsed teammate $name with value EMPTY, $text") - if (oldTeam != null && name in oldTeam) { - val old = oldTeam[name]!! - team[name] = old.copy(tabEntryIndex = pos) - println("Got old teammate $name with value EMPTY, using $old instead") + if (name in teamCached) { + val cache = teamCached[name]!! + team[name] = DungeonTeammate( + name, + cache.first, cache.second, + pos, + entry.locationSkin + ) + println("Got old teammate $name with value EMPTY, using values from cache instead ${cache}") } else { team[name] = DungeonTeammate( name, From d4c47a33db18187fd94cf7ecd297606500d4145e Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Fri, 17 May 2024 18:21:43 -0400 Subject: [PATCH 23/46] version: 1.9.6 --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 90680cde0..000e6bf81 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,7 +34,7 @@ plugins { signing } -version = "1.9.5" +version = "1.9.6" group = "gg.skytils" repositories { From 0a812988e610c0e328318ddd39eaf1a7e692c07d Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Sat, 18 May 2024 01:09:24 -0400 Subject: [PATCH 24/46] fix: Catlas never draws own marker pov: you fell off as a developer --- .../features/impl/dungeons/catlas/core/CatlasElement.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/dungeons/catlas/core/CatlasElement.kt b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/dungeons/catlas/core/CatlasElement.kt index 9e882c8fa..293aa2498 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/dungeons/catlas/core/CatlasElement.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/dungeons/catlas/core/CatlasElement.kt @@ -243,7 +243,7 @@ object CatlasElement : GuiElement(name = "Dungeon Map", x = 0, y = 0) { private fun renderPlayerHeads() { if (DungeonTimer.bossEntryTime != -1L) return DungeonListener.team.forEach { (name, teammate) -> - if (!teammate.dead && !teammate.mapPlayer.isOurMarker) { + if (!teammate.dead || teammate.mapPlayer.isOurMarker) { RenderUtils.drawPlayerHead(name, teammate.mapPlayer) } } From 1c217ed1e2bb4872c266314196fea451d0892778 Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Sat, 18 May 2024 01:10:22 -0400 Subject: [PATCH 25/46] version: 1.9.7 --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 000e6bf81..64e708aeb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,7 +34,7 @@ plugins { signing } -version = "1.9.6" +version = "1.9.7" group = "gg.skytils" repositories { From 269416f70cb85a393596363200073877fe7b3120 Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Thu, 11 Apr 2024 16:08:46 -0400 Subject: [PATCH 26/46] feat: add hypixel payload api chore: bump hypixel mod-api to 0.1.7 chore: upgrade mod-api to 0.2.1 --- build.gradle.kts | 4 ++ .../kotlin/gg/skytils/skytilsmod/Skytils.kt | 32 ++++++++- .../events/impl/HypixelPacketEvent.kt | 45 +++++++++++++ .../listeners/ServerPayloadInterceptor.kt | 66 +++++++++++++++++++ 4 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/gg/skytils/skytilsmod/events/impl/HypixelPacketEvent.kt create mode 100644 src/main/kotlin/gg/skytils/skytilsmod/listeners/ServerPayloadInterceptor.kt diff --git a/build.gradle.kts b/build.gradle.kts index 64e708aeb..0de006008 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -42,6 +42,7 @@ repositories { mavenCentral() maven("https://repo.sk1er.club/repository/maven-public/") maven("https://repo.sk1er.club/repository/maven-releases/") + maven("https://repo.hypixel.net/repository/Hypixel/") maven("https://jitpack.io") { mavenContent { includeGroupAndSubgroups("com.github") @@ -151,6 +152,8 @@ dependencies { exclude(module = "bcprov-jdk18on") } compileOnly("org.bouncycastle:bcprov-jdk18on:1.78.1") + shadowMe("net.hypixel:mod-api:0.2.1") + shadowMe(annotationProcessor("io.github.llamalad7:mixinextras-common:0.3.5")!!) annotationProcessor("org.spongepowered:mixin:0.8.5:processor") @@ -217,6 +220,7 @@ tasks { relocate("kotlinx.serialization", "gg.skytils.ktx-serialization") relocate("kotlinx.coroutines", "gg.skytils.ktx-coroutines") relocate("gg.essential.vigilance", "gg.skytils.vigilance") + relocate("net.hypixel", "gg.skytils.hypixel-net") exclude( "**/LICENSE_MixinExtras", diff --git a/src/main/kotlin/gg/skytils/skytilsmod/Skytils.kt b/src/main/kotlin/gg/skytils/skytilsmod/Skytils.kt index 803584951..74e49122f 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/Skytils.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/Skytils.kt @@ -26,6 +26,7 @@ import gg.skytils.skytilsmod.commands.stats.impl.CataCommand import gg.skytils.skytilsmod.commands.stats.impl.SlayerCommand import gg.skytils.skytilsmod.core.* import gg.skytils.skytilsmod.tweaker.DependencyLoader +import gg.skytils.skytilsmod.events.impl.HypixelPacketEvent import gg.skytils.skytilsmod.events.impl.MainReceivePacketEvent import gg.skytils.skytilsmod.events.impl.PacketEvent import gg.skytils.skytilsmod.features.impl.crimson.KuudraChestProfit @@ -62,6 +63,8 @@ import gg.skytils.skytilsmod.gui.OptionsGui import gg.skytils.skytilsmod.gui.ReopenableGUI import gg.skytils.skytilsmod.listeners.ChatListener import gg.skytils.skytilsmod.listeners.DungeonListener +import gg.skytils.skytilsmod.listeners.ServerPayloadInterceptor +import gg.skytils.skytilsmod.listeners.ServerPayloadInterceptor.toCustomPayload import gg.skytils.skytilsmod.localapi.LocalAPI import gg.skytils.skytilsmod.mixins.extensions.ExtensionEntityLivingBase import gg.skytils.skytilsmod.mixins.hooks.entity.EntityPlayerSPHook @@ -83,11 +86,14 @@ import io.ktor.serialization.kotlinx.json.* import kotlinx.coroutines.* import kotlinx.serialization.json.Json import kotlinx.serialization.modules.SerializersModule +import net.hypixel.modapi.packet.impl.clientbound.ClientboundPingPacket +import net.hypixel.modapi.packet.impl.serverbound.ServerboundPingPacket import net.minecraft.client.Minecraft import net.minecraft.client.gui.GuiButton import net.minecraft.client.gui.GuiGameOver import net.minecraft.client.gui.GuiIngameMenu import net.minecraft.client.gui.GuiScreen +import net.minecraft.client.network.NetHandlerPlayClient import net.minecraft.client.settings.KeyBinding import net.minecraft.inventory.ContainerChest import net.minecraft.launchwrapper.Launch @@ -289,6 +295,7 @@ class Skytils { LocalAPI, MayorInfo, SBInfo, + ServerPayloadInterceptor, SoundQueue, UpdateChecker, @@ -531,11 +538,27 @@ class Skytils { ?: currentServerData?.serverIP?.lowercase()?.contains("hypixel") ?: false) }.onFailure { it.printStackTrace() }.getOrDefault(false) + if (Utils.isOnHypixel) { + onJoinHypixel(event.handler as NetHandlerPlayClient) + } + IO.launch { TrophyFish.loadFromApi() } } + @SubscribeEvent + fun onHypixelPacket(event: HypixelPacketEvent.ReceiveEvent) { + if (event.packet is ClientboundPingPacket) { + println("${event.packet.response} ${event.packet.version}") + } + } + + @SubscribeEvent + fun onHypixelPacketFail(event: HypixelPacketEvent.FailedEvent) { + UChat.chat("$failPrefix Mod API request failed: ${event.reason}") + } + @SubscribeEvent(priority = EventPriority.HIGHEST) fun onPacket(event: MainReceivePacketEvent<*, *>) { if (event.packet is S01PacketJoinGame) { @@ -556,8 +579,11 @@ class Skytils { } } if (!Utils.isOnHypixel && event.packet is S3FPacketCustomPayload && event.packet.channelName == "MC|Brand") { - if (event.packet.bufferData.readStringFromBuffer(Short.MAX_VALUE.toInt()).lowercase().contains("hypixel")) + val brand = event.packet.bufferData.readStringFromBuffer(Short.MAX_VALUE.toInt()) + if (brand.lowercase().contains("hypixel")) { Utils.isOnHypixel = true + onJoinHypixel(event.handler as NetHandlerPlayClient) + } } if (Utils.inDungeons || !Utils.isOnHypixel || event.packet !is S38PacketPlayerListItem || (event.packet.action != S38PacketPlayerListItem.Action.UPDATE_DISPLAY_NAME && @@ -575,6 +601,10 @@ class Skytils { } } + fun onJoinHypixel(handler: NetHandlerPlayClient) { + handler.addToSendQueue(ServerboundPingPacket().toCustomPayload()) + } + @SubscribeEvent fun onDisconnect(event: FMLNetworkEvent.ClientDisconnectionFromServerEvent) { Utils.isOnHypixel = false diff --git a/src/main/kotlin/gg/skytils/skytilsmod/events/impl/HypixelPacketEvent.kt b/src/main/kotlin/gg/skytils/skytilsmod/events/impl/HypixelPacketEvent.kt new file mode 100644 index 000000000..e64a7b125 --- /dev/null +++ b/src/main/kotlin/gg/skytils/skytilsmod/events/impl/HypixelPacketEvent.kt @@ -0,0 +1,45 @@ +/* + * Skytils - Hypixel Skyblock Quality of Life Mod + * Copyright (C) 2020-2023 Skytils + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package gg.skytils.skytilsmod.events.impl + +import gg.skytils.skytilsmod.events.SkytilsEvent +import net.hypixel.modapi.error.ErrorReason +import net.hypixel.modapi.packet.HypixelPacket +import net.hypixel.modapi.packet.HypixelPacketType +import net.minecraftforge.fml.common.eventhandler.Cancelable + +@Cancelable +abstract class HypixelPacketEvent : SkytilsEvent() { + abstract val direction: Direction + + class ReceiveEvent(val packet: HypixelPacket) : HypixelPacketEvent() { + override val direction: Direction = Direction.INBOUND + } + + class SendEvent(val type: HypixelPacketType) : HypixelPacketEvent() { + override val direction: Direction = Direction.OUTBOUND + } + + class FailedEvent(val type: HypixelPacketType, val reason: ErrorReason) : HypixelPacketEvent() { + override val direction: Direction = Direction.OUTBOUND + } + + enum class Direction { + INBOUND, OUTBOUND + } +} \ No newline at end of file diff --git a/src/main/kotlin/gg/skytils/skytilsmod/listeners/ServerPayloadInterceptor.kt b/src/main/kotlin/gg/skytils/skytilsmod/listeners/ServerPayloadInterceptor.kt new file mode 100644 index 000000000..401e77772 --- /dev/null +++ b/src/main/kotlin/gg/skytils/skytilsmod/listeners/ServerPayloadInterceptor.kt @@ -0,0 +1,66 @@ +/* + * Skytils - Hypixel Skyblock Quality of Life Mod + * Copyright (C) 2020-2024 Skytils + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package gg.skytils.skytilsmod.listeners + +import gg.skytils.skytilsmod.events.impl.HypixelPacketEvent +import gg.skytils.skytilsmod.events.impl.PacketEvent +import io.netty.buffer.Unpooled +import net.hypixel.modapi.error.ErrorReason +import net.hypixel.modapi.packet.HypixelPacket +import net.hypixel.modapi.packet.HypixelPacketType +import net.hypixel.modapi.serializer.PacketSerializer +import net.minecraft.network.PacketBuffer +import net.minecraft.network.play.client.C17PacketCustomPayload +import net.minecraft.network.play.server.S3FPacketCustomPayload +import net.minecraftforge.fml.common.eventhandler.EventPriority +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent + +object ServerPayloadInterceptor { + @SubscribeEvent(priority = EventPriority.HIGHEST) + fun onReceivePacket(event: PacketEvent.ReceiveEvent) { + if (event.packet is S3FPacketCustomPayload) { + HypixelPacketType.getByIdentifier(event.packet.channelName)?.let { pt -> + val packetSerializer = PacketSerializer(event.packet.bufferData.duplicate()) + if (!packetSerializer.readBoolean()) { + val reason = ErrorReason.getById(packetSerializer.readVarInt()) + HypixelPacketEvent.FailedEvent(pt, reason).postAndCatch() + } else { + val packet = pt.packetFactory.apply(packetSerializer) + HypixelPacketEvent.ReceiveEvent(packet).postAndCatch() + } + } + } + } + + @SubscribeEvent(priority = EventPriority.HIGHEST) + fun onSendPacket(event: PacketEvent.SendEvent) { + if (event.packet is C17PacketCustomPayload) { + HypixelPacketType.getByIdentifier(event.packet.channelName)?.let { pt -> + HypixelPacketEvent.SendEvent(pt).postAndCatch() + } + } + } + + fun HypixelPacket.toCustomPayload(): C17PacketCustomPayload { + val buffer = PacketBuffer(Unpooled.buffer()) + val serializer = PacketSerializer(buffer) + this.write(serializer) + return C17PacketCustomPayload(type.identifier, buffer) + } +} \ No newline at end of file From d78a4803375c5ebf978cf74cd97293fc106cbacc Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Thu, 11 Apr 2024 16:33:27 -0400 Subject: [PATCH 27/46] feat: add coroutine awaiting for Hypixel packets --- .../kotlin/gg/skytils/skytilsmod/Skytils.kt | 15 +++----- .../listeners/ServerPayloadInterceptor.kt | 35 ++++++++++++++----- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/src/main/kotlin/gg/skytils/skytilsmod/Skytils.kt b/src/main/kotlin/gg/skytils/skytilsmod/Skytils.kt index 74e49122f..aa714ae92 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/Skytils.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/Skytils.kt @@ -64,7 +64,7 @@ import gg.skytils.skytilsmod.gui.ReopenableGUI import gg.skytils.skytilsmod.listeners.ChatListener import gg.skytils.skytilsmod.listeners.DungeonListener import gg.skytils.skytilsmod.listeners.ServerPayloadInterceptor -import gg.skytils.skytilsmod.listeners.ServerPayloadInterceptor.toCustomPayload +import gg.skytils.skytilsmod.listeners.ServerPayloadInterceptor.getResponse import gg.skytils.skytilsmod.localapi.LocalAPI import gg.skytils.skytilsmod.mixins.extensions.ExtensionEntityLivingBase import gg.skytils.skytilsmod.mixins.hooks.entity.EntityPlayerSPHook @@ -547,13 +547,6 @@ class Skytils { } } - @SubscribeEvent - fun onHypixelPacket(event: HypixelPacketEvent.ReceiveEvent) { - if (event.packet is ClientboundPingPacket) { - println("${event.packet.response} ${event.packet.version}") - } - } - @SubscribeEvent fun onHypixelPacketFail(event: HypixelPacketEvent.FailedEvent) { UChat.chat("$failPrefix Mod API request failed: ${event.reason}") @@ -601,8 +594,10 @@ class Skytils { } } - fun onJoinHypixel(handler: NetHandlerPlayClient) { - handler.addToSendQueue(ServerboundPingPacket().toCustomPayload()) + fun onJoinHypixel(handler: NetHandlerPlayClient) = IO.launch { + ServerboundPingPacket().getResponse(handler).let { packet -> + println("Hypixel Pong: ${packet.response}, version ${packet.version}") + } } @SubscribeEvent diff --git a/src/main/kotlin/gg/skytils/skytilsmod/listeners/ServerPayloadInterceptor.kt b/src/main/kotlin/gg/skytils/skytilsmod/listeners/ServerPayloadInterceptor.kt index 401e77772..d13a26054 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/listeners/ServerPayloadInterceptor.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/listeners/ServerPayloadInterceptor.kt @@ -18,31 +18,44 @@ package gg.skytils.skytilsmod.listeners +import gg.skytils.skytilsmod.Skytils.Companion.IO import gg.skytils.skytilsmod.events.impl.HypixelPacketEvent import gg.skytils.skytilsmod.events.impl.PacketEvent import io.netty.buffer.Unpooled +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout import net.hypixel.modapi.error.ErrorReason import net.hypixel.modapi.packet.HypixelPacket import net.hypixel.modapi.packet.HypixelPacketType import net.hypixel.modapi.serializer.PacketSerializer +import net.minecraft.client.network.NetHandlerPlayClient import net.minecraft.network.PacketBuffer import net.minecraft.network.play.client.C17PacketCustomPayload import net.minecraft.network.play.server.S3FPacketCustomPayload import net.minecraftforge.fml.common.eventhandler.EventPriority import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import kotlin.time.Duration.Companion.minutes object ServerPayloadInterceptor { + private val receivedPackets = MutableSharedFlow() + @SubscribeEvent(priority = EventPriority.HIGHEST) fun onReceivePacket(event: PacketEvent.ReceiveEvent) { if (event.packet is S3FPacketCustomPayload) { - HypixelPacketType.getByIdentifier(event.packet.channelName)?.let { pt -> - val packetSerializer = PacketSerializer(event.packet.bufferData.duplicate()) - if (!packetSerializer.readBoolean()) { - val reason = ErrorReason.getById(packetSerializer.readVarInt()) - HypixelPacketEvent.FailedEvent(pt, reason).postAndCatch() - } else { - val packet = pt.packetFactory.apply(packetSerializer) - HypixelPacketEvent.ReceiveEvent(packet).postAndCatch() + IO.launch { + HypixelPacketType.getByIdentifier(event.packet.channelName)?.let { pt -> + val packetSerializer = PacketSerializer(event.packet.bufferData.duplicate()) + if (!packetSerializer.readBoolean()) { + val reason = ErrorReason.getById(packetSerializer.readVarInt()) + HypixelPacketEvent.FailedEvent(pt, reason).postAndCatch() + } else { + val packet = pt.packetFactory.apply(packetSerializer) + receivedPackets.emit(packet) + HypixelPacketEvent.ReceiveEvent(packet).postAndCatch() + } } } } @@ -63,4 +76,10 @@ object ServerPayloadInterceptor { this.write(serializer) return C17PacketCustomPayload(type.identifier, buffer) } + + suspend fun HypixelPacket.getResponse(handler: NetHandlerPlayClient): T = withTimeout(1.minutes) { + val packet: C17PacketCustomPayload = this@getResponse.toCustomPayload() + handler.addToSendQueue(packet) + return@withTimeout receivedPackets.filter { it.type == this@getResponse.type }.first() as T + } } \ No newline at end of file From ee118aba607c952959b84b795e3490f992119a2a Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Thu, 11 Apr 2024 16:47:50 -0400 Subject: [PATCH 28/46] feat: add command to send hypixel packets --- .../commands/impl/SkytilsCommand.kt | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/gg/skytils/skytilsmod/commands/impl/SkytilsCommand.kt b/src/main/kotlin/gg/skytils/skytilsmod/commands/impl/SkytilsCommand.kt index c4e527b97..b6a02ec1f 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/commands/impl/SkytilsCommand.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/commands/impl/SkytilsCommand.kt @@ -45,18 +45,24 @@ import gg.skytils.skytilsmod.features.impl.misc.Ping import gg.skytils.skytilsmod.features.impl.misc.PricePaid import gg.skytils.skytilsmod.features.impl.slayer.SlayerFeatures import gg.skytils.skytilsmod.features.impl.trackers.Tracker -import gg.skytils.skytilsmod.gui.* +import gg.skytils.skytilsmod.gui.OptionsGui import gg.skytils.skytilsmod.gui.editing.ElementaEditingGui import gg.skytils.skytilsmod.gui.editing.VanillaEditingGui import gg.skytils.skytilsmod.gui.features.* import gg.skytils.skytilsmod.gui.profile.ProfileGui import gg.skytils.skytilsmod.gui.updater.UpdateGui import gg.skytils.skytilsmod.gui.waypoints.WaypointsGui +import gg.skytils.skytilsmod.listeners.ServerPayloadInterceptor.getResponse import gg.skytils.skytilsmod.localapi.LocalAPI import gg.skytils.skytilsmod.utils.* import kotlinx.coroutines.async import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import net.hypixel.modapi.packet.HypixelPacket +import net.hypixel.modapi.packet.impl.serverbound.ServerboundLocationPacket +import net.hypixel.modapi.packet.impl.serverbound.ServerboundPartyInfoPacket +import net.hypixel.modapi.packet.impl.serverbound.ServerboundPingPacket +import net.hypixel.modapi.packet.impl.serverbound.ServerboundPlayerInfoPacket import net.minecraft.client.entity.EntityPlayerSP import net.minecraft.client.gui.GuiScreen import net.minecraft.command.WrongUsageException @@ -376,6 +382,27 @@ object SkytilsCommand : BaseCommand("skytils", listOf("st")) { } } + "hypixelpacket" -> { + val packet = when (args.getOrNull(1)) { + "ping" -> ServerboundPingPacket() + "location" -> ServerboundLocationPacket() + "party_info" -> ServerboundPartyInfoPacket() + "player_info" -> ServerboundPlayerInfoPacket() + else -> return UChat.chat("$failPrefix §cPacket not found!") + } + + UChat.chat("$successPrefix §aPacket created: $packet") + Skytils.IO.launch { + runCatching { + packet.getResponse(mc.netHandler) + }.onFailure { + UChat.chat("$failPrefix §cFailed to get packet response: ${it.message}") + }.onSuccess { response -> + UChat.chat("$successPrefix §aPacket response: $response") + } + } + } + else -> UChat.chat("$failPrefix §cThis command doesn't exist!\n §cUse §f/skytils help§c for a full list of commands") } } From 5dace83f62beddd463abf35a9050f26ffa30708f Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Mon, 29 Apr 2024 14:15:03 -0400 Subject: [PATCH 29/46] chore: update hypixel mod-api to 0.3.1 chore: update to mod-api 0.3.2 --- build.gradle.kts | 2 +- .../AccessorHypixelPacketRegistry.java | 32 +++++++++++++ .../commands/impl/SkytilsCommand.kt | 45 ++++++++++--------- .../events/impl/HypixelPacketEvent.kt | 9 ++-- .../listeners/ServerPayloadInterceptor.kt | 28 +++++++----- src/main/resources/mixins.skytils.json | 1 + 6 files changed, 79 insertions(+), 38 deletions(-) create mode 100644 src/main/java/gg/skytils/skytilsmod/mixins/transformers/accessors/AccessorHypixelPacketRegistry.java diff --git a/build.gradle.kts b/build.gradle.kts index 0de006008..cd4949a81 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -152,7 +152,7 @@ dependencies { exclude(module = "bcprov-jdk18on") } compileOnly("org.bouncycastle:bcprov-jdk18on:1.78.1") - shadowMe("net.hypixel:mod-api:0.2.1") + shadowMe("net.hypixel:mod-api:0.3.2") shadowMe(annotationProcessor("io.github.llamalad7:mixinextras-common:0.3.5")!!) diff --git a/src/main/java/gg/skytils/skytilsmod/mixins/transformers/accessors/AccessorHypixelPacketRegistry.java b/src/main/java/gg/skytils/skytilsmod/mixins/transformers/accessors/AccessorHypixelPacketRegistry.java new file mode 100644 index 000000000..4e30a711b --- /dev/null +++ b/src/main/java/gg/skytils/skytilsmod/mixins/transformers/accessors/AccessorHypixelPacketRegistry.java @@ -0,0 +1,32 @@ +/* + * Skytils - Hypixel Skyblock Quality of Life Mod + * Copyright (C) 2020-2024 Skytils + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package gg.skytils.skytilsmod.mixins.transformers.accessors; + +import net.hypixel.modapi.packet.HypixelPacket; +import net.hypixel.modapi.packet.PacketRegistry; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import java.util.Map; + +@Mixin(PacketRegistry.class) +public interface AccessorHypixelPacketRegistry { + @Accessor + Map, String> getClassToIdentifier(); +} diff --git a/src/main/kotlin/gg/skytils/skytilsmod/commands/impl/SkytilsCommand.kt b/src/main/kotlin/gg/skytils/skytilsmod/commands/impl/SkytilsCommand.kt index b6a02ec1f..15f3a8e0a 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/commands/impl/SkytilsCommand.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/commands/impl/SkytilsCommand.kt @@ -54,15 +54,14 @@ import gg.skytils.skytilsmod.gui.updater.UpdateGui import gg.skytils.skytilsmod.gui.waypoints.WaypointsGui import gg.skytils.skytilsmod.listeners.ServerPayloadInterceptor.getResponse import gg.skytils.skytilsmod.localapi.LocalAPI +import gg.skytils.skytilsmod.mixins.transformers.accessors.AccessorHypixelPacketRegistry import gg.skytils.skytilsmod.utils.* import kotlinx.coroutines.async import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import net.hypixel.modapi.packet.HypixelPacket -import net.hypixel.modapi.packet.impl.serverbound.ServerboundLocationPacket -import net.hypixel.modapi.packet.impl.serverbound.ServerboundPartyInfoPacket -import net.hypixel.modapi.packet.impl.serverbound.ServerboundPingPacket -import net.hypixel.modapi.packet.impl.serverbound.ServerboundPlayerInfoPacket +import net.hypixel.modapi.HypixelModAPI +import net.hypixel.modapi.packet.ClientboundHypixelPacket +import net.hypixel.modapi.packet.impl.serverbound.ServerboundVersionedPacket import net.minecraft.client.entity.EntityPlayerSP import net.minecraft.client.gui.GuiScreen import net.minecraft.command.WrongUsageException @@ -383,22 +382,26 @@ object SkytilsCommand : BaseCommand("skytils", listOf("st")) { } "hypixelpacket" -> { - val packet = when (args.getOrNull(1)) { - "ping" -> ServerboundPingPacket() - "location" -> ServerboundLocationPacket() - "party_info" -> ServerboundPartyInfoPacket() - "player_info" -> ServerboundPlayerInfoPacket() - else -> return UChat.chat("$failPrefix §cPacket not found!") - } - - UChat.chat("$successPrefix §aPacket created: $packet") - Skytils.IO.launch { - runCatching { - packet.getResponse(mc.netHandler) - }.onFailure { - UChat.chat("$failPrefix §cFailed to get packet response: ${it.message}") - }.onSuccess { response -> - UChat.chat("$successPrefix §aPacket response: $response") + val registry = HypixelModAPI.getInstance().registry + val id = args.getOrNull(1) ?: return UChat.chat("$failPrefix §cInput a packet type!") + if (id == "list") { + UChat.chat("$successPrefix §eAvailable types: ${registry.identifiers.joinToString(", ")}") + } else if (!registry.isRegistered(id)) { + UChat.chat("$failPrefix §cPacket not found!") + } else { + registry as AccessorHypixelPacketRegistry + val packetClass = registry.classToIdentifier.entries.find { it.value == id && ServerboundVersionedPacket::class.java.isAssignableFrom(it.key) } + ?: return UChat.chat("$failPrefix §cPacket not found!") + val packet = packetClass.key.newInstance() as ServerboundVersionedPacket + UChat.chat("$successPrefix §aPacket created: $packet") + Skytils.IO.launch { + runCatching { + packet.getResponse(mc.netHandler) + }.onFailure { + UChat.chat("$failPrefix §cFailed to get packet response: ${it.message}") + }.onSuccess { response -> + UChat.chat("$successPrefix §aPacket response: $response") + } } } } diff --git a/src/main/kotlin/gg/skytils/skytilsmod/events/impl/HypixelPacketEvent.kt b/src/main/kotlin/gg/skytils/skytilsmod/events/impl/HypixelPacketEvent.kt index e64a7b125..e25f707c7 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/events/impl/HypixelPacketEvent.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/events/impl/HypixelPacketEvent.kt @@ -19,23 +19,22 @@ package gg.skytils.skytilsmod.events.impl import gg.skytils.skytilsmod.events.SkytilsEvent import net.hypixel.modapi.error.ErrorReason -import net.hypixel.modapi.packet.HypixelPacket -import net.hypixel.modapi.packet.HypixelPacketType +import net.hypixel.modapi.packet.ClientboundHypixelPacket import net.minecraftforge.fml.common.eventhandler.Cancelable @Cancelable abstract class HypixelPacketEvent : SkytilsEvent() { abstract val direction: Direction - class ReceiveEvent(val packet: HypixelPacket) : HypixelPacketEvent() { + class ReceiveEvent(val packet: ClientboundHypixelPacket) : HypixelPacketEvent() { override val direction: Direction = Direction.INBOUND } - class SendEvent(val type: HypixelPacketType) : HypixelPacketEvent() { + class SendEvent(val type: String) : HypixelPacketEvent() { override val direction: Direction = Direction.OUTBOUND } - class FailedEvent(val type: HypixelPacketType, val reason: ErrorReason) : HypixelPacketEvent() { + class FailedEvent(val type: String, val reason: ErrorReason) : HypixelPacketEvent() { override val direction: Direction = Direction.OUTBOUND } diff --git a/src/main/kotlin/gg/skytils/skytilsmod/listeners/ServerPayloadInterceptor.kt b/src/main/kotlin/gg/skytils/skytilsmod/listeners/ServerPayloadInterceptor.kt index d13a26054..4dcfe6117 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/listeners/ServerPayloadInterceptor.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/listeners/ServerPayloadInterceptor.kt @@ -27,9 +27,11 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout +import net.hypixel.modapi.HypixelModAPI import net.hypixel.modapi.error.ErrorReason +import net.hypixel.modapi.packet.ClientboundHypixelPacket import net.hypixel.modapi.packet.HypixelPacket -import net.hypixel.modapi.packet.HypixelPacketType +import net.hypixel.modapi.packet.impl.serverbound.ServerboundVersionedPacket import net.hypixel.modapi.serializer.PacketSerializer import net.minecraft.client.network.NetHandlerPlayClient import net.minecraft.network.PacketBuffer @@ -40,19 +42,21 @@ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent import kotlin.time.Duration.Companion.minutes object ServerPayloadInterceptor { - private val receivedPackets = MutableSharedFlow() + private val receivedPackets = MutableSharedFlow() @SubscribeEvent(priority = EventPriority.HIGHEST) fun onReceivePacket(event: PacketEvent.ReceiveEvent) { if (event.packet is S3FPacketCustomPayload) { IO.launch { - HypixelPacketType.getByIdentifier(event.packet.channelName)?.let { pt -> + val registry = HypixelModAPI.getInstance().registry + val id = event.packet.channelName + if (registry.isRegistered(id)) { val packetSerializer = PacketSerializer(event.packet.bufferData.duplicate()) if (!packetSerializer.readBoolean()) { val reason = ErrorReason.getById(packetSerializer.readVarInt()) - HypixelPacketEvent.FailedEvent(pt, reason).postAndCatch() + HypixelPacketEvent.FailedEvent(id, reason).postAndCatch() } else { - val packet = pt.packetFactory.apply(packetSerializer) + val packet = registry.createClientboundPacket(id, packetSerializer) receivedPackets.emit(packet) HypixelPacketEvent.ReceiveEvent(packet).postAndCatch() } @@ -64,22 +68,24 @@ object ServerPayloadInterceptor { @SubscribeEvent(priority = EventPriority.HIGHEST) fun onSendPacket(event: PacketEvent.SendEvent) { if (event.packet is C17PacketCustomPayload) { - HypixelPacketType.getByIdentifier(event.packet.channelName)?.let { pt -> - HypixelPacketEvent.SendEvent(pt).postAndCatch() + val registry = HypixelModAPI.getInstance().registry + val id = event.packet.channelName + if (registry.isRegistered(id)) { + HypixelPacketEvent.SendEvent(id).postAndCatch() } } } - fun HypixelPacket.toCustomPayload(): C17PacketCustomPayload { + fun ServerboundVersionedPacket.toCustomPayload(): C17PacketCustomPayload { val buffer = PacketBuffer(Unpooled.buffer()) val serializer = PacketSerializer(buffer) this.write(serializer) - return C17PacketCustomPayload(type.identifier, buffer) + return C17PacketCustomPayload(this.identifier, buffer) } - suspend fun HypixelPacket.getResponse(handler: NetHandlerPlayClient): T = withTimeout(1.minutes) { + suspend fun ServerboundVersionedPacket.getResponse(handler: NetHandlerPlayClient): T = withTimeout(1.minutes) { val packet: C17PacketCustomPayload = this@getResponse.toCustomPayload() handler.addToSendQueue(packet) - return@withTimeout receivedPackets.filter { it.type == this@getResponse.type }.first() as T + return@withTimeout receivedPackets.filter { it.identifier == this@getResponse.identifier }.first() as T } } \ No newline at end of file diff --git a/src/main/resources/mixins.skytils.json b/src/main/resources/mixins.skytils.json index 02f0872fc..7b52269bd 100644 --- a/src/main/resources/mixins.skytils.json +++ b/src/main/resources/mixins.skytils.json @@ -20,6 +20,7 @@ "accessors.AccessorGuiMainMenu", "accessors.AccessorGuiNewChat", "accessors.AccessorGuiStreamUnavailable", + "accessors.AccessorHypixelPacketRegistry", "accessors.AccessorMinecraft", "accessors.AccessorModelDragon", "accessors.AccessorRenderItem", From ab0088b214fa8c17a7a9bce8d312620a6257de6c Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Sat, 18 May 2024 18:08:53 -0400 Subject: [PATCH 30/46] feat: add dungeon syncing through ws chore: update ws URL fix: rebase errors fix: rebase error fix: queue room packets from before dungeon start fix: rebase error didn't close SkytilsWS connection --- .gitmodules | 4 + build.gradle.kts | 3 +- settings.gradle.kts | 1 + .../kotlin/gg/skytils/skytilsmod/Skytils.kt | 29 ++++++- .../dungeons/catlas/core/map/UniqueRoom.kt | 3 +- .../catlas/handlers/DungeonScanner.kt | 15 ++++ .../features/impl/handlers/Waypoints.kt | 2 - .../skytilsmod/listeners/DungeonListener.kt | 80 ++++++++++++++++--- .../gg/skytils/skytilsmod/utils/SBInfo.kt | 2 +- .../skytils/skytilsws/client/PacketHandler.kt | 76 ++++++++++++++++++ .../gg/skytils/skytilsws/client/WSClient.kt | 63 +++++++++++++++ ws-shared | 1 + 12 files changed, 261 insertions(+), 18 deletions(-) create mode 100644 src/main/kotlin/gg/skytils/skytilsws/client/PacketHandler.kt create mode 100644 src/main/kotlin/gg/skytils/skytilsws/client/WSClient.kt create mode 160000 ws-shared diff --git a/.gitmodules b/.gitmodules index ff5e49108..0f76f3979 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,7 @@ [submodule "hypixel-api"] path = hypixel-api url = https://github.com/Skytils/hypixel-api + +[submodule "ws-shared"] + path = ws-shared + url = https://github.com/Skytils/ws-shared \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index cd4949a81..53d28d6e2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -115,7 +115,7 @@ dependencies { } shadowMe(platform(kotlin("bom"))) - shadowMe(platform(ktor("bom", "2.3.9", addSuffix = false))) + shadowMe(platform(ktor("bom", "2.3.11", addSuffix = false))) shadowMe(ktor("serialization-kotlinx-json")) @@ -147,6 +147,7 @@ dependencies { shadowMe(project(":events")) shadowMe(project(":hypixel-api:types")) + shadowMe(project(":ws-shared")) shadowMe("org.bouncycastle:bcpg-jdk18on:1.78.1") { exclude(module = "bcprov-jdk18on") diff --git a/settings.gradle.kts b/settings.gradle.kts index 19334a791..427b3e7b8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -37,3 +37,4 @@ pluginManagement { rootProject.name = "SkytilsMod" include("events") include("hypixel-api:types") +include("ws-shared") diff --git a/src/main/kotlin/gg/skytils/skytilsmod/Skytils.kt b/src/main/kotlin/gg/skytils/skytilsmod/Skytils.kt index aa714ae92..5fa5e8d2c 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/Skytils.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/Skytils.kt @@ -25,7 +25,6 @@ import gg.skytils.skytilsmod.commands.impl.* import gg.skytils.skytilsmod.commands.stats.impl.CataCommand import gg.skytils.skytilsmod.commands.stats.impl.SlayerCommand import gg.skytils.skytilsmod.core.* -import gg.skytils.skytilsmod.tweaker.DependencyLoader import gg.skytils.skytilsmod.events.impl.HypixelPacketEvent import gg.skytils.skytilsmod.events.impl.MainReceivePacketEvent import gg.skytils.skytilsmod.events.impl.PacketEvent @@ -72,18 +71,25 @@ import gg.skytils.skytilsmod.mixins.hooks.util.MouseHelperHook import gg.skytils.skytilsmod.mixins.transformers.accessors.AccessorCommandHandler import gg.skytils.skytilsmod.mixins.transformers.accessors.AccessorGuiStreamUnavailable import gg.skytils.skytilsmod.mixins.transformers.accessors.AccessorSettingsGui +import gg.skytils.skytilsmod.tweaker.DependencyLoader import gg.skytils.skytilsmod.utils.* import gg.skytils.skytilsmod.utils.graphics.ScreenRenderer import gg.skytils.skytilsmod.utils.graphics.colors.CustomColor +import gg.skytils.skytilsws.client.WSClient +import gg.skytils.skytilsws.shared.SkytilsWS import io.ktor.client.* import io.ktor.client.engine.cio.* import io.ktor.client.plugins.* import io.ktor.client.plugins.cache.* import io.ktor.client.plugins.compression.* import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.plugins.websocket.* import io.ktor.http.* +import io.ktor.serialization.kotlinx.* import io.ktor.serialization.kotlinx.json.* +import io.ktor.websocket.* import kotlinx.coroutines.* +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import kotlinx.serialization.modules.SerializersModule import net.hypixel.modapi.packet.impl.clientbound.ClientboundPingPacket @@ -123,6 +129,7 @@ import java.security.KeyStore import java.util.* import java.util.concurrent.Executors import java.util.concurrent.ThreadPoolExecutor +import java.util.zip.Deflater import javax.net.ssl.TrustManagerFactory import javax.net.ssl.X509TrustManager import kotlin.coroutines.CoroutineContext @@ -260,6 +267,18 @@ class Skytils { trustManager = UnionX509TrustManager(backingManager, ourManager) } } + + install(WebSockets) { + pingInterval = 5_000L + @OptIn(ExperimentalSerializationApi::class) + contentConverter = KotlinxWebsocketSerializationConverter(SkytilsWS.packetSerializer) + extensions { + install(WebSocketDeflateExtension) { + compressionLevel = Deflater.DEFAULT_COMPRESSION + compressIfBiggerThan(bytes = 4 * 1024) + } + } + } } val areaRegex = Regex("§r§b§l(?[\\w]+): §r§7(?[\\w ]+)§r") @@ -545,6 +564,10 @@ class Skytils { IO.launch { TrophyFish.loadFromApi() } + + IO.launch { + WSClient.openConnection() + } } @SubscribeEvent @@ -605,6 +628,10 @@ class Skytils { Utils.isOnHypixel = false Utils.skyblock = false Utils.dungeons = false + + IO.launch { + WSClient.closeConnection() + } } @SubscribeEvent diff --git a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/dungeons/catlas/core/map/UniqueRoom.kt b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/dungeons/catlas/core/map/UniqueRoom.kt index c3c2ceb51..082dcec70 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/dungeons/catlas/core/map/UniqueRoom.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/dungeons/catlas/core/map/UniqueRoom.kt @@ -10,7 +10,8 @@ class UniqueRoom(arrX: Int, arrY: Int, room: Room) { private var topLeft = Pair(arrX, arrY) private var center = Pair(arrX, arrY) var mainRoom = room - private val tiles = mutableListOf(room) + val tiles = mutableListOf(room) + var foundSecrets: Int? = null init { DungeonInfo.cryptCount += room.data.crypts diff --git a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/dungeons/catlas/handlers/DungeonScanner.kt b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/dungeons/catlas/handlers/DungeonScanner.kt index f1699f04d..57154b242 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/dungeons/catlas/handlers/DungeonScanner.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/dungeons/catlas/handlers/DungeonScanner.kt @@ -18,11 +18,21 @@ package gg.skytils.skytilsmod.features.impl.dungeons.catlas.handlers +import gg.skytils.skytilsmod.Skytils.Companion.IO import gg.skytils.skytilsmod.Skytils.Companion.mc import gg.skytils.skytilsmod.features.impl.dungeons.DungeonFeatures.dungeonFloorNumber import gg.skytils.skytilsmod.features.impl.dungeons.catlas.core.map.* import gg.skytils.skytilsmod.features.impl.dungeons.catlas.handlers.DungeonScanner.scan import gg.skytils.skytilsmod.features.impl.dungeons.catlas.utils.ScanUtils +import gg.skytils.skytilsmod.listeners.DungeonListener +import gg.skytils.skytilsmod.listeners.ServerPayloadInterceptor.getResponse +import gg.skytils.skytilsmod.utils.SBInfo +import gg.skytils.skytilsws.client.WSClient +import gg.skytils.skytilsws.shared.packet.C2SPacketDungeonRoom +import gg.skytils.skytilsws.shared.packet.C2SPacketDungeonRoomSecret +import kotlinx.coroutines.launch +import net.hypixel.modapi.packet.impl.clientbound.ClientboundLocationPacket +import net.hypixel.modapi.packet.impl.serverbound.ServerboundLocationPacket import net.minecraft.init.Blocks import net.minecraft.util.BlockPos @@ -73,6 +83,11 @@ object DungeonScanner { scanRoom(xPos, zPos, z, x)?.let { DungeonInfo.dungeonList[z * 11 + x] = it + if (it is Room && it.data.name != "Unknown") { + IO.launch { + DungeonListener.outboundRoomQueue.add(C2SPacketDungeonRoom(SBInfo.server ?: return@launch, it.data.name, xPos, zPos, x, z, it.core, it.isSeparator)) + } + } } } } diff --git a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/handlers/Waypoints.kt b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/handlers/Waypoints.kt index 8acd1f692..95d9674e9 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/handlers/Waypoints.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/handlers/Waypoints.kt @@ -25,8 +25,6 @@ import gg.essential.universal.UMatrixStack import gg.skytils.skytilsmod.Skytils import gg.skytils.skytilsmod.commands.impl.OrderedWaypointCommand import gg.skytils.skytilsmod.core.PersistentSave -import gg.skytils.skytilsmod.core.tickTimer -import gg.skytils.skytilsmod.events.impl.skyblock.LocrawReceivedEvent import gg.skytils.skytilsmod.tweaker.DependencyLoader import gg.skytils.skytilsmod.utils.* import kotlinx.serialization.EncodeDefault diff --git a/src/main/kotlin/gg/skytils/skytilsmod/listeners/DungeonListener.kt b/src/main/kotlin/gg/skytils/skytilsmod/listeners/DungeonListener.kt index a0bced94d..77a965040 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/listeners/DungeonListener.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/listeners/DungeonListener.kt @@ -24,6 +24,7 @@ import gg.essential.lib.caffeine.cache.Expiry import gg.essential.universal.UChat import gg.skytils.hypixel.types.skyblock.Pet import gg.skytils.skytilsmod.Skytils +import gg.skytils.skytilsmod.Skytils.Companion.IO import gg.skytils.skytilsmod.Skytils.Companion.failPrefix import gg.skytils.skytilsmod.Skytils.Companion.mc import gg.skytils.skytilsmod.commands.impl.RepartyCommand @@ -35,14 +36,28 @@ import gg.skytils.skytilsmod.features.impl.dungeons.DungeonFeatures import gg.skytils.skytilsmod.features.impl.dungeons.DungeonTimer import gg.skytils.skytilsmod.features.impl.dungeons.ScoreCalculation import gg.skytils.skytilsmod.features.impl.dungeons.catlas.core.DungeonMapPlayer +import gg.skytils.skytilsmod.features.impl.dungeons.catlas.core.map.RoomType import gg.skytils.skytilsmod.features.impl.dungeons.catlas.handlers.DungeonInfo +import gg.skytils.skytilsmod.features.impl.dungeons.catlas.handlers.DungeonScanner +import gg.skytils.skytilsmod.features.impl.dungeons.catlas.utils.MapUtils import gg.skytils.skytilsmod.features.impl.handlers.CooldownTracker import gg.skytils.skytilsmod.features.impl.handlers.SpiritLeap +import gg.skytils.skytilsmod.listeners.ServerPayloadInterceptor.getResponse import gg.skytils.skytilsmod.mixins.transformers.accessors.AccessorChatComponentText import gg.skytils.skytilsmod.utils.* import gg.skytils.skytilsmod.utils.NumberUtil.addSuffix import gg.skytils.skytilsmod.utils.NumberUtil.romanToDecimal +import gg.skytils.skytilsws.client.WSClient +import gg.skytils.skytilsws.shared.packet.C2SPacketDungeonRoom +import gg.skytils.skytilsws.shared.packet.C2SPacketDungeonRoomSecret +import gg.skytils.skytilsws.shared.packet.C2SPacketStartDungeon +import kotlinx.coroutines.async +import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import net.hypixel.modapi.packet.impl.clientbound.ClientboundLocationPacket +import net.hypixel.modapi.packet.impl.clientbound.ClientboundPartyInfoPacket +import net.hypixel.modapi.packet.impl.serverbound.ServerboundLocationPacket +import net.hypixel.modapi.packet.impl.serverbound.ServerboundPartyInfoPacket import net.minecraft.entity.player.EntityPlayer import net.minecraft.network.play.server.S02PacketChat import net.minecraft.util.ResourceLocation @@ -50,6 +65,7 @@ import net.minecraftforge.client.event.ClientChatReceivedEvent import net.minecraftforge.event.world.WorldEvent import net.minecraftforge.fml.common.eventhandler.EventPriority import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import java.util.concurrent.CopyOnWriteArrayList object DungeonListener { val team = hashMapOf() @@ -99,6 +115,7 @@ object DungeonListener { private val keyPickupRegex = Regex("§r§e§lRIGHT CLICK §r§7on §r§7.+?§r§7 to open it\\. This key can only be used to open §r§a(?\\d+)§r§7 door!§r") private val witherDoorOpenedRegex = Regex("^(?:\\[.+?] )?(?\\w+) opened a WITHER door!$") private const val bloodOpenedString = "§r§cThe §r§c§lBLOOD DOOR§r§c has been opened!§r" + val outboundRoomQueue = arrayListOf() @SubscribeEvent fun onWorldLoad(event: WorldEvent.Unload) { @@ -108,6 +125,7 @@ object DungeonListener { missingPuzzles.clear() completedPuzzles.clear() teamCached.clear() + outboundRoomQueue.clear() } @SubscribeEvent @@ -117,19 +135,28 @@ object DungeonListener { val text = event.packet.chatComponent.formattedText val unformatted = text.stripControlCodes() if (event.packet.type == 2.toByte()) { - if (Skytils.config.dungeonSecretDisplay) { - secretsRegex.find(text)?.destructured?.also { (secrets, maxSecrets) -> - val sec = secrets.toInt() - val max = maxSecrets.toInt().coerceAtLeast(sec) - - DungeonFeatures.DungeonSecretDisplay.secrets = sec - DungeonFeatures.DungeonSecretDisplay.maxSecrets = max - }.ifNull { - DungeonFeatures.DungeonSecretDisplay.secrets = -1 - DungeonFeatures.DungeonSecretDisplay.maxSecrets = -1 + secretsRegex.find(text)?.destructured?.also { (secrets, maxSecrets) -> + val sec = secrets.toInt() + val max = maxSecrets.toInt().coerceAtLeast(sec) + + DungeonFeatures.DungeonSecretDisplay.secrets = sec + DungeonFeatures.DungeonSecretDisplay.maxSecrets = max + + IO.launch { + val x = ((mc.thePlayer.posX - DungeonScanner.startX + 15) * MapUtils.coordMultiplier / (MapUtils.mapRoomSize + 4) * 2).toInt() + val z = ((mc.thePlayer.posZ - DungeonScanner.startZ + 15) * MapUtils.coordMultiplier / (MapUtils.mapRoomSize + 4) * 2).toInt() + + val room = DungeonInfo.uniqueRooms.find { it.tiles.any { it.x == x && it.z == z } } + + if (room != null && room.foundSecrets != sec) { + room.foundSecrets = sec + WSClient.sendPacket(C2SPacketDungeonRoomSecret(SBInfo.locraw?.server ?: ServerboundLocationPacket().getResponse(mc.netHandler).serverName, room.mainRoom.data.name, sec)) + } } + }.ifNull { + DungeonFeatures.DungeonSecretDisplay.secrets = -1 + DungeonFeatures.DungeonSecretDisplay.maxSecrets = -1 } - } else { if (text.stripControlCodes() .trim() == "> EXTRA STATS <" @@ -170,6 +197,35 @@ object DungeonListener { } else if (text == bloodOpenedString) { SpiritLeap.doorOpener = null DungeonInfo.keys-- + } else if (text == "§r§aStarting in 1 second.§r") { + IO.launch { + delay(2000) + if (DungeonTimer.dungeonStartTime != -1L) { + val location = async { + ServerboundLocationPacket().getResponse(mc.netHandler) + } + val party = async { + ServerboundPartyInfoPacket().getResponse(mc.netHandler) + } + val partyMembers = party.await().members.ifEmpty { setOf(mc.thePlayer.uniqueID) }.mapTo(hashSetOf()) { it.toString() } + val entrance = DungeonInfo.uniqueRooms.first { it.mainRoom.data.type == RoomType.ENTRANCE } + WSClient.sendPacket(C2SPacketStartDungeon( + serverId = location.await().serverName, + floor = DungeonFeatures.dungeonFloor!!, + members = partyMembers, + startTime = DungeonTimer.dungeonStartTime, + entranceLoc = entrance.mainRoom.z * entrance.mainRoom.x + )) + while (DungeonTimer.dungeonStartTime != -1L) { + val itr = outboundRoomQueue.iterator() + while (itr.hasNext()) { + val packet = itr.next() + itr.remove() + WSClient.sendPacket(packet) + } + } + } + } } else { witherDoorOpenedRegex.find(unformatted)?.destructured?.let { (name) -> SpiritLeap.doorOpener = name @@ -417,7 +473,7 @@ object DungeonListener { fun checkSpiritPet() { val teamCopy = team.values.toList() - Skytils.IO.launch { + IO.launch { runCatching { for (teammate in teamCopy) { val name = teammate.playerName diff --git a/src/main/kotlin/gg/skytils/skytilsmod/utils/SBInfo.kt b/src/main/kotlin/gg/skytils/skytilsmod/utils/SBInfo.kt index 31155b573..4c7d01c8d 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/utils/SBInfo.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/utils/SBInfo.kt @@ -64,7 +64,7 @@ object SBInfo { private var lastManualLocRaw: Long = -1 private var lastLocRaw: Long = -1 private var joinedWorld: Long = -1 - private var locraw: LocrawObject? = null + var locraw: LocrawObject? = null private val junkRegex = Regex("[^\u0020-\u0127û]") @SubscribeEvent diff --git a/src/main/kotlin/gg/skytils/skytilsws/client/PacketHandler.kt b/src/main/kotlin/gg/skytils/skytilsws/client/PacketHandler.kt new file mode 100644 index 000000000..4a5d4323b --- /dev/null +++ b/src/main/kotlin/gg/skytils/skytilsws/client/PacketHandler.kt @@ -0,0 +1,76 @@ +/* + * Skytils - Hypixel Skyblock Quality of Life Mod + * Copyright (C) 2020-2024 Skytils + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package gg.skytils.skytilsws.client + +import gg.skytils.skytilsmod.Reference +import gg.skytils.skytilsmod.Skytils.Companion.mc +import gg.skytils.skytilsmod.features.impl.dungeons.catlas.core.map.Room +import gg.skytils.skytilsmod.features.impl.dungeons.catlas.core.map.Unknown +import gg.skytils.skytilsmod.features.impl.dungeons.catlas.handlers.DungeonInfo +import gg.skytils.skytilsmod.features.impl.dungeons.catlas.utils.ScanUtils +import gg.skytils.skytilsws.shared.IPacketHandler +import gg.skytils.skytilsws.shared.SkytilsWS +import gg.skytils.skytilsws.shared.packet.* +import io.ktor.websocket.* +import kotlinx.coroutines.coroutineScope +import java.util.* + +object PacketHandler : IPacketHandler { + suspend fun handleLogin(session: WebSocketSession, packet: S2CPacketAcknowledge) { + val serverId = UUID.randomUUID().toString().replace("-".toRegex(), "") + mc.sessionService.joinServer(mc.session.profile, mc.session.token, serverId) + WSClient.sendPacket(C2SPacketLogin(mc.session.username, mc.session.profile.id.toString(), Reference.VERSION, SkytilsWS.version, serverId)) + } + + override suspend fun processPacket(session: WebSocketSession, packet: Packet) { + println("Received packet: $packet") + when (packet) { + is S2CPacketAcknowledge -> { + if (packet.wsVersion != SkytilsWS.version) { + session.close(CloseReason(CloseReason.Codes.CANNOT_ACCEPT, "Incompatible WS version")) + } else { + coroutineScope { + handleLogin(session, packet) + } + } + } + is S2CPacketDungeonRoomSecret -> { + DungeonInfo.uniqueRooms.find { it.mainRoom.data.name == packet.roomId }?.let { + if (packet.secretCount > (it.foundSecrets ?: -1)) { + it.foundSecrets = packet.secretCount + } + } + } + is S2CPacketDungeonRoom -> { + val room = DungeonInfo.dungeonList[packet.row * 11 + packet.col] + if (room is Unknown || (room as? Room)?.data?.name == "Unknown") { + val data = ScanUtils.roomList.find { it.name == packet.roomId } + DungeonInfo.dungeonList[packet.row * 11 + packet.col] = Room(packet.x, packet.z, data ?: return).apply { + isSeparator = packet.isSeparator + core = packet.core + addToUnique(packet.row, packet.col) + } + } + } + else -> { + session.close(CloseReason(CloseReason.Codes.CANNOT_ACCEPT, "Unknown packet type")) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/gg/skytils/skytilsws/client/WSClient.kt b/src/main/kotlin/gg/skytils/skytilsws/client/WSClient.kt new file mode 100644 index 000000000..c7fb57257 --- /dev/null +++ b/src/main/kotlin/gg/skytils/skytilsws/client/WSClient.kt @@ -0,0 +1,63 @@ +/* + * Skytils - Hypixel Skyblock Quality of Life Mod + * Copyright (C) 2020-2024 Skytils + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package gg.skytils.skytilsws.client + +import gg.skytils.skytilsmod.Skytils +import gg.skytils.skytilsmod.Skytils.Companion.client +import gg.skytils.skytilsws.shared.SkytilsWS +import gg.skytils.skytilsws.shared.packet.C2SPacketConnect +import gg.skytils.skytilsws.shared.packet.Packet +import io.ktor.client.plugins.websocket.* +import io.ktor.websocket.* +import kotlinx.coroutines.channels.ClosedReceiveChannelException + +object WSClient { + var session: DefaultClientWebSocketSession? = null + + suspend fun openConnection() { + if (session != null) error("Session already open") + + client.webSocketSession("wss://ws.skytils.gg/ws").apply { + session = this + try { + sendSerialized(C2SPacketConnect(SkytilsWS.version, Skytils.VERSION)) + while (true) { + val packet = receiveDeserialized() + PacketHandler.processPacket(this@apply, packet) + } + } catch(e: ClosedReceiveChannelException) { + e.printStackTrace() + closeExceptionally(e) + } catch (e: Throwable) { + e.printStackTrace() + closeExceptionally(e) + } finally { + session = null + } + } + } + + suspend fun closeConnection() { + session?.close(CloseReason(CloseReason.Codes.NORMAL, "Client closed connection")) + } + + suspend fun sendPacket(packet: Packet) { + session?.sendSerialized(packet) ?: error("Tried to send packet but session was null") + } +} \ No newline at end of file diff --git a/ws-shared b/ws-shared new file mode 160000 index 000000000..51e490538 --- /dev/null +++ b/ws-shared @@ -0,0 +1 @@ +Subproject commit 51e4905381c9875a9283091d4ff628c904b42e66 From 6145a5d65cc5ed6c5aceec28fff2302954f10873 Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Sat, 18 May 2024 18:09:16 -0400 Subject: [PATCH 31/46] feat: remove usage of locraw for hypixel mod api fix: SBInfo fields are always null --- .../impl/skyblock/LocrawReceivedEvent.kt | 24 ------ .../features/impl/handlers/Waypoints.kt | 15 ++-- .../skytilsmod/listeners/DungeonListener.kt | 2 +- .../gg/skytils/skytilsmod/utils/SBInfo.kt | 86 +++++-------------- 4 files changed, 32 insertions(+), 95 deletions(-) delete mode 100644 src/main/kotlin/gg/skytils/skytilsmod/events/impl/skyblock/LocrawReceivedEvent.kt diff --git a/src/main/kotlin/gg/skytils/skytilsmod/events/impl/skyblock/LocrawReceivedEvent.kt b/src/main/kotlin/gg/skytils/skytilsmod/events/impl/skyblock/LocrawReceivedEvent.kt deleted file mode 100644 index 10282ea87..000000000 --- a/src/main/kotlin/gg/skytils/skytilsmod/events/impl/skyblock/LocrawReceivedEvent.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Skytils - Hypixel Skyblock Quality of Life Mod - * Copyright (C) 2020-2023 Skytils - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package gg.skytils.skytilsmod.events.impl.skyblock - -import gg.skytils.skytilsmod.events.SkytilsEvent -import gg.skytils.skytilsmod.utils.LocrawObject - -data class LocrawReceivedEvent(val loc: LocrawObject) : SkytilsEvent() \ No newline at end of file diff --git a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/handlers/Waypoints.kt b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/handlers/Waypoints.kt index 95d9674e9..424b98aac 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/handlers/Waypoints.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/handlers/Waypoints.kt @@ -60,6 +60,7 @@ object Waypoints : PersistentSave(File(Skytils.modDir, "waypoints.json")) { private val sbeWaypointFormat = Regex("(?:\\.?\\/?crystalwaypoint parse )?(?[a-zA-Z\\d]+)@-(?[-\\d]+),(?[-\\d]+),(?[-\\d]+)\\\\?n?") private var visibleWaypoints = emptyList() + var needsRefresh = false @OptIn(ExperimentalSerializationApi::class) fun getWaypointsFromString(str: String): Set { @@ -231,17 +232,17 @@ object Waypoints : PersistentSave(File(Skytils.modDir, "waypoints.json")) { @SubscribeEvent fun onWorldChange(event: WorldEvent.Unload) { visibleWaypoints = emptyList() - } - - @SubscribeEvent - fun onLocraw(event: LocrawReceivedEvent) { - tickTimer(20, task = ::computeVisibleWaypoints) + needsRefresh = true } @SubscribeEvent fun onPlayerMove(event: ClientTickEvent) { - if (event.phase == TickEvent.Phase.END || mc.thePlayer?.hasMoved != true) return - if (SBInfo.mode != null && OrderedWaypointCommand.trackedIsland?.mode == SBInfo.mode) { + if (event.phase == TickEvent.Phase.END) return + if (needsRefresh && SBInfo.mode != null) { + computeVisibleWaypoints() + needsRefresh = false + } + if (mc.thePlayer?.hasMoved == true && SBInfo.mode != null && OrderedWaypointCommand.trackedIsland?.mode == SBInfo.mode) { val tracked = OrderedWaypointCommand.trackedSet?.firstOrNull() if (tracked == null) { OrderedWaypointCommand.doneTracking() diff --git a/src/main/kotlin/gg/skytils/skytilsmod/listeners/DungeonListener.kt b/src/main/kotlin/gg/skytils/skytilsmod/listeners/DungeonListener.kt index 77a965040..2ae81684a 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/listeners/DungeonListener.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/listeners/DungeonListener.kt @@ -150,7 +150,7 @@ object DungeonListener { if (room != null && room.foundSecrets != sec) { room.foundSecrets = sec - WSClient.sendPacket(C2SPacketDungeonRoomSecret(SBInfo.locraw?.server ?: ServerboundLocationPacket().getResponse(mc.netHandler).serverName, room.mainRoom.data.name, sec)) + WSClient.sendPacket(C2SPacketDungeonRoomSecret(SBInfo.server ?: return@launch, room.mainRoom.data.name, sec)) } } }.ifNull { diff --git a/src/main/kotlin/gg/skytils/skytilsmod/utils/SBInfo.kt b/src/main/kotlin/gg/skytils/skytilsmod/utils/SBInfo.kt index 4c7d01c8d..320596b72 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/utils/SBInfo.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/utils/SBInfo.kt @@ -17,27 +17,24 @@ */ package gg.skytils.skytilsmod.utils -import gg.skytils.skytilsmod.Skytils -import gg.skytils.skytilsmod.Skytils.Companion.json import gg.skytils.skytilsmod.Skytils.Companion.mc -import gg.skytils.skytilsmod.events.impl.skyblock.LocrawReceivedEvent -import gg.skytils.skytilsmod.events.impl.PacketEvent -import gg.skytils.skytilsmod.events.impl.SendChatMessageEvent +import gg.skytils.skytilsmod.events.impl.HypixelPacketEvent +import gg.skytils.skytilsmod.listeners.ServerPayloadInterceptor.toCustomPayload import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable -import kotlinx.serialization.SerializationException import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* +import net.hypixel.modapi.HypixelModAPI +import net.hypixel.modapi.packet.impl.clientbound.event.ClientboundLocationPacket import net.minecraft.client.gui.inventory.GuiChest import net.minecraft.inventory.ContainerChest -import net.minecraft.network.play.client.C01PacketChatMessage -import net.minecraft.network.play.server.S02PacketChat import net.minecraftforge.client.event.GuiOpenEvent import net.minecraftforge.event.world.WorldEvent import net.minecraftforge.fml.common.eventhandler.EventPriority import net.minecraftforge.fml.common.eventhandler.SubscribeEvent import net.minecraftforge.fml.common.gameevent.TickEvent import net.minecraftforge.fml.common.gameevent.TickEvent.ClientTickEvent +import net.minecraftforge.fml.common.network.FMLNetworkEvent.ClientDisconnectionFromServerEvent import java.text.ParseException import java.text.SimpleDateFormat import java.util.* @@ -55,19 +52,17 @@ object SBInfo { var location = "" var date = "" var time = "" - var objective: String? = "" - var mode: String? = "" + var objective: String? = null + var mode: String? = null + var server: String? = null var currentTimeDate: Date? = null + var lastLocationPacket: ClientboundLocationPacket? = null @JvmField var lastOpenContainerName: String? = null - private var lastManualLocRaw: Long = -1 - private var lastLocRaw: Long = -1 - private var joinedWorld: Long = -1 - var locraw: LocrawObject? = null private val junkRegex = Regex("[^\u0020-\u0127û]") - @SubscribeEvent + @SubscribeEvent(priority = EventPriority.HIGHEST) fun onGuiOpen(event: GuiOpenEvent) { if (!Utils.inSkyblock) return if (event.gui is GuiChest) { @@ -80,46 +75,25 @@ object SBInfo { @SubscribeEvent fun onWorldChange(event: WorldEvent.Unload) { - lastLocRaw = -1 - locraw = null - mode = null - joinedWorld = System.currentTimeMillis() lastOpenContainerName = null + lastLocationRequest = -1 } @SubscribeEvent - fun onSendChatMessage(event: SendChatMessageEvent) { - val msg = event.message - if (msg.trim().startsWith("/locraw")) { - lastManualLocRaw = System.currentTimeMillis() - } - } - - @SubscribeEvent(priority = EventPriority.LOW, receiveCanceled = true) - fun onChatMessage(event: PacketEvent.ReceiveEvent) { - if (event.packet is S02PacketChat) { - val unformatted = event.packet.chatComponent.unformattedText - if (unformatted.startsWith("{") && unformatted.endsWith("}")) { - try { - val obj = json.decodeFromString(unformatted) - if (System.currentTimeMillis() - lastManualLocRaw > 5000) { - Utils.cancelChatPacket(event) - } - locraw = obj - mode = obj.mode - LocrawReceivedEvent(obj).postAndCatch() - } catch (e: SerializationException) { - e.printStackTrace() - } - } - } + fun onDisconnect(event: ClientDisconnectionFromServerEvent) { + mode = null + server = null + lastLocationPacket = null } @SubscribeEvent - fun onPacket(event: PacketEvent.SendEvent) { - if (Utils.isOnHypixel && event.packet is C01PacketChatMessage) { - if (event.packet.message.startsWith("/locraw")) { - lastLocRaw = System.currentTimeMillis() + fun onHypixelPacket(event: HypixelPacketEvent.ReceiveEvent) { + if (event.packet is ClientboundLocationPacket) { + Utils.checkThreadAndQueue { + mode = event.packet.mode.orElse(null) + server = event.packet.serverName + lastLocationPacket = event.packet + println(event.packet) } } } @@ -127,11 +101,6 @@ object SBInfo { @SubscribeEvent fun onTick(event: ClientTickEvent) { if (event.phase != TickEvent.Phase.START || mc.thePlayer == null || mc.theWorld == null || !Utils.inSkyblock) return - val currentTime = System.currentTimeMillis() - if (locraw == null && currentTime - joinedWorld > 1300 && currentTime - lastLocRaw > 15000) { - lastLocRaw = System.currentTimeMillis() - Skytils.sendMessageQueue.add("/locraw") - } try { val lines = ScoreboardUtil.fetchScoreboardLines().map { it.stripControlCodes() } if (lines.size >= 5) { @@ -222,13 +191,4 @@ enum class SkyblockIsland(val displayName: String, val mode: String) { encodeStringElement(descriptor, 1, value.mode) } } -} - - -@Serializable -data class LocrawObject( - val server: String, - val gametype: String = "unknown", - val mode: String = "unknown", - val map: String = "unknown" -) \ No newline at end of file +} \ No newline at end of file From d20d8d1c27697d9fbc02aba3aea0df7ccc8dc725 Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Sun, 21 Apr 2024 12:43:47 -0400 Subject: [PATCH 32/46] fix: tell the server when dungeon ends --- .../kotlin/gg/skytils/skytilsmod/listeners/DungeonListener.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/kotlin/gg/skytils/skytilsmod/listeners/DungeonListener.kt b/src/main/kotlin/gg/skytils/skytilsmod/listeners/DungeonListener.kt index 2ae81684a..049fefad1 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/listeners/DungeonListener.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/listeners/DungeonListener.kt @@ -48,6 +48,7 @@ import gg.skytils.skytilsmod.utils.* import gg.skytils.skytilsmod.utils.NumberUtil.addSuffix import gg.skytils.skytilsmod.utils.NumberUtil.romanToDecimal import gg.skytils.skytilsws.client.WSClient +import gg.skytils.skytilsws.shared.packet.C2SPacketDungeonEnd import gg.skytils.skytilsws.shared.packet.C2SPacketDungeonRoom import gg.skytils.skytilsws.shared.packet.C2SPacketDungeonRoomSecret import gg.skytils.skytilsws.shared.packet.C2SPacketStartDungeon @@ -161,6 +162,9 @@ object DungeonListener { if (text.stripControlCodes() .trim() == "> EXTRA STATS <" ) { + IO.launch { + WSClient.sendPacket(C2SPacketDungeonEnd(SBInfo.server ?: return@launch)) + } if (Skytils.config.dungeonDeathCounter) { tickTimer(6) { UChat.chat("§c☠ §lDeaths:§r ${team.values.sumOf { it.deaths }}\n${ From 21259ac10cbc31bbbf62317578ba21dddc5a89fa Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Sun, 19 May 2024 14:51:48 -0400 Subject: [PATCH 33/46] chore: update mod-api to 0.4.0 fix: various issues from hypixel mod api event registration fix: serverpayloadinterceptor uses incorrect duplication method fix: register packet fails to send in prod fix: hyevent registration on subsequent logins --- build.gradle.kts | 2 +- .../accessors/AccessorHypixelModAPI.java | 36 +++++++++ .../network/MixinNetworkManager.java | 1 + .../kotlin/gg/skytils/skytilsmod/Skytils.kt | 16 +--- .../commands/impl/SkytilsCommand.kt | 2 +- .../catlas/handlers/DungeonScanner.kt | 5 -- .../skytilsmod/listeners/DungeonListener.kt | 38 ++++----- .../listeners/ServerPayloadInterceptor.kt | 80 ++++++++++++++----- .../gg/skytils/skytilsmod/utils/SBInfo.kt | 3 - .../gg/skytils/skytilsmod/utils/Utils.kt | 3 + src/main/resources/mixins.skytils.json | 2 + 11 files changed, 123 insertions(+), 65 deletions(-) create mode 100644 src/main/java/gg/skytils/skytilsmod/mixins/transformers/accessors/AccessorHypixelModAPI.java diff --git a/build.gradle.kts b/build.gradle.kts index 53d28d6e2..c995a8c04 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -153,7 +153,7 @@ dependencies { exclude(module = "bcprov-jdk18on") } compileOnly("org.bouncycastle:bcprov-jdk18on:1.78.1") - shadowMe("net.hypixel:mod-api:0.3.2") + shadowMe("net.hypixel:mod-api:0.4.0") shadowMe(annotationProcessor("io.github.llamalad7:mixinextras-common:0.3.5")!!) diff --git a/src/main/java/gg/skytils/skytilsmod/mixins/transformers/accessors/AccessorHypixelModAPI.java b/src/main/java/gg/skytils/skytilsmod/mixins/transformers/accessors/AccessorHypixelModAPI.java new file mode 100644 index 000000000..f8315fb15 --- /dev/null +++ b/src/main/java/gg/skytils/skytilsmod/mixins/transformers/accessors/AccessorHypixelModAPI.java @@ -0,0 +1,36 @@ +/* + * Skytils - Hypixel Skyblock Quality of Life Mod + * Copyright (C) 2020-2024 Skytils + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package gg.skytils.skytilsmod.mixins.transformers.accessors; + +import net.hypixel.modapi.HypixelModAPI; +import net.hypixel.modapi.packet.HypixelPacket; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.gen.Invoker; + +import java.util.function.Predicate; + +@Mixin(HypixelModAPI.class) +public interface AccessorHypixelModAPI { + @Accessor + Predicate getPacketSender(); + + @Invoker + void invokeSendRegisterPacket(boolean alwaysSendIfNotEmpty); +} diff --git a/src/main/java/gg/skytils/skytilsmod/mixins/transformers/network/MixinNetworkManager.java b/src/main/java/gg/skytils/skytilsmod/mixins/transformers/network/MixinNetworkManager.java index 0ec81b92c..e7bd3fb28 100644 --- a/src/main/java/gg/skytils/skytilsmod/mixins/transformers/network/MixinNetworkManager.java +++ b/src/main/java/gg/skytils/skytilsmod/mixins/transformers/network/MixinNetworkManager.java @@ -19,6 +19,7 @@ package gg.skytils.skytilsmod.mixins.transformers.network; import gg.skytils.skytilsmod.mixins.hooks.network.NetworkManagerHookKt; +import gg.skytils.skytilsmod.utils.Utils; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import net.minecraft.network.EnumPacketDirection; diff --git a/src/main/kotlin/gg/skytils/skytilsmod/Skytils.kt b/src/main/kotlin/gg/skytils/skytilsmod/Skytils.kt index 5fa5e8d2c..50701e5e8 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/Skytils.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/Skytils.kt @@ -63,7 +63,6 @@ import gg.skytils.skytilsmod.gui.ReopenableGUI import gg.skytils.skytilsmod.listeners.ChatListener import gg.skytils.skytilsmod.listeners.DungeonListener import gg.skytils.skytilsmod.listeners.ServerPayloadInterceptor -import gg.skytils.skytilsmod.listeners.ServerPayloadInterceptor.getResponse import gg.skytils.skytilsmod.localapi.LocalAPI import gg.skytils.skytilsmod.mixins.extensions.ExtensionEntityLivingBase import gg.skytils.skytilsmod.mixins.hooks.entity.EntityPlayerSPHook @@ -92,8 +91,6 @@ import kotlinx.coroutines.* import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import kotlinx.serialization.modules.SerializersModule -import net.hypixel.modapi.packet.impl.clientbound.ClientboundPingPacket -import net.hypixel.modapi.packet.impl.serverbound.ServerboundPingPacket import net.minecraft.client.Minecraft import net.minecraft.client.gui.GuiButton import net.minecraft.client.gui.GuiGameOver @@ -552,15 +549,12 @@ class Skytils { @SubscribeEvent fun onConnect(event: FMLNetworkEvent.ClientConnectedToServerEvent) { + Utils.lastNHPC = event.handler as? NetHandlerPlayClient Utils.isOnHypixel = mc.runCatching { !event.isLocal && (thePlayer?.clientBrand?.lowercase()?.contains("hypixel") ?: currentServerData?.serverIP?.lowercase()?.contains("hypixel") ?: false) }.onFailure { it.printStackTrace() }.getOrDefault(false) - if (Utils.isOnHypixel) { - onJoinHypixel(event.handler as NetHandlerPlayClient) - } - IO.launch { TrophyFish.loadFromApi() } @@ -598,7 +592,6 @@ class Skytils { val brand = event.packet.bufferData.readStringFromBuffer(Short.MAX_VALUE.toInt()) if (brand.lowercase().contains("hypixel")) { Utils.isOnHypixel = true - onJoinHypixel(event.handler as NetHandlerPlayClient) } } if (Utils.inDungeons || !Utils.isOnHypixel || event.packet !is S38PacketPlayerListItem || @@ -617,14 +610,9 @@ class Skytils { } } - fun onJoinHypixel(handler: NetHandlerPlayClient) = IO.launch { - ServerboundPingPacket().getResponse(handler).let { packet -> - println("Hypixel Pong: ${packet.response}, version ${packet.version}") - } - } - @SubscribeEvent fun onDisconnect(event: FMLNetworkEvent.ClientDisconnectionFromServerEvent) { + Utils.lastNHPC = null Utils.isOnHypixel = false Utils.skyblock = false Utils.dungeons = false diff --git a/src/main/kotlin/gg/skytils/skytilsmod/commands/impl/SkytilsCommand.kt b/src/main/kotlin/gg/skytils/skytilsmod/commands/impl/SkytilsCommand.kt index 15f3a8e0a..20fa0b11e 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/commands/impl/SkytilsCommand.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/commands/impl/SkytilsCommand.kt @@ -396,7 +396,7 @@ object SkytilsCommand : BaseCommand("skytils", listOf("st")) { UChat.chat("$successPrefix §aPacket created: $packet") Skytils.IO.launch { runCatching { - packet.getResponse(mc.netHandler) + packet.getResponse() }.onFailure { UChat.chat("$failPrefix §cFailed to get packet response: ${it.message}") }.onSuccess { response -> diff --git a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/dungeons/catlas/handlers/DungeonScanner.kt b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/dungeons/catlas/handlers/DungeonScanner.kt index 57154b242..6bd51058c 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/dungeons/catlas/handlers/DungeonScanner.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/dungeons/catlas/handlers/DungeonScanner.kt @@ -25,14 +25,9 @@ import gg.skytils.skytilsmod.features.impl.dungeons.catlas.core.map.* import gg.skytils.skytilsmod.features.impl.dungeons.catlas.handlers.DungeonScanner.scan import gg.skytils.skytilsmod.features.impl.dungeons.catlas.utils.ScanUtils import gg.skytils.skytilsmod.listeners.DungeonListener -import gg.skytils.skytilsmod.listeners.ServerPayloadInterceptor.getResponse import gg.skytils.skytilsmod.utils.SBInfo -import gg.skytils.skytilsws.client.WSClient import gg.skytils.skytilsws.shared.packet.C2SPacketDungeonRoom -import gg.skytils.skytilsws.shared.packet.C2SPacketDungeonRoomSecret import kotlinx.coroutines.launch -import net.hypixel.modapi.packet.impl.clientbound.ClientboundLocationPacket -import net.hypixel.modapi.packet.impl.serverbound.ServerboundLocationPacket import net.minecraft.init.Blocks import net.minecraft.util.BlockPos diff --git a/src/main/kotlin/gg/skytils/skytilsmod/listeners/DungeonListener.kt b/src/main/kotlin/gg/skytils/skytilsmod/listeners/DungeonListener.kt index 049fefad1..626aef210 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/listeners/DungeonListener.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/listeners/DungeonListener.kt @@ -36,10 +36,10 @@ import gg.skytils.skytilsmod.features.impl.dungeons.DungeonFeatures import gg.skytils.skytilsmod.features.impl.dungeons.DungeonTimer import gg.skytils.skytilsmod.features.impl.dungeons.ScoreCalculation import gg.skytils.skytilsmod.features.impl.dungeons.catlas.core.DungeonMapPlayer +import gg.skytils.skytilsmod.features.impl.dungeons.catlas.core.map.Room import gg.skytils.skytilsmod.features.impl.dungeons.catlas.core.map.RoomType import gg.skytils.skytilsmod.features.impl.dungeons.catlas.handlers.DungeonInfo -import gg.skytils.skytilsmod.features.impl.dungeons.catlas.handlers.DungeonScanner -import gg.skytils.skytilsmod.features.impl.dungeons.catlas.utils.MapUtils +import gg.skytils.skytilsmod.features.impl.dungeons.catlas.utils.ScanUtils import gg.skytils.skytilsmod.features.impl.handlers.CooldownTracker import gg.skytils.skytilsmod.features.impl.handlers.SpiritLeap import gg.skytils.skytilsmod.listeners.ServerPayloadInterceptor.getResponse @@ -55,9 +55,7 @@ import gg.skytils.skytilsws.shared.packet.C2SPacketStartDungeon import kotlinx.coroutines.async import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import net.hypixel.modapi.packet.impl.clientbound.ClientboundLocationPacket import net.hypixel.modapi.packet.impl.clientbound.ClientboundPartyInfoPacket -import net.hypixel.modapi.packet.impl.serverbound.ServerboundLocationPacket import net.hypixel.modapi.packet.impl.serverbound.ServerboundPartyInfoPacket import net.minecraft.entity.player.EntityPlayer import net.minecraft.network.play.server.S02PacketChat @@ -66,7 +64,7 @@ import net.minecraftforge.client.event.ClientChatReceivedEvent import net.minecraftforge.event.world.WorldEvent import net.minecraftforge.fml.common.eventhandler.EventPriority import net.minecraftforge.fml.common.eventhandler.SubscribeEvent -import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.ConcurrentLinkedQueue object DungeonListener { val team = hashMapOf() @@ -116,7 +114,7 @@ object DungeonListener { private val keyPickupRegex = Regex("§r§e§lRIGHT CLICK §r§7on §r§7.+?§r§7 to open it\\. This key can only be used to open §r§a(?\\d+)§r§7 door!§r") private val witherDoorOpenedRegex = Regex("^(?:\\[.+?] )?(?\\w+) opened a WITHER door!$") private const val bloodOpenedString = "§r§cThe §r§c§lBLOOD DOOR§r§c has been opened!§r" - val outboundRoomQueue = arrayListOf() + val outboundRoomQueue = ConcurrentLinkedQueue() @SubscribeEvent fun onWorldLoad(event: WorldEvent.Unload) { @@ -144,14 +142,13 @@ object DungeonListener { DungeonFeatures.DungeonSecretDisplay.maxSecrets = max IO.launch { - val x = ((mc.thePlayer.posX - DungeonScanner.startX + 15) * MapUtils.coordMultiplier / (MapUtils.mapRoomSize + 4) * 2).toInt() - val z = ((mc.thePlayer.posZ - DungeonScanner.startZ + 15) * MapUtils.coordMultiplier / (MapUtils.mapRoomSize + 4) * 2).toInt() - - val room = DungeonInfo.uniqueRooms.find { it.tiles.any { it.x == x && it.z == z } } - - if (room != null && room.foundSecrets != sec) { - room.foundSecrets = sec - WSClient.sendPacket(C2SPacketDungeonRoomSecret(SBInfo.server ?: return@launch, room.mainRoom.data.name, sec)) + val tile = ScanUtils.getRoomFromPos(mc.thePlayer.position) + if (tile is Room && tile.data.name != "Unknown") { + val room = DungeonInfo.uniqueRooms.find { tile in it.tiles } ?: return@launch + if (room.foundSecrets != sec) { + room.foundSecrets = sec + WSClient.sendPacket(C2SPacketDungeonRoomSecret(SBInfo.server ?: return@launch, room.mainRoom.data.name, sec)) + } } } }.ifNull { @@ -205,26 +202,21 @@ object DungeonListener { IO.launch { delay(2000) if (DungeonTimer.dungeonStartTime != -1L) { - val location = async { - ServerboundLocationPacket().getResponse(mc.netHandler) - } val party = async { - ServerboundPartyInfoPacket().getResponse(mc.netHandler) + ServerboundPartyInfoPacket().getResponse() } val partyMembers = party.await().members.ifEmpty { setOf(mc.thePlayer.uniqueID) }.mapTo(hashSetOf()) { it.toString() } val entrance = DungeonInfo.uniqueRooms.first { it.mainRoom.data.type == RoomType.ENTRANCE } WSClient.sendPacket(C2SPacketStartDungeon( - serverId = location.await().serverName, + serverId = SBInfo.server ?: return@launch, floor = DungeonFeatures.dungeonFloor!!, members = partyMembers, startTime = DungeonTimer.dungeonStartTime, entranceLoc = entrance.mainRoom.z * entrance.mainRoom.x )) while (DungeonTimer.dungeonStartTime != -1L) { - val itr = outboundRoomQueue.iterator() - while (itr.hasNext()) { - val packet = itr.next() - itr.remove() + while (outboundRoomQueue.isNotEmpty()) { + val packet = outboundRoomQueue.poll() ?: continue WSClient.sendPacket(packet) } } diff --git a/src/main/kotlin/gg/skytils/skytilsmod/listeners/ServerPayloadInterceptor.kt b/src/main/kotlin/gg/skytils/skytilsmod/listeners/ServerPayloadInterceptor.kt index 4dcfe6117..5b3281660 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/listeners/ServerPayloadInterceptor.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/listeners/ServerPayloadInterceptor.kt @@ -18,22 +18,27 @@ package gg.skytils.skytilsmod.listeners +import gg.skytils.skytilsmod.Skytils import gg.skytils.skytilsmod.Skytils.Companion.IO +import gg.skytils.skytilsmod.Skytils.Companion.mc +import gg.skytils.skytilsmod.core.MC import gg.skytils.skytilsmod.events.impl.HypixelPacketEvent import gg.skytils.skytilsmod.events.impl.PacketEvent +import gg.skytils.skytilsmod.mixins.transformers.accessors.AccessorHypixelModAPI +import gg.skytils.skytilsmod.utils.Utils +import gg.skytils.skytilsmod.utils.ifNull import io.netty.buffer.Unpooled +import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import kotlinx.coroutines.withTimeout import net.hypixel.modapi.HypixelModAPI import net.hypixel.modapi.error.ErrorReason import net.hypixel.modapi.packet.ClientboundHypixelPacket -import net.hypixel.modapi.packet.HypixelPacket +import net.hypixel.modapi.packet.impl.clientbound.ClientboundHelloPacket +import net.hypixel.modapi.packet.impl.clientbound.event.ClientboundLocationPacket import net.hypixel.modapi.packet.impl.serverbound.ServerboundVersionedPacket import net.hypixel.modapi.serializer.PacketSerializer -import net.minecraft.client.network.NetHandlerPlayClient import net.minecraft.network.PacketBuffer import net.minecraft.network.play.client.C17PacketCustomPayload import net.minecraft.network.play.server.S3FPacketCustomPayload @@ -47,19 +52,29 @@ object ServerPayloadInterceptor { @SubscribeEvent(priority = EventPriority.HIGHEST) fun onReceivePacket(event: PacketEvent.ReceiveEvent) { if (event.packet is S3FPacketCustomPayload) { - IO.launch { - val registry = HypixelModAPI.getInstance().registry - val id = event.packet.channelName - if (registry.isRegistered(id)) { - val packetSerializer = PacketSerializer(event.packet.bufferData.duplicate()) - if (!packetSerializer.readBoolean()) { - val reason = ErrorReason.getById(packetSerializer.readVarInt()) - HypixelPacketEvent.FailedEvent(id, reason).postAndCatch() - } else { - val packet = registry.createClientboundPacket(id, packetSerializer) - receivedPackets.emit(packet) - HypixelPacketEvent.ReceiveEvent(packet).postAndCatch() + val registry = HypixelModAPI.getInstance().registry + val id = event.packet.channelName + if (registry.isRegistered(id)) { + println("Received Hypixel packet $id") + val data = event.packet.bufferData + synchronized(data) { + data.retain() + runCatching { + val packetSerializer = PacketSerializer(data.duplicate()) + if (!packetSerializer.readBoolean()) { + val reason = ErrorReason.getById(packetSerializer.readVarInt()) + HypixelPacketEvent.FailedEvent(id, reason).postAndCatch() + } else { + val packet = registry.createClientboundPacket(id, packetSerializer) + IO.launch { + receivedPackets.emit(packet) + } + HypixelPacketEvent.ReceiveEvent(packet).postAndCatch() + } + }.onFailure { + it.printStackTrace() } + data.release() } } } @@ -71,11 +86,38 @@ object ServerPayloadInterceptor { val registry = HypixelModAPI.getInstance().registry val id = event.packet.channelName if (registry.isRegistered(id)) { + println("Sent Hypixel packet $id") HypixelPacketEvent.SendEvent(id).postAndCatch() } } } + @SubscribeEvent + fun onHypixelPacket(event: HypixelPacketEvent.ReceiveEvent) { + if (event.packet is ClientboundHelloPacket) { + val modAPI = HypixelModAPI.getInstance() + modAPI as AccessorHypixelModAPI + if (modAPI.packetSender == null) { + println("Hypixel Mod API packet sender is not set, Skytils will set the packet sender.") + modAPI.setPacketSender { + return@setPacketSender getNetClientHandler()?.addToSendQueue((it as ServerboundVersionedPacket).toCustomPayload()).ifNull { + println("Failed to send packet ${it.identifier}") + } != null + } + } + Skytils.launch { + while (getNetClientHandler() == null) { + println("Waiting for client handler to be set.") + delay(50L) + } + withContext(Dispatchers.MC) { + modAPI.subscribeToEventPacket(ClientboundLocationPacket::class.java) + modAPI.invokeSendRegisterPacket(true) + } + } + } + } + fun ServerboundVersionedPacket.toCustomPayload(): C17PacketCustomPayload { val buffer = PacketBuffer(Unpooled.buffer()) val serializer = PacketSerializer(buffer) @@ -83,9 +125,11 @@ object ServerPayloadInterceptor { return C17PacketCustomPayload(this.identifier, buffer) } - suspend fun ServerboundVersionedPacket.getResponse(handler: NetHandlerPlayClient): T = withTimeout(1.minutes) { + suspend fun ServerboundVersionedPacket.getResponse(): T = withTimeout(1.minutes) { val packet: C17PacketCustomPayload = this@getResponse.toCustomPayload() - handler.addToSendQueue(packet) + getNetClientHandler()?.addToSendQueue(packet) return@withTimeout receivedPackets.filter { it.identifier == this@getResponse.identifier }.first() as T } + + private fun getNetClientHandler() = mc.netHandler ?: Utils.lastNHPC } \ No newline at end of file diff --git a/src/main/kotlin/gg/skytils/skytilsmod/utils/SBInfo.kt b/src/main/kotlin/gg/skytils/skytilsmod/utils/SBInfo.kt index 320596b72..794914468 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/utils/SBInfo.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/utils/SBInfo.kt @@ -19,12 +19,10 @@ package gg.skytils.skytilsmod.utils import gg.skytils.skytilsmod.Skytils.Companion.mc import gg.skytils.skytilsmod.events.impl.HypixelPacketEvent -import gg.skytils.skytilsmod.listeners.ServerPayloadInterceptor.toCustomPayload import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* -import net.hypixel.modapi.HypixelModAPI import net.hypixel.modapi.packet.impl.clientbound.event.ClientboundLocationPacket import net.minecraft.client.gui.inventory.GuiChest import net.minecraft.inventory.ContainerChest @@ -76,7 +74,6 @@ object SBInfo { @SubscribeEvent fun onWorldChange(event: WorldEvent.Unload) { lastOpenContainerName = null - lastLocationRequest = -1 } @SubscribeEvent diff --git a/src/main/kotlin/gg/skytils/skytilsmod/utils/Utils.kt b/src/main/kotlin/gg/skytils/skytilsmod/utils/Utils.kt index f5f2f458c..b1b867963 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/utils/Utils.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/utils/Utils.kt @@ -42,6 +42,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.launch import net.minecraft.client.gui.ChatLine import net.minecraft.client.gui.GuiNewChat +import net.minecraft.client.network.NetHandlerPlayClient import net.minecraft.client.settings.GameSettings import net.minecraft.entity.Entity import net.minecraft.entity.EntityLivingBase @@ -89,6 +90,8 @@ object Utils { @JvmField var lastRenderedSkullEntity: EntityLivingBase? = null + var lastNHPC: NetHandlerPlayClient? = null + @JvmStatic var random = Random() diff --git a/src/main/resources/mixins.skytils.json b/src/main/resources/mixins.skytils.json index 7b52269bd..571f6211c 100644 --- a/src/main/resources/mixins.skytils.json +++ b/src/main/resources/mixins.skytils.json @@ -20,6 +20,7 @@ "accessors.AccessorGuiMainMenu", "accessors.AccessorGuiNewChat", "accessors.AccessorGuiStreamUnavailable", + "accessors.AccessorHypixelModAPI", "accessors.AccessorHypixelPacketRegistry", "accessors.AccessorMinecraft", "accessors.AccessorModelDragon", @@ -96,6 +97,7 @@ ], "verbose": true, "client": [ + "accessors.AccessorPlayerControllerMP", "gui.MixinGuiEditSign", "util.MixinMouseHelper" ] From ebceef89205a8a098192694d2f3dc86a710cfce9 Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Tue, 21 May 2024 18:08:40 -0400 Subject: [PATCH 34/46] perf: only send dungeon data if there are teammates --- .../skytilsmod/listeners/DungeonListener.kt | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/gg/skytils/skytilsmod/listeners/DungeonListener.kt b/src/main/kotlin/gg/skytils/skytilsmod/listeners/DungeonListener.kt index 626aef210..de2472857 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/listeners/DungeonListener.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/listeners/DungeonListener.kt @@ -115,6 +115,7 @@ object DungeonListener { private val witherDoorOpenedRegex = Regex("^(?:\\[.+?] )?(?\\w+) opened a WITHER door!$") private const val bloodOpenedString = "§r§cThe §r§c§lBLOOD DOOR§r§c has been opened!§r" val outboundRoomQueue = ConcurrentLinkedQueue() + var isSoloDungeon = false @SubscribeEvent fun onWorldLoad(event: WorldEvent.Unload) { @@ -125,6 +126,7 @@ object DungeonListener { completedPuzzles.clear() teamCached.clear() outboundRoomQueue.clear() + isSoloDungeon = false } @SubscribeEvent @@ -141,13 +143,15 @@ object DungeonListener { DungeonFeatures.DungeonSecretDisplay.secrets = sec DungeonFeatures.DungeonSecretDisplay.maxSecrets = max - IO.launch { - val tile = ScanUtils.getRoomFromPos(mc.thePlayer.position) - if (tile is Room && tile.data.name != "Unknown") { - val room = DungeonInfo.uniqueRooms.find { tile in it.tiles } ?: return@launch - if (room.foundSecrets != sec) { - room.foundSecrets = sec - WSClient.sendPacket(C2SPacketDungeonRoomSecret(SBInfo.server ?: return@launch, room.mainRoom.data.name, sec)) + if (team.size > 1) { + IO.launch { + val tile = ScanUtils.getRoomFromPos(mc.thePlayer.position) + if (tile is Room && tile.data.name != "Unknown") { + val room = DungeonInfo.uniqueRooms.find { tile in it.tiles } ?: return@launch + if (room.foundSecrets != sec) { + room.foundSecrets = sec + WSClient.sendPacket(C2SPacketDungeonRoomSecret(SBInfo.server ?: return@launch, room.mainRoom.data.name, sec)) + } } } } @@ -159,8 +163,10 @@ object DungeonListener { if (text.stripControlCodes() .trim() == "> EXTRA STATS <" ) { - IO.launch { - WSClient.sendPacket(C2SPacketDungeonEnd(SBInfo.server ?: return@launch)) + if (team.size > 1) { + IO.launch { + WSClient.sendPacket(C2SPacketDungeonEnd(SBInfo.server ?: return@launch)) + } } if (Skytils.config.dungeonDeathCounter) { tickTimer(6) { @@ -201,7 +207,7 @@ object DungeonListener { } else if (text == "§r§aStarting in 1 second.§r") { IO.launch { delay(2000) - if (DungeonTimer.dungeonStartTime != -1L) { + if (DungeonTimer.dungeonStartTime != -1L && team.size > 1) { val party = async { ServerboundPartyInfoPacket().getResponse() } From b019d62562e02068b244f859635c0b9d8f7c032f Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Wed, 22 May 2024 00:05:48 -0400 Subject: [PATCH 35/46] feat: add found room secrets to Catlas --- .../impl/dungeons/catlas/core/CatlasConfig.kt | 11 +++++++++++ .../impl/dungeons/catlas/core/CatlasElement.kt | 13 +++++++++---- .../skytilsmod/listeners/DungeonListener.kt | 15 +++++++-------- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/dungeons/catlas/core/CatlasConfig.kt b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/dungeons/catlas/core/CatlasConfig.kt index 50bf6cc0e..e69b81d5e 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/dungeons/catlas/core/CatlasConfig.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/dungeons/catlas/core/CatlasConfig.kt @@ -226,6 +226,17 @@ object CatlasConfig : Vigilant( ) var mapRoomSecrets = 0 + // TODO: Add translation + @Property( + name = "Found Room Secrets", + type = PropertyType.SELECTOR, + description = "Shows found secrets of rooms on map.", + category = "Rooms", + options = ["Off", "On", "Replace Total"], + i18nCategory = "catlas.config.rooms" + ) + var foundRoomSecrets = 0 + @Property( name = "Color Text", type = PropertyType.SWITCH, diff --git a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/dungeons/catlas/core/CatlasElement.kt b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/dungeons/catlas/core/CatlasElement.kt index 293aa2498..3877ee4af 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/dungeons/catlas/core/CatlasElement.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/dungeons/catlas/core/CatlasElement.kt @@ -142,7 +142,7 @@ object CatlasElement : GuiElement(name = "Dungeon Map", x = 0, y = 0) { DungeonInfo.uniqueRooms.forEach { unq -> val room = unq.mainRoom - if (room.state == RoomState.UNDISCOVERED) return@forEach + if (room.state == RoomState.UNDISCOVERED || room.state == RoomState.UNOPENED) return@forEach val size = MapUtils.mapRoomSize + DungeonMapColorParser.quarterRoom val checkPos = unq.getCheckmarkPosition() val namePos = unq.getNamePosition() @@ -161,7 +161,12 @@ object CatlasElement : GuiElement(name = "Dungeon Map", x = 0, y = 0) { val roomType = room.data.type val hasSecrets = secretCount > 0 - if (room.state == RoomState.UNOPENED) return@forEach + val secretText = when (CatlasConfig.foundRoomSecrets) { + 0 -> secretCount.toString() + 1 -> "${unq.foundSecrets ?: "?"}/${secretCount}" + 2 -> unq.foundSecrets?.toString() ?: "?" + else -> error("Invalid foundRoomSecrets value") + } if (CatlasConfig.mapRoomSecrets == 2 && hasSecrets) { GlStateManager.pushMatrix() @@ -171,7 +176,7 @@ object CatlasElement : GuiElement(name = "Dungeon Map", x = 0, y = 0) { 0f ) GlStateManager.scale(2f, 2f, 1f) - RenderUtils.renderCenteredText(listOf(secretCount.toString()), 0, 0, color) + RenderUtils.renderCenteredText(listOf(secretText), 0, 0, color) GlStateManager.popMatrix() } else if (CatlasConfig.mapCheckmark != 0) { drawCheckmark(room, xOffsetCheck, yOffsetCheck, checkmarkSize) @@ -191,7 +196,7 @@ object CatlasElement : GuiElement(name = "Dungeon Map", x = 0, y = 0) { name.addAll(room.data.name.split(" ")) } if (room.data.type == RoomType.NORMAL && CatlasConfig.mapRoomSecrets == 1) { - name.add(secretCount.toString()) + name.add(secretText) } // Offset + half of roomsize RenderUtils.renderCenteredText( diff --git a/src/main/kotlin/gg/skytils/skytilsmod/listeners/DungeonListener.kt b/src/main/kotlin/gg/skytils/skytilsmod/listeners/DungeonListener.kt index de2472857..9e0bfedce 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/listeners/DungeonListener.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/listeners/DungeonListener.kt @@ -143,15 +143,14 @@ object DungeonListener { DungeonFeatures.DungeonSecretDisplay.secrets = sec DungeonFeatures.DungeonSecretDisplay.maxSecrets = max - if (team.size > 1) { - IO.launch { - val tile = ScanUtils.getRoomFromPos(mc.thePlayer.position) - if (tile is Room && tile.data.name != "Unknown") { - val room = DungeonInfo.uniqueRooms.find { tile in it.tiles } ?: return@launch - if (room.foundSecrets != sec) { - room.foundSecrets = sec + IO.launch { + val tile = ScanUtils.getRoomFromPos(mc.thePlayer.position) + if (tile is Room && tile.data.name != "Unknown") { + val room = DungeonInfo.uniqueRooms.find { tile in it.tiles } ?: return@launch + if (room.foundSecrets != sec) { + room.foundSecrets = sec + if (team.size > 1) WSClient.sendPacket(C2SPacketDungeonRoomSecret(SBInfo.server ?: return@launch, room.mainRoom.data.name, sec)) - } } } } From a6c044db683257b2c0ae258e19509e4bdab137b1 Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Wed, 22 May 2024 18:01:53 -0400 Subject: [PATCH 36/46] refactor: move CH Waypoints to their own class --- .../kotlin/gg/skytils/skytilsmod/Skytils.kt | 2 + .../commands/impl/HollowWaypointCommand.kt | 18 +- .../features/impl/mining/CHWaypoints.kt | 355 ++++++++++++++++++ .../features/impl/mining/MiningFeatures.kt | 299 --------------- 4 files changed, 366 insertions(+), 308 deletions(-) create mode 100644 src/main/kotlin/gg/skytils/skytilsmod/features/impl/mining/CHWaypoints.kt diff --git a/src/main/kotlin/gg/skytils/skytilsmod/Skytils.kt b/src/main/kotlin/gg/skytils/skytilsmod/Skytils.kt index 50701e5e8..fed9022f9 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/Skytils.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/Skytils.kt @@ -46,6 +46,7 @@ import gg.skytils.skytilsmod.features.impl.farming.TreasureHunter import gg.skytils.skytilsmod.features.impl.farming.VisitorHelper import gg.skytils.skytilsmod.features.impl.funny.Funny import gg.skytils.skytilsmod.features.impl.handlers.* +import gg.skytils.skytilsmod.features.impl.mining.CHWaypoints import gg.skytils.skytilsmod.features.impl.mining.MiningFeatures import gg.skytils.skytilsmod.features.impl.mining.StupidTreasureChestOpeningThing import gg.skytils.skytilsmod.features.impl.misc.* @@ -328,6 +329,7 @@ class Skytils { BoulderSolver, ChatTabs, ChangeAllToSameColorSolver, + CHWaypoints, DungeonChestProfit, ClickInOrderSolver, NamespacedCommands, diff --git a/src/main/kotlin/gg/skytils/skytilsmod/commands/impl/HollowWaypointCommand.kt b/src/main/kotlin/gg/skytils/skytilsmod/commands/impl/HollowWaypointCommand.kt index 55c403356..8841206e7 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/commands/impl/HollowWaypointCommand.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/commands/impl/HollowWaypointCommand.kt @@ -27,7 +27,7 @@ import gg.skytils.skytilsmod.Skytils.Companion.mc import gg.skytils.skytilsmod.Skytils.Companion.prefix import gg.skytils.skytilsmod.Skytils.Companion.successPrefix import gg.skytils.skytilsmod.commands.BaseCommand -import gg.skytils.skytilsmod.features.impl.mining.MiningFeatures +import gg.skytils.skytilsmod.features.impl.mining.CHWaypoints import gg.skytils.skytilsmod.utils.append import gg.skytils.skytilsmod.utils.setHoverText import net.minecraft.client.entity.EntityPlayerSP @@ -47,13 +47,13 @@ object HollowWaypointCommand : BaseCommand("skytilshollowwaypoint", listOf("sthw } if (args.isEmpty()) { val message = UMessage("$prefix §eWaypoints:\n") - for (loc in MiningFeatures.CrystalHollowsMap.Locations.entries) { + for (loc in CHWaypoints.CrystalHollowsMap.Locations.entries) { if (!loc.loc.exists()) continue message.append("${loc.displayName} ") message.append(copyMessage("${loc.cleanName}: ${loc.loc}")) message.append(removeMessage(loc.id)) } - for ((key, value) in MiningFeatures.waypoints) { + for ((key, value) in CHWaypoints.waypoints) { message.append("§e$key ") message.append(copyMessage("$key: ${value.x} ${value.y} ${value.z}")) message.append(removeMessage(key)) @@ -81,13 +81,13 @@ object HollowWaypointCommand : BaseCommand("skytilshollowwaypoint", listOf("sthw y = match.groups["y"]!!.value.toDouble() z = match.groups["z"]!!.value.toDouble() } - val internalLoc = MiningFeatures.CrystalHollowsMap.Locations.entries.find { it.id == loc }?.loc + val internalLoc = CHWaypoints.CrystalHollowsMap.Locations.entries.find { it.id == loc }?.loc if (internalLoc != null) { internalLoc.locX = (x - 200).coerceIn(0.0, 624.0) internalLoc.locY = y internalLoc.locZ = (z - 200).coerceIn(0.0, 624.0) } else { - MiningFeatures.waypoints[loc] = BlockPos(x, y, z) + CHWaypoints.waypoints[loc] = BlockPos(x, y, z) } UChat.chat("$successPrefix §aSuccessfully created waypoint $loc") } @@ -95,11 +95,11 @@ object HollowWaypointCommand : BaseCommand("skytilshollowwaypoint", listOf("sthw "remove", "delete" -> { if (args.size >= 2) { val name = args.drop(1).joinToString(" ") - if (MiningFeatures.CrystalHollowsMap.Locations.entries + if (CHWaypoints.CrystalHollowsMap.Locations.entries .find { it.id == name }?.loc?.reset() != null ) UChat.chat("$successPrefix §aSuccessfully removed waypoint ${name}!") - else if (MiningFeatures.waypoints.remove(name) != null) + else if (CHWaypoints.waypoints.remove(name) != null) UChat.chat("$successPrefix §aSuccessfully removed waypoint $name") else UChat.chat("$failPrefix §cWaypoint $name does not exist") @@ -108,8 +108,8 @@ object HollowWaypointCommand : BaseCommand("skytilshollowwaypoint", listOf("sthw } "clear" -> { - MiningFeatures.CrystalHollowsMap.Locations.entries.forEach { it.loc.reset() } - MiningFeatures.waypoints.clear() + CHWaypoints.CrystalHollowsMap.Locations.entries.forEach { it.loc.reset() } + CHWaypoints.waypoints.clear() UChat.chat("$successPrefix §aSuccessfully cleared all waypoints.") } diff --git a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/mining/CHWaypoints.kt b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/mining/CHWaypoints.kt new file mode 100644 index 000000000..b7020a162 --- /dev/null +++ b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/mining/CHWaypoints.kt @@ -0,0 +1,355 @@ +/* + * Skytils - Hypixel Skyblock Quality of Life Mod + * Copyright (C) 2020-2024 Skytils + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package gg.skytils.skytilsmod.features.impl.mining + +import gg.essential.universal.UChat +import gg.essential.universal.UGraphics +import gg.essential.universal.UMatrixStack +import gg.essential.universal.utils.MCClickEventAction +import gg.essential.universal.wrappers.message.UMessage +import gg.essential.universal.wrappers.message.UTextComponent +import gg.skytils.skytilsmod.Skytils +import gg.skytils.skytilsmod.Skytils.Companion.mc +import gg.skytils.skytilsmod.Skytils.Companion.prefix +import gg.skytils.skytilsmod.core.structure.GuiElement +import gg.skytils.skytilsmod.events.impl.PacketEvent +import gg.skytils.skytilsmod.features.impl.handlers.MayorInfo +import gg.skytils.skytilsmod.utils.* +import gg.skytils.skytilsmod.utils.graphics.colors.ColorFactory +import net.minecraft.client.entity.EntityOtherPlayerMP +import net.minecraft.client.renderer.GlStateManager +import net.minecraft.client.renderer.vertex.DefaultVertexFormats +import net.minecraft.entity.EntityLivingBase +import net.minecraft.network.play.server.S08PacketPlayerPosLook +import net.minecraft.util.BlockPos +import net.minecraft.util.ResourceLocation +import net.minecraftforge.client.event.ClientChatReceivedEvent +import net.minecraftforge.client.event.RenderLivingEvent +import net.minecraftforge.client.event.RenderWorldLastEvent +import net.minecraftforge.event.world.WorldEvent +import net.minecraftforge.fml.common.eventhandler.EventPriority +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import net.minecraftforge.fml.common.gameevent.TickEvent +import net.minecraftforge.fml.common.gameevent.TickEvent.ClientTickEvent + +object CHWaypoints { + var lastTPLoc: BlockPos? = null + var waypoints = hashMapOf() + var waypointDelayTicks = 0 + private val SBE_DSM_PATTERN = + Regex("\\\$(?:SBECHWP\\b|DSMCHWP):(?.*?)@-(?-?\\d+),(?-?\\d+),(?-?\\d+)") + private val xyzPattern = + Regex(".*?(?[a-zA-Z0-9_]{3,16}):.*?(?[0-9]{1,3}),? (?:y: )?(?[0-9]{1,3}),? (?:z: )?(?[0-9]{1,3}).*?") + private val xzPattern = + Regex(".*(?[a-zA-Z0-9_]{3,16}):.* (?[0-9]{1,3}),? (?[0-9]{1,3}).*") + + @SubscribeEvent + fun onReceivePacket(event: PacketEvent.ReceiveEvent) { + if (!Utils.inSkyblock) return + if (Skytils.config.crystalHollowDeathWaypoint && event.packet is S08PacketPlayerPosLook && mc.thePlayer != null) { + lastTPLoc = mc.thePlayer.position + } + } + + @SubscribeEvent(priority = EventPriority.HIGHEST, receiveCanceled = true) + fun onChat(event: ClientChatReceivedEvent) { + if (!Utils.inSkyblock || event.type == 2.toByte()) return + val unformatted = event.message.unformattedText.stripControlCodes() + if (Skytils.config.hollowChatCoords && SBInfo.mode == SkyblockIsland.CrystalHollows.mode) { + xyzPattern.find(unformatted)?.groups?.let { + waypointChatMessage(it["x"]!!.value, it["y"]!!.value, it["z"]!!.value) + return + } + xzPattern.find(unformatted)?.groups?.let { + waypointChatMessage(it["x"]!!.value, "100", it["z"]!!.value) + return + } + + /** + * Checks for the format used in DSM and SBE + * $DSMCHWP:Mines of Divan@-673,117,426 ✔ + * $SBECHWP:Khazad-dûm@-292,63,281 ✔ + * $asdf:Khazad-dûm@-292,63,281 ❌ + * $SBECHWP:Khazad-dûm@asdf,asdf,asdf ❌ + */ + val cleaned = SBE_DSM_PATTERN.find(unformatted) + if (cleaned != null) { + val stringLocation = cleaned.groups["stringLocation"]!!.value + val x = cleaned.groups["x"]!!.value + val y = cleaned.groups["y"]!!.value + val z = cleaned.groups["z"]!!.value + CrystalHollowsMap.Locations.entries.find { it.cleanName == stringLocation } + ?.takeIf { !it.loc.exists() }?.let { loc -> + /** + * Sends the waypoints message except it suggests which one should be used based on + * the name contained in the message and converts it to the internally used names for the waypoints. + */ + UMessage("§3Skytils > §eFound coordinates in a chat message, click a button to set a waypoint.\n") + .append( + UTextComponent("§f${loc.displayName} ") + .setClick( + MCClickEventAction.RUN_COMMAND, + "/skytilshollowwaypoint set $x $y $z ${loc.id}" + ) + .setHoverText("§eSet waypoint for ${loc.displayName}") + ) + .append( + UTextComponent("§e[Custom]") + .setClick( + MCClickEventAction.SUGGEST_COMMAND, + "/skytilshollowwaypoint set $x $y $z name_here" + ) + .setHoverText("§eSet custom waypoint") + ).chat() + } + } + } + if ((Skytils.config.crystalHollowWaypoints || Skytils.config.crystalHollowMapPlaces) && Skytils.config.kingYolkarWaypoint && SBInfo.mode == SkyblockIsland.CrystalHollows.mode + && mc.thePlayer != null && unformatted.startsWith("[NPC] King Yolkar:") + ) { + CrystalHollowsMap.Locations.KingYolkar.loc.set() + } + if (unformatted.startsWith("You died") || unformatted.startsWith("☠ You were killed")) { + waypointDelayTicks = + 50 //this is to make sure the scoreboard has time to update and nothing moves halfway across the map + if (Skytils.config.crystalHollowDeathWaypoint && SBInfo.mode == SkyblockIsland.CrystalHollows.mode && lastTPLoc != null) { + UChat.chat( + UTextComponent("$prefix §eClick to set a death waypoint at ${lastTPLoc!!.x} ${lastTPLoc!!.y} ${lastTPLoc!!.z}").setClick( + MCClickEventAction.RUN_COMMAND, + "/sthw set ${lastTPLoc!!.x} ${lastTPLoc!!.y} ${lastTPLoc!!.z} Last Death" + ) + ) + } + } else if (unformatted.startsWith("Warp")) { + waypointDelayTicks = 50 + } + } + + private fun waypointChatMessage(x: String, y: String, z: String) { + val message = UMessage( + "$prefix §eFound coordinates in a chat message, click a button to set a waypoint.\n" + ) + for (loc in CrystalHollowsMap.Locations.entries) { + if (loc.loc.exists()) continue + message.append( + UTextComponent("${loc.displayName.substring(0, 2)}[${loc.displayName}] ") + .setClick(MCClickEventAction.SUGGEST_COMMAND, "/sthw set $x $y $z ${loc.id}") + .setHoverText("§eSet waypoint for ${loc.cleanName}") + ) + } + message.append( + UTextComponent("§e[Custom]").setClick( + MCClickEventAction.SUGGEST_COMMAND, + "/sthw set $x $y $z Name" + ).setHoverText("§eSet waypoint for custom location") + ) + message.chat() + } + + @SubscribeEvent + fun onRenderWorld(event: RenderWorldLastEvent) { + if (!Utils.inSkyblock) return + val matrixStack = UMatrixStack() + if (Skytils.config.crystalHollowWaypoints && SBInfo.mode == SkyblockIsland.CrystalHollows.mode) { + GlStateManager.disableDepth() + for (loc in CrystalHollowsMap.Locations.entries) { + loc.loc.drawWaypoint(loc.cleanName, event.partialTicks, matrixStack) + } + RenderUtil.renderWaypointText("Crystal Nucleus", 513.5, 107.0, 513.5, event.partialTicks, matrixStack) + for ((key, value) in waypoints) + RenderUtil.renderWaypointText(key, value, event.partialTicks, matrixStack) + GlStateManager.enableDepth() + } + } + + @SubscribeEvent + fun onRenderLivingPre(event: RenderLivingEvent.Pre) { + if (!Utils.inSkyblock) return + if (Skytils.config.crystalHollowWaypoints && + event.entity is EntityOtherPlayerMP && + event.entity.name == "Team Treasurite" && + mc.thePlayer.canEntityBeSeen(event.entity) && + event.entity.baseMaxHealth == if (MayorInfo.mayorPerks.contains("DOUBLE MOBS HP!!!")) 2_000_000.0 else 1_000_000.0 + ) { + waypoints["Corleone"] = event.entity.position + } + } + + @SubscribeEvent(priority = EventPriority.LOW) // priority low so it always runs after sbinfo is updated + fun onTick(event: ClientTickEvent) { + if (!Utils.inSkyblock || event.phase != TickEvent.Phase.START) return + if ((Skytils.config.crystalHollowWaypoints || Skytils.config.crystalHollowMapPlaces) && SBInfo.mode == SkyblockIsland.CrystalHollows.mode + && waypointDelayTicks == 0 && mc.thePlayer != null + ) { + CrystalHollowsMap.Locations.cleanNameToLocation[SBInfo.location]?.loc?.set() + } else if (waypointDelayTicks > 0) + waypointDelayTicks-- + } + + @SubscribeEvent + fun onWorldChange(event: WorldEvent.Unload) { + CrystalHollowsMap.Locations.entries.forEach { it.loc.reset() } + waypoints.clear() + } + + + class CrystalHollowsMap : GuiElement(name = "Crystal Hollows Map", x = 0, y = 0) { + val mapLocation = ResourceLocation("skytils", "crystalhollowsmap.png") + + enum class Locations(val displayName: String, val id: String, val color: Int, val size: Int = 50) { + LostPrecursorCity("§fLost Precursor City", "internal_city", ColorFactory.WHITE.rgb), + JungleTemple("§aJungle Temple", "internal_temple", ColorFactory.GREEN.rgb), + GoblinQueensDen("§eGoblin Queen's Den", "internal_den", ColorFactory.YELLOW.rgb), + MinesOfDivan("§9Mines of Divan", "internal_mines", ColorFactory.BLUE.rgb), + KingYolkar("§6King Yolkar", "internal_king", ColorFactory.ORANGE.rgb, 25), + KhazadDum("§cKhazad-dûm", "internal_bal", ColorFactory.RED.rgb), + FairyGrotto("§dFairy Grotto", "internal_fairy", ColorFactory.PINK.rgb, 26), + Corleone("§bCorleone", "internal_corleone", ColorFactory.AQUA.rgb, 26); + + val loc = LocationObject() + val cleanName = displayName.stripControlCodes() + + companion object { + val cleanNameToLocation = entries.associateBy { it.cleanName } + } + } + + override fun render() { + if (!toggled || SBInfo.mode != SkyblockIsland.CrystalHollows.mode || mc.thePlayer == null) return + val stack = UMatrixStack() + UMatrixStack.Compat.runLegacyMethod(stack) { + stack.scale(0.1, 0.1, 1.0) + UGraphics.disableLighting() + stack.runWithGlobalState { + RenderUtil.renderTexture(mapLocation, 0, 0, 624, 624, false) + if (Skytils.config.crystalHollowMapPlaces) { + Locations.entries.forEach { + it.loc.drawOnMap(it.size, it.color) + } + } + } + val x = (mc.thePlayer.posX - 202).coerceIn(0.0, 624.0) + val y = (mc.thePlayer.posZ - 202).coerceIn(0.0, 624.0) + + // player marker code + val wr = UGraphics.getFromTessellator() + mc.textureManager.bindTexture(ResourceLocation("textures/map/map_icons.png")) + + stack.push() + stack.translate(x, y, 0.0) + + // Rotate about the center to match the player's yaw + stack.rotate((mc.thePlayer.rotationYawHead + 180f) % 360f, 0f, 0f, 1f) + stack.scale(1.5f, 1.5f, 1.5f) + stack.translate(-0.125f, 0.125f, 0.0f) + UGraphics.color4f(1f, 1f, 1f, 1f) + UGraphics.enableAlpha() + val d1 = 0.0 + val d2 = 0.25 + wr.beginWithActiveShader(UGraphics.DrawMode.QUADS, DefaultVertexFormats.POSITION_TEX) + wr.pos(stack, -8.0, -8.0, 100.0).tex(d1, d1).endVertex() + wr.pos(stack, -8.0, 8.0, 100.0).tex(d1, d2).endVertex() + wr.pos(stack, 8.0, 8.0, 100.0).tex(d2, d2).endVertex() + wr.pos(stack, 8.0, -8.0, 100.0).tex(d2, d1).endVertex() + wr.drawDirect() + stack.pop() + } + } + + override fun demoRender() { + UGraphics.disableLighting() + RenderUtil.renderTexture(mapLocation, 0, 0, 62, 62, false) + } + + override val toggled: Boolean + get() = Skytils.config.crystalHollowMap + override val height: Int + get() = 62 // should be 62.4 but oh well + override val width: Int + get() = 62 + + init { + Skytils.guiManager.registerElement(this) + } + } + + init { + CrystalHollowsMap() + } + + class LocationObject { + var locX: Double? = null + var locY: Double? = null + var locZ: Double? = null + private var locMinX: Double = 1100.0 + private var locMinY: Double = 1100.0 + private var locMinZ: Double = 1100.0 + private var locMaxX: Double = -100.0 + private var locMaxY: Double = -100.0 + private var locMaxZ: Double = -100.0 + + fun reset() { + locX = null + locY = null + locZ = null + locMinX = 1100.0 + locMinY = 1100.0 + locMinZ = 1100.0 + locMaxX = -100.0 + locMaxY = -100.0 + locMaxZ = -100.0 + } + + fun set() { + locMinX = (mc.thePlayer.posX - 200).coerceIn(0.0, 624.0).coerceAtMost(locMinX) + locMinY = mc.thePlayer.posY.coerceIn(0.0, 256.0).coerceAtMost(locMinY) + locMinZ = (mc.thePlayer.posZ - 200).coerceIn(0.0, 624.0).coerceAtMost(locMinZ) + locMaxX = (mc.thePlayer.posX - 200).coerceIn(0.0, 624.0).coerceAtLeast(locMaxX) + locMaxY = mc.thePlayer.posY.coerceIn(0.0, 256.0).coerceAtLeast(locMaxY) + locMaxZ = (mc.thePlayer.posZ - 200).coerceIn(0.0, 624.0).coerceAtLeast(locMaxZ) + locX = (locMinX + locMaxX) / 2 + locY = (locMinY + locMaxY) / 2 + locZ = (locMinZ + locMaxZ) / 2 + } + + fun exists(): Boolean { + return locX != null && locY != null && locZ != null + } + + fun drawWaypoint(text: String, partialTicks: Float, matrixStack: UMatrixStack) { + if (exists()) + RenderUtil.renderWaypointText(text, locX!! + 200, locY!!, locZ!! + 200, partialTicks, matrixStack) + } + + fun drawOnMap(size: Int, color: Int) { + if (exists()) + RenderUtil.drawRect(locX!! - size, locZ!! - size, locX!! + size, locZ!! + size, color) + } + + override fun toString(): String { + return String.format("%.0f", locX?.plus(200)) + " " + String.format( + "%.0f", + locY + ) + " " + String.format( + "%.0f", + locZ?.plus(200) + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/mining/MiningFeatures.kt b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/mining/MiningFeatures.kt index 35bca0c4c..aede86fa9 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/mining/MiningFeatures.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/mining/MiningFeatures.kt @@ -18,7 +18,6 @@ package gg.skytils.skytilsmod.features.impl.mining import gg.essential.universal.UChat -import gg.essential.universal.UGraphics import gg.essential.universal.UMatrixStack import gg.essential.universal.utils.MCClickEventAction import gg.essential.universal.wrappers.message.UMessage @@ -26,41 +25,29 @@ import gg.essential.universal.wrappers.message.UTextComponent import gg.skytils.skytilsmod.Skytils import gg.skytils.skytilsmod.Skytils.Companion.failPrefix import gg.skytils.skytilsmod.Skytils.Companion.mc -import gg.skytils.skytilsmod.Skytils.Companion.prefix import gg.skytils.skytilsmod.Skytils.Companion.successPrefix import gg.skytils.skytilsmod.core.DataFetcher import gg.skytils.skytilsmod.core.GuiManager import gg.skytils.skytilsmod.core.GuiManager.createTitle -import gg.skytils.skytilsmod.core.structure.GuiElement import gg.skytils.skytilsmod.core.tickTimer import gg.skytils.skytilsmod.events.impl.BossBarEvent import gg.skytils.skytilsmod.events.impl.GuiContainerEvent import gg.skytils.skytilsmod.events.impl.PacketEvent -import gg.skytils.skytilsmod.features.impl.handlers.MayorInfo import gg.skytils.skytilsmod.utils.* import gg.skytils.skytilsmod.utils.RenderUtil.highlight -import gg.skytils.skytilsmod.utils.graphics.colors.ColorFactory -import net.minecraft.client.entity.EntityOtherPlayerMP import net.minecraft.client.renderer.GlStateManager -import net.minecraft.client.renderer.vertex.DefaultVertexFormats -import net.minecraft.entity.EntityLivingBase import net.minecraft.init.Blocks import net.minecraft.init.Items import net.minecraft.inventory.ContainerChest -import net.minecraft.network.play.server.S08PacketPlayerPosLook import net.minecraft.network.play.server.S3EPacketTeams import net.minecraft.util.AxisAlignedBB import net.minecraft.util.BlockPos -import net.minecraft.util.ResourceLocation import net.minecraftforge.client.event.ClientChatReceivedEvent -import net.minecraftforge.client.event.RenderLivingEvent import net.minecraftforge.client.event.RenderWorldLastEvent import net.minecraftforge.event.entity.player.PlayerInteractEvent import net.minecraftforge.event.world.WorldEvent import net.minecraftforge.fml.common.eventhandler.EventPriority import net.minecraftforge.fml.common.eventhandler.SubscribeEvent -import net.minecraftforge.fml.common.gameevent.TickEvent -import net.minecraftforge.fml.common.gameevent.TickEvent.ClientTickEvent import java.awt.Color import java.util.regex.Pattern @@ -74,15 +61,6 @@ object MiningFeatures { private var puzzlerSolution: BlockPos? = null private var raffleBox: BlockPos? = null private var inRaffle = false - var lastTPLoc: BlockPos? = null - var waypoints = hashMapOf() - var waypointDelayTicks = 0 - private val SBE_DSM_PATTERN = - Regex("\\\$(?:SBECHWP\\b|DSMCHWP):(?.*?)@-(?-?\\d+),(?-?\\d+),(?-?\\d+)") - private val xyzPattern = - Regex(".*?(?[a-zA-Z0-9_]{3,16}):.*?(?[0-9]{1,3}),? (?:y: )?(?[0-9]{1,3}),? (?:z: )?(?[0-9]{1,3}).*?") - private val xzPattern = - Regex(".*(?[a-zA-Z0-9_]{3,16}):.* (?[0-9]{1,3}),? (?[0-9]{1,3}).*") @SubscribeEvent fun onBossBar(event: BossBarEvent.Set) { @@ -106,14 +84,6 @@ object MiningFeatures { } } - @SubscribeEvent - fun onReceivePacket(event: PacketEvent.ReceiveEvent) { - if (!Utils.inSkyblock) return - if (Skytils.config.crystalHollowDeathWaypoint && event.packet is S08PacketPlayerPosLook && mc.thePlayer != null) { - lastTPLoc = mc.thePlayer.position - } - } - @SubscribeEvent(priority = EventPriority.HIGHEST, receiveCanceled = true) fun onChat(event: ClientChatReceivedEvent) { if (!Utils.inSkyblock || event.type == 2.toByte()) return @@ -186,95 +156,6 @@ object MiningFeatures { } } } - if (Skytils.config.hollowChatCoords && SBInfo.mode == SkyblockIsland.CrystalHollows.mode) { - xyzPattern.find(unformatted)?.groups?.let { - waypointChatMessage(it["x"]!!.value, it["y"]!!.value, it["z"]!!.value) - return - } - xzPattern.find(unformatted)?.groups?.let { - waypointChatMessage(it["x"]!!.value, "100", it["z"]!!.value) - return - } - - /** - * Checks for the format used in DSM and SBE - * $DSMCHWP:Mines of Divan@-673,117,426 ✔ - * $SBECHWP:Khazad-dûm@-292,63,281 ✔ - * $asdf:Khazad-dûm@-292,63,281 ❌ - * $SBECHWP:Khazad-dûm@asdf,asdf,asdf ❌ - */ - val cleaned = SBE_DSM_PATTERN.find(unformatted) - if (cleaned != null) { - val stringLocation = cleaned.groups["stringLocation"]!!.value - val x = cleaned.groups["x"]!!.value - val y = cleaned.groups["y"]!!.value - val z = cleaned.groups["z"]!!.value - CrystalHollowsMap.Locations.entries.find { it.cleanName == stringLocation } - ?.takeIf { !it.loc.exists() }?.let { loc -> - /** - * Sends the waypoints message except it suggests which one should be used based on - * the name contained in the message and converts it to the internally used names for the waypoints. - */ - UMessage("§3Skytils > §eFound coordinates in a chat message, click a button to set a waypoint.\n") - .append( - UTextComponent("§f${loc.displayName} ") - .setClick( - MCClickEventAction.RUN_COMMAND, - "/skytilshollowwaypoint set $x $y $z ${loc.id}" - ) - .setHoverText("§eSet waypoint for ${loc.displayName}") - ) - .append( - UTextComponent("§e[Custom]") - .setClick( - MCClickEventAction.SUGGEST_COMMAND, - "/skytilshollowwaypoint set $x $y $z name_here" - ) - .setHoverText("§eSet custom waypoint") - ).chat() - } - } - } - if ((Skytils.config.crystalHollowWaypoints || Skytils.config.crystalHollowMapPlaces) && Skytils.config.kingYolkarWaypoint && SBInfo.mode == SkyblockIsland.CrystalHollows.mode - && mc.thePlayer != null && unformatted.startsWith("[NPC] King Yolkar:") - ) { - CrystalHollowsMap.Locations.KingYolkar.loc.set() - } - if (unformatted.startsWith("You died") || unformatted.startsWith("☠ You were killed")) { - waypointDelayTicks = - 50 //this is to make sure the scoreboard has time to update and nothing moves halfway across the map - if (Skytils.config.crystalHollowDeathWaypoint && SBInfo.mode == SkyblockIsland.CrystalHollows.mode && lastTPLoc != null) { - UChat.chat( - UTextComponent("$prefix §eClick to set a death waypoint at ${lastTPLoc!!.x} ${lastTPLoc!!.y} ${lastTPLoc!!.z}").setClick( - MCClickEventAction.RUN_COMMAND, - "/sthw set ${lastTPLoc!!.x} ${lastTPLoc!!.y} ${lastTPLoc!!.z} Last Death" - ) - ) - } - } else if (unformatted.startsWith("Warp")) { - waypointDelayTicks = 50 - } - } - - private fun waypointChatMessage(x: String, y: String, z: String) { - val message = UMessage( - "$prefix §eFound coordinates in a chat message, click a button to set a waypoint.\n" - ) - for (loc in CrystalHollowsMap.Locations.entries) { - if (loc.loc.exists()) continue - message.append( - UTextComponent("${loc.displayName.substring(0, 2)}[${loc.displayName}] ") - .setClick(MCClickEventAction.SUGGEST_COMMAND, "/sthw set $x $y $z ${loc.id}") - .setHoverText("§eSet waypoint for ${loc.cleanName}") - ) - } - message.append( - UTextComponent("§e[Custom]").setClick( - MCClickEventAction.SUGGEST_COMMAND, - "/sthw set $x $y $z Name" - ).setHoverText("§eSet waypoint for custom location") - ) - message.chat() } @SubscribeEvent @@ -340,29 +221,6 @@ object MiningFeatures { GlStateManager.enableDepth() GlStateManager.enableCull() } - if (Skytils.config.crystalHollowWaypoints && SBInfo.mode == SkyblockIsland.CrystalHollows.mode) { - GlStateManager.disableDepth() - for (loc in CrystalHollowsMap.Locations.entries) { - loc.loc.drawWaypoint(loc.cleanName, event.partialTicks, matrixStack) - } - RenderUtil.renderWaypointText("Crystal Nucleus", 513.5, 107.0, 513.5, event.partialTicks, matrixStack) - for ((key, value) in waypoints) - RenderUtil.renderWaypointText(key, value, event.partialTicks, matrixStack) - GlStateManager.enableDepth() - } - } - - @SubscribeEvent - fun onRenderLivingPre(event: RenderLivingEvent.Pre) { - if (!Utils.inSkyblock) return - if (Skytils.config.crystalHollowWaypoints && - event.entity is EntityOtherPlayerMP && - event.entity.name == "Team Treasurite" && - mc.thePlayer.canEntityBeSeen(event.entity) && - event.entity.baseMaxHealth == if (MayorInfo.mayorPerks.contains("DOUBLE MOBS HP!!!")) 2_000_000.0 else 1_000_000.0 - ) { - waypoints["Corleone"] = event.entity.position - } } @SubscribeEvent @@ -386,168 +244,11 @@ object MiningFeatures { } } - @SubscribeEvent(priority = EventPriority.LOW) // priority low so it always runs after sbinfo is updated - fun onTick(event: ClientTickEvent) { - if (!Utils.inSkyblock || event.phase != TickEvent.Phase.START) return - if ((Skytils.config.crystalHollowWaypoints || Skytils.config.crystalHollowMapPlaces) && SBInfo.mode == SkyblockIsland.CrystalHollows.mode - && waypointDelayTicks == 0 && mc.thePlayer != null - ) { - CrystalHollowsMap.Locations.cleanNameToLocation[SBInfo.location]?.loc?.set() - } else if (waypointDelayTicks > 0) - waypointDelayTicks-- - } - @SubscribeEvent fun onWorldChange(event: WorldEvent.Unload) { puzzlerSolution = null lastJukebox = null raffleBox = null inRaffle = false - CrystalHollowsMap.Locations.entries.forEach { it.loc.reset() } - waypoints.clear() - } - - class CrystalHollowsMap : GuiElement(name = "Crystal Hollows Map", x = 0, y = 0) { - val mapLocation = ResourceLocation("skytils", "crystalhollowsmap.png") - - enum class Locations(val displayName: String, val id: String, val color: Int, val size: Int = 50) { - LostPrecursorCity("§fLost Precursor City", "internal_city", ColorFactory.WHITE.rgb), - JungleTemple("§aJungle Temple", "internal_temple", ColorFactory.GREEN.rgb), - GoblinQueensDen("§eGoblin Queen's Den", "internal_den", ColorFactory.YELLOW.rgb), - MinesOfDivan("§9Mines of Divan", "internal_mines", ColorFactory.BLUE.rgb), - KingYolkar("§6King Yolkar", "internal_king", ColorFactory.ORANGE.rgb, 25), - KhazadDum("§cKhazad-dûm", "internal_bal", ColorFactory.RED.rgb), - FairyGrotto("§dFairy Grotto", "internal_fairy", ColorFactory.PINK.rgb, 26), - Corleone("§bCorleone", "internal_corleone", ColorFactory.AQUA.rgb, 26); - - val loc = LocationObject() - val cleanName = displayName.stripControlCodes() - - companion object { - val cleanNameToLocation = entries.associateBy { it.cleanName } - } - } - - override fun render() { - if (!toggled || SBInfo.mode != SkyblockIsland.CrystalHollows.mode || mc.thePlayer == null) return - val stack = UMatrixStack() - UMatrixStack.Compat.runLegacyMethod(stack) { - stack.scale(0.1, 0.1, 1.0) - UGraphics.disableLighting() - stack.runWithGlobalState { - RenderUtil.renderTexture(mapLocation, 0, 0, 624, 624, false) - if (Skytils.config.crystalHollowMapPlaces) { - Locations.entries.forEach { - it.loc.drawOnMap(it.size, it.color) - } - } - } - val x = (mc.thePlayer.posX - 202).coerceIn(0.0, 624.0) - val y = (mc.thePlayer.posZ - 202).coerceIn(0.0, 624.0) - - // player marker code - val wr = UGraphics.getFromTessellator() - mc.textureManager.bindTexture(ResourceLocation("textures/map/map_icons.png")) - - stack.push() - stack.translate(x, y, 0.0) - - // Rotate about the center to match the player's yaw - stack.rotate((mc.thePlayer.rotationYawHead + 180f) % 360f, 0f, 0f, 1f) - stack.scale(1.5f, 1.5f, 1.5f) - stack.translate(-0.125f, 0.125f, 0.0f) - UGraphics.color4f(1f, 1f, 1f, 1f) - UGraphics.enableAlpha() - val d1 = 0.0 - val d2 = 0.25 - wr.beginWithActiveShader(UGraphics.DrawMode.QUADS, DefaultVertexFormats.POSITION_TEX) - wr.pos(stack, -8.0, -8.0, 100.0).tex(d1, d1).endVertex() - wr.pos(stack, -8.0, 8.0, 100.0).tex(d1, d2).endVertex() - wr.pos(stack, 8.0, 8.0, 100.0).tex(d2, d2).endVertex() - wr.pos(stack, 8.0, -8.0, 100.0).tex(d2, d1).endVertex() - wr.drawDirect() - stack.pop() - } - } - - override fun demoRender() { - UGraphics.disableLighting() - RenderUtil.renderTexture(mapLocation, 0, 0, 62, 62, false) - } - - override val toggled: Boolean - get() = Skytils.config.crystalHollowMap - override val height: Int - get() = 62 // should be 62.4 but oh well - override val width: Int - get() = 62 - - init { - Skytils.guiManager.registerElement(this) - } - } - - init { - CrystalHollowsMap() - } - - class LocationObject { - var locX: Double? = null - var locY: Double? = null - var locZ: Double? = null - private var locMinX: Double = 1100.0 - private var locMinY: Double = 1100.0 - private var locMinZ: Double = 1100.0 - private var locMaxX: Double = -100.0 - private var locMaxY: Double = -100.0 - private var locMaxZ: Double = -100.0 - - fun reset() { - locX = null - locY = null - locZ = null - locMinX = 1100.0 - locMinY = 1100.0 - locMinZ = 1100.0 - locMaxX = -100.0 - locMaxY = -100.0 - locMaxZ = -100.0 - } - - fun set() { - locMinX = (mc.thePlayer.posX - 200).coerceIn(0.0, 624.0).coerceAtMost(locMinX) - locMinY = mc.thePlayer.posY.coerceIn(0.0, 256.0).coerceAtMost(locMinY) - locMinZ = (mc.thePlayer.posZ - 200).coerceIn(0.0, 624.0).coerceAtMost(locMinZ) - locMaxX = (mc.thePlayer.posX - 200).coerceIn(0.0, 624.0).coerceAtLeast(locMaxX) - locMaxY = mc.thePlayer.posY.coerceIn(0.0, 256.0).coerceAtLeast(locMaxY) - locMaxZ = (mc.thePlayer.posZ - 200).coerceIn(0.0, 624.0).coerceAtLeast(locMaxZ) - locX = (locMinX + locMaxX) / 2 - locY = (locMinY + locMaxY) / 2 - locZ = (locMinZ + locMaxZ) / 2 - } - - fun exists(): Boolean { - return locX != null && locY != null && locZ != null - } - - fun drawWaypoint(text: String, partialTicks: Float, matrixStack: UMatrixStack) { - if (exists()) - RenderUtil.renderWaypointText(text, locX!! + 200, locY!!, locZ!! + 200, partialTicks, matrixStack) - } - - fun drawOnMap(size: Int, color: Int) { - if (exists()) - RenderUtil.drawRect(locX!! - size, locZ!! - size, locX!! + size, locZ!! + size, color) - } - - override fun toString(): String { - return String.format("%.0f", locX?.plus(200)) + " " + String.format( - "%.0f", - locY - ) + " " + String.format( - "%.0f", - locZ?.plus(200) - ) - } } } From f7b9416ad6069034d46068721215578f6aa47158 Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Thu, 23 May 2024 18:10:35 -0400 Subject: [PATCH 37/46] chore: split the ktor client used for http and ws --- .../kotlin/gg/skytils/skytilsmod/Skytils.kt | 50 +++++++------------ .../gg/skytils/skytilsws/client/WSClient.kt | 36 ++++++++++++- 2 files changed, 53 insertions(+), 33 deletions(-) diff --git a/src/main/kotlin/gg/skytils/skytilsmod/Skytils.kt b/src/main/kotlin/gg/skytils/skytilsmod/Skytils.kt index fed9022f9..e137637b8 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/Skytils.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/Skytils.kt @@ -76,20 +76,15 @@ import gg.skytils.skytilsmod.utils.* import gg.skytils.skytilsmod.utils.graphics.ScreenRenderer import gg.skytils.skytilsmod.utils.graphics.colors.CustomColor import gg.skytils.skytilsws.client.WSClient -import gg.skytils.skytilsws.shared.SkytilsWS import io.ktor.client.* import io.ktor.client.engine.cio.* import io.ktor.client.plugins.* import io.ktor.client.plugins.cache.* import io.ktor.client.plugins.compression.* import io.ktor.client.plugins.contentnegotiation.* -import io.ktor.client.plugins.websocket.* import io.ktor.http.* -import io.ktor.serialization.kotlinx.* import io.ktor.serialization.kotlinx.json.* -import io.ktor.websocket.* import kotlinx.coroutines.* -import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import kotlinx.serialization.modules.SerializersModule import net.minecraft.client.Minecraft @@ -127,7 +122,6 @@ import java.security.KeyStore import java.util.* import java.util.concurrent.Executors import java.util.concurrent.ThreadPoolExecutor -import java.util.zip.Deflater import javax.net.ssl.TrustManagerFactory import javax.net.ssl.X509TrustManager import kotlin.coroutines.CoroutineContext @@ -220,6 +214,23 @@ class Skytils { } } + val trustManager by lazy { + val backingManager = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()).apply { + init(null as KeyStore?) + }.trustManagers.first { it is X509TrustManager } as X509TrustManager + + val ourManager = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()).apply { + Skytils::class.java.getResourceAsStream("/skytilscacerts.jks").use { + val ourKs = KeyStore.getInstance(KeyStore.getDefaultType()).apply { + load(it, "skytilsontop".toCharArray()) + } + init(ourKs) + } + }.trustManagers.first { it is X509TrustManager } as X509TrustManager + + UnionX509TrustManager(backingManager, ourManager) + } + val client = HttpClient(CIO) { install(ContentEncoding) { customEncoder(BrotliEncoder, 1.0F) @@ -249,32 +260,7 @@ class Skytils { socketTimeout = 10000 } https { - val backingManager = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()).apply { - init(null as KeyStore?) - }.trustManagers.first { it is X509TrustManager } as X509TrustManager - - val ourManager = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()).apply { - Skytils::class.java.getResourceAsStream("/skytilscacerts.jks").use { - val ourKs = KeyStore.getInstance(KeyStore.getDefaultType()).apply { - load(it, "skytilsontop".toCharArray()) - } - init(ourKs) - } - }.trustManagers.first { it is X509TrustManager } as X509TrustManager - - trustManager = UnionX509TrustManager(backingManager, ourManager) - } - } - - install(WebSockets) { - pingInterval = 5_000L - @OptIn(ExperimentalSerializationApi::class) - contentConverter = KotlinxWebsocketSerializationConverter(SkytilsWS.packetSerializer) - extensions { - install(WebSocketDeflateExtension) { - compressionLevel = Deflater.DEFAULT_COMPRESSION - compressIfBiggerThan(bytes = 4 * 1024) - } + trustManager = Skytils.trustManager } } } diff --git a/src/main/kotlin/gg/skytils/skytilsws/client/WSClient.kt b/src/main/kotlin/gg/skytils/skytilsws/client/WSClient.kt index c7fb57257..22a027524 100644 --- a/src/main/kotlin/gg/skytils/skytilsws/client/WSClient.kt +++ b/src/main/kotlin/gg/skytils/skytilsws/client/WSClient.kt @@ -19,16 +19,50 @@ package gg.skytils.skytilsws.client import gg.skytils.skytilsmod.Skytils -import gg.skytils.skytilsmod.Skytils.Companion.client import gg.skytils.skytilsws.shared.SkytilsWS import gg.skytils.skytilsws.shared.packet.C2SPacketConnect import gg.skytils.skytilsws.shared.packet.Packet +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.* import io.ktor.client.plugins.websocket.* +import io.ktor.serialization.kotlinx.* import io.ktor.websocket.* import kotlinx.coroutines.channels.ClosedReceiveChannelException +import kotlinx.serialization.ExperimentalSerializationApi +import java.util.zip.Deflater object WSClient { var session: DefaultClientWebSocketSession? = null + val wsClient by lazy { + HttpClient(CIO) { + install(UserAgent) { + agent = "Skytils/${Skytils.VERSION} SkytilsWS/${SkytilsWS.version}" + } + + install(WebSockets) { + pingInterval = 59_000L + @OptIn(ExperimentalSerializationApi::class) + contentConverter = KotlinxWebsocketSerializationConverter(SkytilsWS.packetSerializer) + extensions { + install(WebSocketDeflateExtension) { + compressionLevel = Deflater.DEFAULT_COMPRESSION + compressIfBiggerThan(bytes = 4 * 1024) + } + } + } + + engine { + endpoint { + connectTimeout = 10000 + keepAliveTime = 60000 + } + https { + trustManager = Skytils.trustManager + } + } + } + } suspend fun openConnection() { if (session != null) error("Session already open") From b685d61a87a6afd3d46dbecb351eeb0b94df54e2 Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Fri, 24 May 2024 15:09:39 -0400 Subject: [PATCH 38/46] feat!: add crystal hollows waypoint sharing fix: ws client connects to localhost fix: client sends CH location as serverid --- .../features/impl/mining/CHWaypoints.kt | 70 +++++++++++++++---- .../skytilsmod/listeners/DungeonListener.kt | 4 +- .../gg/skytils/skytilsmod/utils/SBInfo.kt | 2 +- .../skytils/skytilsws/client/PacketHandler.kt | 26 +++++++ .../gg/skytils/skytilsws/client/WSClient.kt | 2 +- ws-shared | 2 +- 6 files changed, 88 insertions(+), 18 deletions(-) diff --git a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/mining/CHWaypoints.kt b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/mining/CHWaypoints.kt index b7020a162..e44c4ff15 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/mining/CHWaypoints.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/mining/CHWaypoints.kt @@ -28,10 +28,17 @@ import gg.skytils.skytilsmod.Skytils import gg.skytils.skytilsmod.Skytils.Companion.mc import gg.skytils.skytilsmod.Skytils.Companion.prefix import gg.skytils.skytilsmod.core.structure.GuiElement +import gg.skytils.skytilsmod.events.impl.HypixelPacketEvent import gg.skytils.skytilsmod.events.impl.PacketEvent import gg.skytils.skytilsmod.features.impl.handlers.MayorInfo import gg.skytils.skytilsmod.utils.* import gg.skytils.skytilsmod.utils.graphics.colors.ColorFactory +import gg.skytils.skytilsws.client.WSClient +import gg.skytils.skytilsws.shared.packet.C2SPacketCHWaypoint +import gg.skytils.skytilsws.shared.packet.C2SPacketCHWaypointsSubscribe +import gg.skytils.skytilsws.shared.structs.CHWaypointType +import kotlinx.coroutines.launch +import net.hypixel.modapi.packet.impl.clientbound.event.ClientboundLocationPacket import net.minecraft.client.entity.EntityOtherPlayerMP import net.minecraft.client.renderer.GlStateManager import net.minecraft.client.renderer.vertex.DefaultVertexFormats @@ -47,6 +54,7 @@ import net.minecraftforge.fml.common.eventhandler.EventPriority import net.minecraftforge.fml.common.eventhandler.SubscribeEvent import net.minecraftforge.fml.common.gameevent.TickEvent import net.minecraftforge.fml.common.gameevent.TickEvent.ClientTickEvent +import kotlin.jvm.optionals.getOrNull object CHWaypoints { var lastTPLoc: BlockPos? = null @@ -58,6 +66,22 @@ object CHWaypoints { Regex(".*?(?[a-zA-Z0-9_]{3,16}):.*?(?[0-9]{1,3}),? (?:y: )?(?[0-9]{1,3}),? (?:z: )?(?[0-9]{1,3}).*?") private val xzPattern = Regex(".*(?[a-zA-Z0-9_]{3,16}):.* (?[0-9]{1,3}),? (?[0-9]{1,3}).*") + val chWaypointsList = hashMapOf() + class CHInstance { + val waypoints = hashMapOf() + } + + + @SubscribeEvent + fun onHypixelPacket(event: HypixelPacketEvent.ReceiveEvent) { + if (event.packet is ClientboundLocationPacket) { + if (event.packet.mode.getOrNull() == SkyblockIsland.CrystalHollows.mode) { + Skytils.IO.launch { + WSClient.sendPacket(C2SPacketCHWaypointsSubscribe(event.packet.serverName)) + } + } + } + } @SubscribeEvent fun onReceivePacket(event: PacketEvent.ReceiveEvent) { @@ -187,7 +211,14 @@ object CHWaypoints { mc.thePlayer.canEntityBeSeen(event.entity) && event.entity.baseMaxHealth == if (MayorInfo.mayorPerks.contains("DOUBLE MOBS HP!!!")) 2_000_000.0 else 1_000_000.0 ) { - waypoints["Corleone"] = event.entity.position + if (!CrystalHollowsMap.Locations.Corleone.loc.exists()) { + CrystalHollowsMap.Locations.Corleone.apply { + loc.set() + Skytils.IO.launch { + WSClient.sendPacket(C2SPacketCHWaypoint(serverId = SBInfo.server ?: "", serverTime = mc.theWorld.worldTime, packetType, loc.locX!!.toInt(), loc.locY!!.toInt(), loc.locZ!!.toInt())) + } + } + } else CrystalHollowsMap.Locations.Corleone.loc.set() } } @@ -197,14 +228,27 @@ object CHWaypoints { if ((Skytils.config.crystalHollowWaypoints || Skytils.config.crystalHollowMapPlaces) && SBInfo.mode == SkyblockIsland.CrystalHollows.mode && waypointDelayTicks == 0 && mc.thePlayer != null ) { - CrystalHollowsMap.Locations.cleanNameToLocation[SBInfo.location]?.loc?.set() + CrystalHollowsMap.Locations.cleanNameToLocation[SBInfo.location]?.let { + if (!it.loc.exists()) { + it.loc.set() + Skytils.IO.launch { + WSClient.sendPacket(C2SPacketCHWaypoint(serverId = SBInfo.server ?: "", serverTime = mc.theWorld.worldTime, it.packetType, it.loc.locX!!.toInt(), it.loc.locY!!.toInt(), it.loc.locZ!!.toInt())) + } + } else it.loc.set() + } } else if (waypointDelayTicks > 0) waypointDelayTicks-- } - @SubscribeEvent + @SubscribeEvent(priority = EventPriority.HIGHEST) fun onWorldChange(event: WorldEvent.Unload) { - CrystalHollowsMap.Locations.entries.forEach { it.loc.reset() } + val instance = chWaypointsList.getOrPut(SBInfo.server ?: "") { CHInstance() } + CrystalHollowsMap.Locations.entries.forEach { + if (it.loc.exists()) { + instance.waypoints[it.packetType] = BlockPos(it.loc.locX!!, it.loc.locY!!, it.loc.locZ!!) + } + it.loc.reset() + } waypoints.clear() } @@ -212,15 +256,15 @@ object CHWaypoints { class CrystalHollowsMap : GuiElement(name = "Crystal Hollows Map", x = 0, y = 0) { val mapLocation = ResourceLocation("skytils", "crystalhollowsmap.png") - enum class Locations(val displayName: String, val id: String, val color: Int, val size: Int = 50) { - LostPrecursorCity("§fLost Precursor City", "internal_city", ColorFactory.WHITE.rgb), - JungleTemple("§aJungle Temple", "internal_temple", ColorFactory.GREEN.rgb), - GoblinQueensDen("§eGoblin Queen's Den", "internal_den", ColorFactory.YELLOW.rgb), - MinesOfDivan("§9Mines of Divan", "internal_mines", ColorFactory.BLUE.rgb), - KingYolkar("§6King Yolkar", "internal_king", ColorFactory.ORANGE.rgb, 25), - KhazadDum("§cKhazad-dûm", "internal_bal", ColorFactory.RED.rgb), - FairyGrotto("§dFairy Grotto", "internal_fairy", ColorFactory.PINK.rgb, 26), - Corleone("§bCorleone", "internal_corleone", ColorFactory.AQUA.rgb, 26); + enum class Locations(val displayName: String, val id: String, val color: Int, val packetType: CHWaypointType, val size: Int = 50) { + LostPrecursorCity("§fLost Precursor City", "internal_city", ColorFactory.WHITE.rgb, CHWaypointType.LostPrecursorCity), + JungleTemple("§aJungle Temple", "internal_temple", ColorFactory.GREEN.rgb, CHWaypointType.JungleTemple), + GoblinQueensDen("§eGoblin Queen's Den", "internal_den", ColorFactory.YELLOW.rgb, CHWaypointType.GoblinQueensDen), + MinesOfDivan("§9Mines of Divan", "internal_mines", ColorFactory.BLUE.rgb, CHWaypointType.MinesOfDivan), + KingYolkar("§6King Yolkar", "internal_king", ColorFactory.ORANGE.rgb, CHWaypointType.KingYolkar,25), + KhazadDum("§cKhazad-dûm", "internal_bal", ColorFactory.RED.rgb, CHWaypointType.KhazadDum), + FairyGrotto("§dFairy Grotto", "internal_fairy", ColorFactory.PINK.rgb, CHWaypointType.FairyGrotto, 26), + Corleone("§bCorleone", "internal_corleone", ColorFactory.AQUA.rgb, CHWaypointType.Corleone, 26); val loc = LocationObject() val cleanName = displayName.stripControlCodes() diff --git a/src/main/kotlin/gg/skytils/skytilsmod/listeners/DungeonListener.kt b/src/main/kotlin/gg/skytils/skytilsmod/listeners/DungeonListener.kt index 9e0bfedce..64d32f96d 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/listeners/DungeonListener.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/listeners/DungeonListener.kt @@ -51,7 +51,7 @@ import gg.skytils.skytilsws.client.WSClient import gg.skytils.skytilsws.shared.packet.C2SPacketDungeonEnd import gg.skytils.skytilsws.shared.packet.C2SPacketDungeonRoom import gg.skytils.skytilsws.shared.packet.C2SPacketDungeonRoomSecret -import gg.skytils.skytilsws.shared.packet.C2SPacketStartDungeon +import gg.skytils.skytilsws.shared.packet.C2SPacketDungeonStart import kotlinx.coroutines.async import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -212,7 +212,7 @@ object DungeonListener { } val partyMembers = party.await().members.ifEmpty { setOf(mc.thePlayer.uniqueID) }.mapTo(hashSetOf()) { it.toString() } val entrance = DungeonInfo.uniqueRooms.first { it.mainRoom.data.type == RoomType.ENTRANCE } - WSClient.sendPacket(C2SPacketStartDungeon( + WSClient.sendPacket(C2SPacketDungeonStart( serverId = SBInfo.server ?: return@launch, floor = DungeonFeatures.dungeonFloor!!, members = partyMembers, diff --git a/src/main/kotlin/gg/skytils/skytilsmod/utils/SBInfo.kt b/src/main/kotlin/gg/skytils/skytilsmod/utils/SBInfo.kt index 794914468..80cf6ade6 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/utils/SBInfo.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/utils/SBInfo.kt @@ -83,7 +83,7 @@ object SBInfo { lastLocationPacket = null } - @SubscribeEvent + @SubscribeEvent(priority = EventPriority.HIGH) fun onHypixelPacket(event: HypixelPacketEvent.ReceiveEvent) { if (event.packet is ClientboundLocationPacket) { Utils.checkThreadAndQueue { diff --git a/src/main/kotlin/gg/skytils/skytilsws/client/PacketHandler.kt b/src/main/kotlin/gg/skytils/skytilsws/client/PacketHandler.kt index 4a5d4323b..f4b0bb0ed 100644 --- a/src/main/kotlin/gg/skytils/skytilsws/client/PacketHandler.kt +++ b/src/main/kotlin/gg/skytils/skytilsws/client/PacketHandler.kt @@ -24,11 +24,16 @@ import gg.skytils.skytilsmod.features.impl.dungeons.catlas.core.map.Room import gg.skytils.skytilsmod.features.impl.dungeons.catlas.core.map.Unknown import gg.skytils.skytilsmod.features.impl.dungeons.catlas.handlers.DungeonInfo import gg.skytils.skytilsmod.features.impl.dungeons.catlas.utils.ScanUtils +import gg.skytils.skytilsmod.features.impl.mining.CHWaypoints +import gg.skytils.skytilsmod.features.impl.mining.CHWaypoints.CHInstance +import gg.skytils.skytilsmod.features.impl.mining.CHWaypoints.chWaypointsList +import gg.skytils.skytilsmod.utils.SBInfo import gg.skytils.skytilsws.shared.IPacketHandler import gg.skytils.skytilsws.shared.SkytilsWS import gg.skytils.skytilsws.shared.packet.* import io.ktor.websocket.* import kotlinx.coroutines.coroutineScope +import net.minecraft.util.BlockPos import java.util.* object PacketHandler : IPacketHandler { @@ -68,6 +73,27 @@ object PacketHandler : IPacketHandler { } } } + is S2CPacketCHReset -> { + CHWaypoints.waypoints.remove(packet.serverId) + } + is S2CPacketCHWaypoint -> { + if (SBInfo.server == packet.serverId) { + if (mc.theWorld.worldTime < packet.serverTime) { + WSClient.sendPacket(C2SPacketCHReset(packet.serverId)) + } else { + CHWaypoints.CrystalHollowsMap.Locations.entries.find { it.packetType == packet.type }?.let { + if (!it.loc.exists()) { + it.loc.locX = packet.x.toDouble() + it.loc.locY = packet.y.toDouble() + it.loc.locZ = packet.z.toDouble() + } + } + } + } else { + val instance = chWaypointsList.getOrPut(SBInfo.server ?: "") { CHInstance() } + instance.waypoints[packet.type] = BlockPos(packet.x, packet.y, packet.z) + } + } else -> { session.close(CloseReason(CloseReason.Codes.CANNOT_ACCEPT, "Unknown packet type")) } diff --git a/src/main/kotlin/gg/skytils/skytilsws/client/WSClient.kt b/src/main/kotlin/gg/skytils/skytilsws/client/WSClient.kt index 22a027524..76e176418 100644 --- a/src/main/kotlin/gg/skytils/skytilsws/client/WSClient.kt +++ b/src/main/kotlin/gg/skytils/skytilsws/client/WSClient.kt @@ -67,7 +67,7 @@ object WSClient { suspend fun openConnection() { if (session != null) error("Session already open") - client.webSocketSession("wss://ws.skytils.gg/ws").apply { + wsClient.webSocketSession(System.getProperty("skytils.websocketURL", "wss://ws.skytils.gg/ws")).apply { session = this try { sendSerialized(C2SPacketConnect(SkytilsWS.version, Skytils.VERSION)) diff --git a/ws-shared b/ws-shared index 51e490538..826fc2b77 160000 --- a/ws-shared +++ b/ws-shared @@ -1 +1 @@ -Subproject commit 51e4905381c9875a9283091d4ff628c904b42e66 +Subproject commit 826fc2b779144272195eb055c0e49eebffe52892 From 12da2df10d2bcdd10e28e98b510833332ae124c7 Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Mon, 27 May 2024 20:47:08 -0400 Subject: [PATCH 39/46] fix: small player model draws hat layer in incorrect location --- .../renderer/MixinItemRenderer.java | 2 +- .../transformers/renderer/MixinLayerCape.java | 2 +- .../renderer/MixinLayerCustomHead.java | 8 +++ .../renderer/MixinModelBiped.java | 49 +++++++++++++++++++ .../renderer/MixinRenderManager.java | 2 +- src/main/resources/mixins.skytils.json | 1 + 6 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 src/main/java/gg/skytils/skytilsmod/mixins/transformers/renderer/MixinModelBiped.java diff --git a/src/main/java/gg/skytils/skytilsmod/mixins/transformers/renderer/MixinItemRenderer.java b/src/main/java/gg/skytils/skytilsmod/mixins/transformers/renderer/MixinItemRenderer.java index 5189e2e0d..391ee1b8e 100644 --- a/src/main/java/gg/skytils/skytilsmod/mixins/transformers/renderer/MixinItemRenderer.java +++ b/src/main/java/gg/skytils/skytilsmod/mixins/transformers/renderer/MixinItemRenderer.java @@ -31,7 +31,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(ItemRenderer.class) -public class MixinItemRenderer { +public abstract class MixinItemRenderer { @Shadow private ItemStack itemToRender; diff --git a/src/main/java/gg/skytils/skytilsmod/mixins/transformers/renderer/MixinLayerCape.java b/src/main/java/gg/skytils/skytilsmod/mixins/transformers/renderer/MixinLayerCape.java index 7681d6e02..995a8666c 100644 --- a/src/main/java/gg/skytils/skytilsmod/mixins/transformers/renderer/MixinLayerCape.java +++ b/src/main/java/gg/skytils/skytilsmod/mixins/transformers/renderer/MixinLayerCape.java @@ -38,7 +38,7 @@ public abstract class MixinLayerCape implements LayerRenderer { + @WrapOperation(method = "doRenderLayer", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/EntityLivingBase;isSneaking()Z")) + private boolean disableSneakOffset(EntityLivingBase instance, Operation original) { + return (!(instance instanceof EntityPlayer) || !instance.isChild()) && original.call(instance); + } + @Inject(method = "doRenderLayer", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/GlStateManager;color(FFFF)V", shift = At.Shift.AFTER), cancellable = true) private void renderCustomHeadLayer(EntityLivingBase entity, float p_177141_2_, float p_177141_3_, float partialTicks, float p_177141_5_, float p_177141_6_, float p_177141_7_, float scale, CallbackInfo ci) { LayerCustomHeadHookKt.renderCustomHeadLayer(entity, p_177141_2_, p_177141_3_, partialTicks, p_177141_5_, p_177141_6_, p_177141_7_, scale, ci); diff --git a/src/main/java/gg/skytils/skytilsmod/mixins/transformers/renderer/MixinModelBiped.java b/src/main/java/gg/skytils/skytilsmod/mixins/transformers/renderer/MixinModelBiped.java new file mode 100644 index 000000000..3b7fd7661 --- /dev/null +++ b/src/main/java/gg/skytils/skytilsmod/mixins/transformers/renderer/MixinModelBiped.java @@ -0,0 +1,49 @@ +/* + * Skytils - Hypixel Skyblock Quality of Life Mod + * Copyright (C) 2020-2024 Skytils + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package gg.skytils.skytilsmod.mixins.transformers.renderer; + +import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; +import com.llamalad7.mixinextras.sugar.Local; +import net.minecraft.client.model.ModelBase; +import net.minecraft.client.model.ModelBiped; +import net.minecraft.client.model.ModelRenderer; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.EntityPlayer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ModelBiped.class) +public abstract class MixinModelBiped extends ModelBase { + @Shadow public ModelRenderer bipedHeadwear; + + @Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/GlStateManager;popMatrix()V", ordinal = 0)) + private void renderChildHeadPost(Entity entityIn, float f, float g, float h, float i, float j, float scale, CallbackInfo ci) { + if (this.isChild && entityIn instanceof EntityPlayer) { + this.bipedHeadwear.render(scale); + } + } + + @WrapWithCondition(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/model/ModelRenderer;render(F)V", ordinal = 6)) + private boolean renderChildHeadwear(ModelRenderer instance, float j, @Local(argsOnly = true) Entity entityIn) { + return !this.isChild || !(entityIn instanceof EntityPlayer); + } +} diff --git a/src/main/java/gg/skytils/skytilsmod/mixins/transformers/renderer/MixinRenderManager.java b/src/main/java/gg/skytils/skytilsmod/mixins/transformers/renderer/MixinRenderManager.java index 2e76c5972..75709aee4 100644 --- a/src/main/java/gg/skytils/skytilsmod/mixins/transformers/renderer/MixinRenderManager.java +++ b/src/main/java/gg/skytils/skytilsmod/mixins/transformers/renderer/MixinRenderManager.java @@ -28,7 +28,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; @Mixin(RenderManager.class) -public class MixinRenderManager { +public abstract class MixinRenderManager { @Inject(method = "shouldRender", at = @At("HEAD"), cancellable = true) private void shouldRender(Entity entityIn, ICamera camera, double camX, double camY, double camZ, CallbackInfoReturnable cir) { RenderManagerHookKt.shouldRender(entityIn, camera, camX, camY, camZ, cir); diff --git a/src/main/resources/mixins.skytils.json b/src/main/resources/mixins.skytils.json index 571f6211c..30f8fae30 100644 --- a/src/main/resources/mixins.skytils.json +++ b/src/main/resources/mixins.skytils.json @@ -99,6 +99,7 @@ "client": [ "accessors.AccessorPlayerControllerMP", "gui.MixinGuiEditSign", + "renderer.MixinModelBiped", "util.MixinMouseHelper" ] } \ No newline at end of file From dc6268f3adc7fbb4d1489e0ca0d9fe1d0c593a78 Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Thu, 30 May 2024 16:28:49 -0400 Subject: [PATCH 40/46] fix: waypoints not loading regression --- .../impl/skyblock/LocationChangeEvent.kt | 24 +++++++++++++++++++ .../features/impl/handlers/Waypoints.kt | 13 +++++----- .../gg/skytils/skytilsmod/utils/SBInfo.kt | 2 ++ 3 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 src/main/kotlin/gg/skytils/skytilsmod/events/impl/skyblock/LocationChangeEvent.kt diff --git a/src/main/kotlin/gg/skytils/skytilsmod/events/impl/skyblock/LocationChangeEvent.kt b/src/main/kotlin/gg/skytils/skytilsmod/events/impl/skyblock/LocationChangeEvent.kt new file mode 100644 index 000000000..3fa40d124 --- /dev/null +++ b/src/main/kotlin/gg/skytils/skytilsmod/events/impl/skyblock/LocationChangeEvent.kt @@ -0,0 +1,24 @@ +/* + * Skytils - Hypixel Skyblock Quality of Life Mod + * Copyright (C) 2020-2024 Skytils + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package gg.skytils.skytilsmod.events.impl.skyblock + +import gg.skytils.skytilsmod.events.SkytilsEvent +import net.hypixel.modapi.packet.impl.clientbound.event.ClientboundLocationPacket + +data class LocationChangeEvent(val packet: ClientboundLocationPacket) : SkytilsEvent() diff --git a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/handlers/Waypoints.kt b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/handlers/Waypoints.kt index 424b98aac..ef8af9bc7 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/handlers/Waypoints.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/handlers/Waypoints.kt @@ -25,6 +25,8 @@ import gg.essential.universal.UMatrixStack import gg.skytils.skytilsmod.Skytils import gg.skytils.skytilsmod.commands.impl.OrderedWaypointCommand import gg.skytils.skytilsmod.core.PersistentSave +import gg.skytils.skytilsmod.core.tickTimer +import gg.skytils.skytilsmod.events.impl.skyblock.LocationChangeEvent import gg.skytils.skytilsmod.tweaker.DependencyLoader import gg.skytils.skytilsmod.utils.* import kotlinx.serialization.EncodeDefault @@ -60,7 +62,6 @@ object Waypoints : PersistentSave(File(Skytils.modDir, "waypoints.json")) { private val sbeWaypointFormat = Regex("(?:\\.?\\/?crystalwaypoint parse )?(?[a-zA-Z\\d]+)@-(?[-\\d]+),(?[-\\d]+),(?[-\\d]+)\\\\?n?") private var visibleWaypoints = emptyList() - var needsRefresh = false @OptIn(ExperimentalSerializationApi::class) fun getWaypointsFromString(str: String): Set { @@ -232,16 +233,16 @@ object Waypoints : PersistentSave(File(Skytils.modDir, "waypoints.json")) { @SubscribeEvent fun onWorldChange(event: WorldEvent.Unload) { visibleWaypoints = emptyList() - needsRefresh = true + } + + @SubscribeEvent + fun onLocationChange(event: LocationChangeEvent) { + tickTimer(20, task = ::computeVisibleWaypoints) } @SubscribeEvent fun onPlayerMove(event: ClientTickEvent) { if (event.phase == TickEvent.Phase.END) return - if (needsRefresh && SBInfo.mode != null) { - computeVisibleWaypoints() - needsRefresh = false - } if (mc.thePlayer?.hasMoved == true && SBInfo.mode != null && OrderedWaypointCommand.trackedIsland?.mode == SBInfo.mode) { val tracked = OrderedWaypointCommand.trackedSet?.firstOrNull() if (tracked == null) { diff --git a/src/main/kotlin/gg/skytils/skytilsmod/utils/SBInfo.kt b/src/main/kotlin/gg/skytils/skytilsmod/utils/SBInfo.kt index 80cf6ade6..239546d6c 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/utils/SBInfo.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/utils/SBInfo.kt @@ -19,6 +19,7 @@ package gg.skytils.skytilsmod.utils import gg.skytils.skytilsmod.Skytils.Companion.mc import gg.skytils.skytilsmod.events.impl.HypixelPacketEvent +import gg.skytils.skytilsmod.events.impl.skyblock.LocationChangeEvent import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.* @@ -91,6 +92,7 @@ object SBInfo { server = event.packet.serverName lastLocationPacket = event.packet println(event.packet) + LocationChangeEvent(event.packet).postAndCatch() } } } From 551b1d7bd5b05f4e0586ec1e384528878e6ac349 Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Sat, 1 Jun 2024 15:59:42 -0400 Subject: [PATCH 41/46] fix: config typo Trophy Fish Tracker --- src/main/kotlin/gg/skytils/skytilsmod/core/Config.kt | 4 ++-- src/main/resources/assets/skytils/lang/en_US.lang | 2 +- src/main/resources/assets/skytils/lang/zh_CN.lang | 2 +- src/main/resources/assets/skytils/lang/zh_TW.lang | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/gg/skytils/skytilsmod/core/Config.kt b/src/main/kotlin/gg/skytils/skytilsmod/core/Config.kt index f7bd0699b..a0016c846 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/core/Config.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/core/Config.kt @@ -3287,10 +3287,10 @@ object Config : Vigilant( var fishingHookAge = false @Property( - type = PropertyType.SWITCH, name = "Tropy Fish Tracker", + type = PropertyType.SWITCH, name = "Trophy Fish Tracker", description = "Tracks trophy fish caught.", category = "Miscellaneous", subcategory = "Quality of Life", - i18nName = "skytils.config.miscellaneous.quality_of_life.tropy_fish_tracker", + i18nName = "skytils.config.miscellaneous.quality_of_life.trophy_fish_tracker", i18nCategory = "skytils.config.miscellaneous", i18nSubcategory = "skytils.config.miscellaneous.quality_of_life" ) diff --git a/src/main/resources/assets/skytils/lang/en_US.lang b/src/main/resources/assets/skytils/lang/en_US.lang index a992c140c..7a1ac39bd 100644 --- a/src/main/resources/assets/skytils/lang/en_US.lang +++ b/src/main/resources/assets/skytils/lang/en_US.lang @@ -311,7 +311,7 @@ skytils.config.miscellaneous.quality_of_life.reset_found_relic_waypoints=Reset F skytils.config.miscellaneous.quality_of_life.potion_duration_notifications=Potion Duration Notifications skytils.config.miscellaneous.quality_of_life.stop_hook_sinking_in_lava=Stop Hook Sinking in Lava skytils.config.miscellaneous.quality_of_life.fishing_hook_age=Fishing Hook Age -skytils.config.miscellaneous.quality_of_life.tropy_fish_tracker=Tropy Fish Tracker +skytils.config.miscellaneous.quality_of_life.trophy_fish_tracker=Tropy Fish Tracker skytils.config.miscellaneous.quality_of_life.show_trophy_fish_totals=Show Trophy Fish Totals skytils.config.miscellaneous.quality_of_life.show_total_trophy_fish=Show Total Trophy Fish skytils.config.pets.quality_of_life.autopet_message_hider=Autopet Message Hider diff --git a/src/main/resources/assets/skytils/lang/zh_CN.lang b/src/main/resources/assets/skytils/lang/zh_CN.lang index e1f2be2bb..8a96c2ae5 100644 --- a/src/main/resources/assets/skytils/lang/zh_CN.lang +++ b/src/main/resources/assets/skytils/lang/zh_CN.lang @@ -311,7 +311,7 @@ skytils.config.miscellaneous.quality_of_life.reset_found_relic_waypoints=重置 skytils.config.miscellaneous.quality_of_life.potion_duration_notifications=药水效果即将消失警告 skytils.config.miscellaneous.quality_of_life.stop_hook_sinking_in_lava=防止鱼钩在岩浆中下沉 skytils.config.miscellaneous.quality_of_life.fishing_hook_age=鱼钩计时器 -skytils.config.miscellaneous.quality_of_life.tropy_fish_tracker=奖杯鱼数据跟踪器 +skytils.config.miscellaneous.quality_of_life.trophy_fish_tracker=奖杯鱼数据跟踪器 skytils.config.miscellaneous.quality_of_life.show_trophy_fish_totals=显示每个奖杯鱼总数 skytils.config.miscellaneous.quality_of_life.show_total_trophy_fish=显示总奖杯鱼数 skytils.config.pets.quality_of_life.autopet_message_hider=自动换宠物消息隐藏 diff --git a/src/main/resources/assets/skytils/lang/zh_TW.lang b/src/main/resources/assets/skytils/lang/zh_TW.lang index 37515d7c1..65e3ac57d 100644 --- a/src/main/resources/assets/skytils/lang/zh_TW.lang +++ b/src/main/resources/assets/skytils/lang/zh_TW.lang @@ -311,7 +311,7 @@ skytils.config.miscellaneous.quality_of_life.reset_found_relic_waypoints=重置 skytils.config.miscellaneous.quality_of_life.potion_duration_notifications=藥水效果即將消失警告 skytils.config.miscellaneous.quality_of_life.stop_hook_sinking_in_lava=防止魚鉤在岩漿中下沉 skytils.config.miscellaneous.quality_of_life.fishing_hook_age=魚鉤計時器 -skytils.config.miscellaneous.quality_of_life.tropy_fish_tracker=獎盃魚數據跟蹤器 +skytils.config.miscellaneous.quality_of_life.trophy_fish_tracker=獎盃魚數據跟蹤器 skytils.config.miscellaneous.quality_of_life.show_trophy_fish_totals=顯示每個獎盃魚總數 skytils.config.miscellaneous.quality_of_life.show_total_trophy_fish=顯示總獎盃魚數 skytils.config.pets.quality_of_life.autopet_message_hider=自動換寵物消息隱藏 From 21b74196342f23feaadfe2651269366108f78b14 Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Fri, 24 May 2024 15:48:58 -0400 Subject: [PATCH 42/46] version: 1.10.0-pre8 version: 1.10.0-pre1 version: 1.10.0-pre2 version: 1.10.0-pre3 version: 1.10.0-pre4 version: 1.10.0-pre5 version: 1.10.0-pre6 version: 1.10.0-pre7 --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index c995a8c04..9b92c55de 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,7 +34,7 @@ plugins { signing } -version = "1.9.7" +version = "1.10.0-pre8" group = "gg.skytils" repositories { From 14c7acb802ec7e85d37f50381813981052c58ed1 Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Thu, 13 Jun 2024 13:13:15 -0400 Subject: [PATCH 43/46] fix: add a preinit mixin config because vendor lock in --- build.gradle.kts | 5 ++-- .../mixins/SkytilsMixinInitPlugin.kt | 30 +++++++++++++++++++ .../skytilsmod/mixins/SkytilsMixinPlugin.kt | 6 ++-- src/main/resources/mixins.skytils-init.json | 11 +++++++ src/main/resources/mixins.skytils.json | 1 - 5 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 src/main/kotlin/gg/skytils/skytilsmod/mixins/SkytilsMixinInitPlugin.kt create mode 100644 src/main/resources/mixins.skytils-init.json diff --git a/build.gradle.kts b/build.gradle.kts index 9b92c55de..3dcb502b4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -74,11 +74,12 @@ loom { programArgs("--tweakClass", "gg.skytils.skytilsmod.tweaker.SkytilsTweaker") programArgs("--mixin", "mixins.skytils.json") programArgs("--mixin", "mixins.skytils-events.json") + programArgs("--mixin", "mixins.skytils-init.json") } remove(getByName("server")) } forge { - mixinConfig("mixins.skytils.json", "mixins.skytils-events.json") + mixinConfig("mixins.skytils.json", "mixins.skytils-events.json", "mixins.skytils-init.json") } mixin { defaultRefmapName = "mixins.skytils.refmap.json" @@ -188,7 +189,7 @@ tasks { "FMLCorePlugin" to "gg.skytils.skytilsmod.tweaker.SkytilsLoadingPlugin", "FMLCorePluginContainsFMLMod" to true, "ForceLoadAsMod" to true, - "MixinConfigs" to "mixins.skytils.json,mixins.skytils-events.json", + "MixinConfigs" to "mixins.skytils.json,mixins.skytils-events.json,mixins.skytils-init.json", "ModSide" to "CLIENT", "ModType" to "FML", "TweakClass" to "gg.skytils.skytilsmod.tweaker.SkytilsTweaker", diff --git a/src/main/kotlin/gg/skytils/skytilsmod/mixins/SkytilsMixinInitPlugin.kt b/src/main/kotlin/gg/skytils/skytilsmod/mixins/SkytilsMixinInitPlugin.kt new file mode 100644 index 000000000..c9269ecc7 --- /dev/null +++ b/src/main/kotlin/gg/skytils/skytilsmod/mixins/SkytilsMixinInitPlugin.kt @@ -0,0 +1,30 @@ +/* + * Skytils - Hypixel Skyblock Quality of Life Mod + * Copyright (C) 2020-2024 Skytils + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package gg.skytils.skytilsmod.mixins + +import com.llamalad7.mixinextras.MixinExtrasBootstrap + +class SkytilsMixinInitPlugin : SkytilsMixinPlugin() { + override val mixinPackage = "gg.skytils.skytilsmod.mixins.transformers.init" + + override fun onLoad(mixinPackage: String) { + MixinExtrasBootstrap.init() + super.onLoad(mixinPackage) + } +} \ No newline at end of file diff --git a/src/main/kotlin/gg/skytils/skytilsmod/mixins/SkytilsMixinPlugin.kt b/src/main/kotlin/gg/skytils/skytilsmod/mixins/SkytilsMixinPlugin.kt index 1f86585fe..20299f256 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/mixins/SkytilsMixinPlugin.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/mixins/SkytilsMixinPlugin.kt @@ -18,14 +18,13 @@ package gg.skytils.skytilsmod.mixins -import com.llamalad7.mixinextras.MixinExtrasBootstrap import net.minecraft.launchwrapper.Launch import org.objectweb.asm.tree.ClassNode import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin import org.spongepowered.asm.mixin.extensibility.IMixinInfo -class SkytilsMixinPlugin : IMixinConfigPlugin { - val mixinPackage = "gg.skytils.skytilsmod.mixins.transformers" +open class SkytilsMixinPlugin : IMixinConfigPlugin { + open val mixinPackage = "gg.skytils.skytilsmod.mixins.transformers" val eventsPackage = "gg.skytils.events.mixins" var deobfEnvironment = false @@ -34,7 +33,6 @@ class SkytilsMixinPlugin : IMixinConfigPlugin { if (deobfEnvironment) { println("We are in a deobfuscated environment, loading compatibility mixins.") } - MixinExtrasBootstrap.init() } override fun getRefMapperConfig(): String? = null diff --git a/src/main/resources/mixins.skytils-init.json b/src/main/resources/mixins.skytils-init.json new file mode 100644 index 000000000..894598378 --- /dev/null +++ b/src/main/resources/mixins.skytils-init.json @@ -0,0 +1,11 @@ +{ + "compatibilityLevel": "JAVA_8", + "minVersion": "0.8", + "plugin": "gg.skytils.skytilsmod.mixins.SkytilsMixinInitPlugin", + "package": "gg.skytils.skytilsmod.mixins.transformers.init", + "refmap": "mixins.skytils-init.refmap.json", + "target": "@env(PREINIT)", + "priority": 999, + "mixins": [], + "verbose": true +} \ No newline at end of file diff --git a/src/main/resources/mixins.skytils.json b/src/main/resources/mixins.skytils.json index 30f8fae30..7ea9201b0 100644 --- a/src/main/resources/mixins.skytils.json +++ b/src/main/resources/mixins.skytils.json @@ -97,7 +97,6 @@ ], "verbose": true, "client": [ - "accessors.AccessorPlayerControllerMP", "gui.MixinGuiEditSign", "renderer.MixinModelBiped", "util.MixinMouseHelper" From 3faf305b45b5e98da9e1a6cda0e366d3c9646f8c Mon Sep 17 00:00:00 2001 From: hannibal2 <24389977+hannibal002@users.noreply.github.com> Date: Thu, 13 Jun 2024 19:15:26 +0200 Subject: [PATCH 44/46] Fixed item stack copy (#484) * fixed too many item copy calls * removed comment * inline item --------- Co-authored-by: hannibal2 <24389977+hannibal00212@users.noreply.github.com> --- .../features/impl/mining/MiningFeatures.kt | 23 +++++++++---------- .../mixins/hooks/inventory/SlotHook.kt | 8 +++---- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/mining/MiningFeatures.kt b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/mining/MiningFeatures.kt index aede86fa9..fcdd1d8fb 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/features/impl/mining/MiningFeatures.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/features/impl/mining/MiningFeatures.kt @@ -161,22 +161,21 @@ object MiningFeatures { @SubscribeEvent fun onDrawSlot(event: GuiContainerEvent.DrawSlotEvent.Pre) { if (!Utils.inSkyblock || event.container !is ContainerChest) return - if (event.slot.hasStack) { + if (!event.slot.hasStack) return + if (Skytils.config.highlightDisabledHOTMPerks && SBInfo.lastOpenContainerName == "Heart of the Mountain") { + if (ItemUtil.getItemLore(event.slot.stack).any { it == "§c§lDISABLED" }) { + event.slot highlight Color(255, 0, 0) + } + } + if (Skytils.config.highlightCompletedCommissions && SBInfo.lastOpenContainerName.equals("Commissions")) { val item = event.slot.stack - if (Skytils.config.highlightDisabledHOTMPerks && SBInfo.lastOpenContainerName == "Heart of the Mountain") { - if (ItemUtil.getItemLore(item).any { it == "§c§lDISABLED" }) { + if (item.displayName.startsWith("§6Commission #") && item.item == Items.writable_book) { + if (ItemUtil.getItemLore(item).any { + it == "§eClick to claim rewards!" + }) { event.slot highlight Color(255, 0, 0) } } - if (Skytils.config.highlightCompletedCommissions && SBInfo.lastOpenContainerName.equals("Commissions")) { - if (item.displayName.startsWith("§6Commission #") && item.item == Items.writable_book) { - if (ItemUtil.getItemLore(item).any { - it == "§eClick to claim rewards!" - }) { - event.slot highlight Color(255, 0, 0) - } - } - } } } diff --git a/src/main/kotlin/gg/skytils/skytilsmod/mixins/hooks/inventory/SlotHook.kt b/src/main/kotlin/gg/skytils/skytilsmod/mixins/hooks/inventory/SlotHook.kt index b31f725b5..9b8dbcf36 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/mixins/hooks/inventory/SlotHook.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/mixins/hooks/inventory/SlotHook.kt @@ -27,11 +27,11 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable fun markTerminalItems(slot: Slot, cir: CallbackInfoReturnable) { if (!Utils.inSkyblock) return - val item: ItemStack = (slot.inventory.getStackInSlot(slot.slotIndex) ?: return).copy() - if (!item.isItemEnchanted && (SelectAllColorSolver.shouldClick.contains(slot.slotNumber) || StartsWithSequenceSolver.shouldClick.contains( - slot.slotNumber - )) + val original = slot.inventory.getStackInSlot(slot.slotIndex) ?: return + if (!original.isItemEnchanted && (SelectAllColorSolver.shouldClick.contains(slot.slotNumber) || + StartsWithSequenceSolver.shouldClick.contains(slot.slotNumber)) ) { + val item = original.copy() if (item.tagCompound == null) { item.tagCompound = NBTTagCompound() } From 47a9530068daed027e075977878e6be5dec79433 Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Thu, 13 Jun 2024 13:22:56 -0400 Subject: [PATCH 45/46] Major fixes of zh-tw localization (#482) * Update zh_TW.lang Fixed mistranslations Signed-off-by: wbxshiori * Update zh_TW.lang Signed-off-by: wbxshiori * Update zh_TW.lang Signed-off-by: wbxshiori * Update zh_TW.lang Signed-off-by: wbxshiori * Update zh_TW.lang Signed-off-by: wbxshiori * Update zh_TW.lang Replaced fullwidth parentheses (not displayable by Minecraft) with halfwidth ones and a space. (In previous changes) Fixed a lot of mistranslations, also replaced certain unofficial translations to official, original English names/terms for readability. Signed-off-by: wbxshiori * Update zh_TW.lang Added some parentheses to mark original English names/terms for readability. Other fixes. Signed-off-by: wbxshiori * Update zh_TW.lang Signed-off-by: wbxshiori * Update zh_TW.lang Signed-off-by: wbxshiori * Update zh_TW.lang Signed-off-by: wbxshiori * Update zh_TW.lang Signed-off-by: wbxshiori --------- Signed-off-by: wbxshiori --- .../resources/assets/catlas/lang/zh_TW.lang | 48 +- .../resources/assets/skytils/lang/zh_TW.lang | 718 +++++++++--------- 2 files changed, 383 insertions(+), 383 deletions(-) diff --git a/src/main/resources/assets/catlas/lang/zh_TW.lang b/src/main/resources/assets/catlas/lang/zh_TW.lang index c4d13de41..78bbd7e30 100644 --- a/src/main/resources/assets/catlas/lang/zh_TW.lang +++ b/src/main/resources/assets/catlas/lang/zh_TW.lang @@ -1,43 +1,43 @@ -catlas.config.map.toggle.map_enabled=地圖啓用 +catlas.config.map.toggle.map_enabled=啟用地圖 catlas.config.map.toggle.rotate_map=旋轉地圖 -catlas.config.map.toggle.center_map=居中地圖 +catlas.config.map.toggle.center_map=玩家置中 catlas.config.map.toggle.dynamic_rotate=動態旋轉 -catlas.config.map.toggle.hide_in_boss=Boss戰隱藏 +catlas.config.map.toggle.hide_in_boss=Boss戰時隱藏 catlas.config.map.toggle.show_player_names=顯示玩家名稱 -catlas.config.map.toggle.vanilla_head_marker=原版頭顱標記 -catlas.config.map.size.map_text_scale=地圖文本縮放 +catlas.config.map.toggle.vanilla_head_marker=原版Minecraft地圖標記 +catlas.config.map.size.map_text_scale=地圖文字縮放 catlas.config.map.size.player_heads_scale=玩家頭顱縮放 catlas.config.map.size.player_name_scale=玩家名稱縮放 catlas.config.map.render.map_background_color=地圖背景顏色 catlas.config.map.render.map_border_color=地圖邊框顏色 -catlas.config.map.render.border_thickness=邊框厚度 +catlas.config.map.render.border_thickness=邊框粗細 catlas.config.rooms..room_names=房間名稱 -catlas.config.rooms.text.center_room_names=居中房間名稱 +catlas.config.rooms.text.center_room_names=房間名稱置中 catlas.config.rooms..room_secrets=房間祕密 -catlas.config.rooms..color_text=文本着色 -catlas.config.rooms..room_checkmarks=房間複選標記 -catlas.config.rooms.checkmarks.center_room_checkmarks=居中房間複選標記 +catlas.config.rooms..color_text=文字上色 +catlas.config.rooms..room_checkmarks=房間打勾標記 +catlas.config.rooms.checkmarks.center_room_checkmarks=房間打勾標記置中 catlas.config.colors.doors.blood_door=血之門 catlas.config.colors.doors.entrance_door=入口門 catlas.config.colors.doors.normal_door=普通門 catlas.config.colors.doors.wither_door=凋零之門 -catlas.config.colors.doors.opened_wither_door=已開啓凋零之門 -catlas.config.colors.doors.unopened_door=未開啓門 +catlas.config.colors.doors.opened_wither_door=已開啟的凋零之門 +catlas.config.colors.doors.unopened_door=未開啟的門 catlas.config.colors.rooms.blood_room=血之房間 catlas.config.colors.rooms.entrance_room=入口房間 -catlas.config.colors.rooms.fairy_room=精靈房間 +catlas.config.colors.rooms.fairy_room=小精靈房間 catlas.config.colors.rooms.miniboss_room=小Boss房間 catlas.config.colors.rooms.normal_room=普通房間 -catlas.config.colors.rooms.puzzle_room=拼圖房間 +catlas.config.colors.rooms.puzzle_room=謎題房間 catlas.config.colors.rooms.rare_room=稀有房間 catlas.config.colors.rooms.trap_room=陷阱房間 -catlas.config.colors.rooms.unopened_room=未開啓房間 -catlas.config.other_features.wither_door.box_wither_doors=標框凋零之門 -catlas.config.other_features.wither_door.no_key_color=無鑰匙顏色 -catlas.config.other_features.wither_door.has_key_color=有鑰匙顏色 -catlas.config.other_features.wither_door.door_outline_width=門框輪廓寬度 -catlas.config.other_features.wither_door.door_outline_opacity=門框輪廓不透明度 -catlas.config.other_features.wither_door.door_fill_opacity=門填充不透明度 +catlas.config.colors.rooms.unopened_room=未開啟的房間 +catlas.config.other_features.wither_door.box_wither_doors=標記凋零之門外框 +catlas.config.other_features.wither_door.no_key_color=無鑰匙時顏色 +catlas.config.other_features.wither_door.has_key_color=有鑰匙時顏色 +catlas.config.other_features.wither_door.door_outline_width=門框線寬度 +catlas.config.other_features.wither_door.door_outline_opacity=門框線不透明度 +catlas.config.other_features.wither_door.door_fill_opacity=門填色不透明度 catlas.config.map=地圖 catlas.config.rooms=房間 catlas.config.colors=顏色 @@ -45,8 +45,8 @@ catlas.config.other_features=其他功能 catlas.config.map.toggle=切換 catlas.config.map.size=大小 catlas.config.map.render=渲染 -catlas.config.rooms.text=文本 -catlas.config.rooms.checkmarks=複選標記 +catlas.config.rooms.text=文字 +catlas.config.rooms.checkmarks=打勾標記 catlas.config.colors.doors=門 catlas.config.colors.rooms=房間 -catlas.config.other_features.wither_door=凋零之門 \ No newline at end of file +catlas.config.other_features.wither_door=凋零之門 diff --git a/src/main/resources/assets/skytils/lang/zh_TW.lang b/src/main/resources/assets/skytils/lang/zh_TW.lang index 65e3ac57d..ed7e93dc8 100644 --- a/src/main/resources/assets/skytils/lang/zh_TW.lang +++ b/src/main/resources/assets/skytils/lang/zh_TW.lang @@ -1,221 +1,221 @@ -skytils.config.general.api.fetch_kuudra_prices=獲取最低的 Kuudra 一口價的數據 -skytils.config.general.api.fetch_lowest_bin_prices=獲取最低一口價的數據 +skytils.config.general.api.fetch_kuudra_prices=取得 Kuudra 最低直購價資訊 +skytils.config.general.api.fetch_lowest_bin_prices=取得最低直購價資訊 skytils.config.general.command_aliases.command_alias_mode=指令別名模式 -skytils.config.general.local_api.auto_start_local_api=自動啓動本地API +skytils.config.general.local_api.auto_start_local_api=自動啟動本地API skytils.config.general.local_api.local_api_password=本地API密碼 skytils.config.general.other.join_the_skytils_discord=加入Skytils Discord -skytils.config.general.other.first_launch=首次啓動 -skytils.config.general.other.last_launched_skytils_version=上次啓動的Skytils版本 -skytils.config.general.other.always_sprint_in_skyblock=在空島生存中保持疾跑 -skytils.config.general.other.config_button_on_pause=暫停時顯示配置按鈕 -skytils.config.general.other.reopen_options_menu=重新打開Skytils選項菜單 -skytils.config.general.reparty.override_other_reparty_commands=覆蓋其他模組的重新組隊指令 -skytils.config.general.hypixel.coop_add_confirmation=邀請其他人加入存檔確認 -skytils.config.general.hypixel.guild_leave_confirmation=離開公會確認 -skytils.config.general.hypixel.multiple_party_invites_fix=邀請多人導致幽靈隊伍的修復 +skytils.config.general.other.first_launch=首次啟動 +skytils.config.general.other.last_launched_skytils_version=上次啟動的Skytils版本 +skytils.config.general.other.always_sprint_in_skyblock=在空島生存中維持疾走 +skytils.config.general.other.config_button_on_pause=暫停畫面中顯示設定按鈕 +skytils.config.general.other.reopen_options_menu=重新開啟Skytils選項選單 +skytils.config.general.reparty.override_other_reparty_commands=覆寫其他模組的重新組隊指令 +skytils.config.general.hypixel.coop_add_confirmation=確認邀請其他人加入Coop +skytils.config.general.hypixel.guild_leave_confirmation=確認離開公會 +skytils.config.general.hypixel.multiple_party_invites_fix=修正邀請多人時產生的空白隊伍 skytils.config.general.reparty.autoaccept_reparty=自動接受重新組隊邀請 -skytils.config.general.reparty.autoaccept_reparty_timeout=自動接受重新組隊邀請延時 +skytils.config.general.reparty.autoaccept_reparty_timeout=自動接受重新組隊邀請逾時 skytils.config.general.updates.update_channel=Skytils更新提醒 -skytils.config.dungeons.fixes.inject_fake_dungeon_map=添加假地牢地圖 -skytils.config.dungeons.hud.dungeon_crypts_counter=地牢墳墓計數器 -skytils.config.dungeons.miscellaneous.auto_copy_fails_to_clipboard=自動複製失敗信息到剪貼板 -skytils.config.dungeons.quality_of_life.autoreparty_on_dungeon_ending=地牢結束時自動重新組隊 +skytils.config.dungeons.fixes.inject_fake_dungeon_map=快捷列中插入虛擬的Dungeon地圖 +skytils.config.dungeons.hud.dungeon_crypts_counter=Dungeon Crypt計數器 +skytils.config.dungeons.miscellaneous.auto_copy_fails_to_clipboard=自動複製失敗訊息至剪貼簿 +skytils.config.dungeons.quality_of_life.autoreparty_on_dungeon_ending=Dungeon結束時自動重新組隊 skytils.config.dungeons.miscellaneous.death_counter=死亡計數器 -skytils.config.dungeons.party_finder.party_finder_stats=隊伍尋找加入時查詢數據 -skytils.config.dungeons.miscellaneous.dungeon_chest_profit=地牢寶箱利潤 -skytils.config.dungeons.miscellaneous.dungeon_chest_profit_includes_essence=地牢寶箱利潤將包括精華 -skytils.config.dungeons.miscellaneous.highlight_unopened_croesus_chests=在Croesus高亮未開啓的寶箱 -skytils.config.dungeons.miscellaneous.hide_opened_croesus_chests=在Croesus隱藏已開啓的寶箱 -skytils.config.dungeons.miscellaneous.catlas=地牢地圖 -skytils.config.dungeons.miscellaneous.dungeon_start_confirmation=進入地牢確認 -skytils.config.dungeons.miscellaneous.dungeon_sweat=地牢彩蛋 -skytils.config.dungeons.miscellaneous.dungeon_timer=地牢計時器 +skytils.config.dungeons.party_finder.party_finder_stats=隊伍搜尋器顯示加入者資訊 +skytils.config.dungeons.miscellaneous.dungeon_chest_profit=Dungeon寶箱利潤 +skytils.config.dungeons.miscellaneous.dungeon_chest_profit_includes_essence=Dungeon寶箱利潤包含精華 +skytils.config.dungeons.miscellaneous.highlight_unopened_croesus_chests=在Croesus介面中醒目提示未開啟的寶箱 +skytils.config.dungeons.miscellaneous.hide_opened_croesus_chests=在Croesus介面中隱藏已開啟的寶箱 +skytils.config.dungeons.miscellaneous.catlas=Catlas (Dungeon小地圖) +skytils.config.dungeons.miscellaneous.dungeon_start_confirmation=確認進入Dungeon +skytils.config.dungeons.miscellaneous.dungeon_sweat=Dungeon彩蛋 +skytils.config.dungeons.miscellaneous.dungeon_timer=Dungeon計時器 skytils.config.dungeons.miscellaneous.necron_phase_timer=F7/M7 Boss階段計時器 -skytils.config.dungeons.miscellaneous.red_screen_fix=邊界外紅屏修復 -skytils.config.dungeons.miscellaneous.show_decimal_seconds_on_timers=計時器顯示小數秒數 +skytils.config.dungeons.miscellaneous.red_screen_fix=修正畫面泛紅 +skytils.config.dungeons.miscellaneous.show_decimal_seconds_on_timers=計時器秒數顯示小數 skytils.config.dungeons.miscellaneous.sadan_phase_timer=F6/M6 Boss階段計時器 -skytils.config.dungeons.score_calculation.show_dungeon_score_estimate=顯示地牢分數估算 -skytils.config.dungeons.score_calculation.minimized_dungeon_score_estimate=簡化顯示地牢分數估算 +skytils.config.dungeons.score_calculation.show_dungeon_score_estimate=顯示Dungeon分數估算 +skytils.config.dungeons.score_calculation.minimized_dungeon_score_estimate=簡化顯示Dungeon分數估算 skytils.config.dungeons.score_calculation.score_calculation_party_assist=協助隊伍計算Mimic擊殺分數 -skytils.config.dungeons.score_calculation.receive_score_calculation_party_assist=接收隊員的Mimic擊殺信息協助分數計算 -skytils.config.dungeons.score_calculation.allow_mimic_dead_from_other_mods=允許接收其他模組的Mimic擊殺消息 -skytils.config.dungeons.score_calculation.send_message_on_270_score=達到270分時發送消息 -skytils.config.dungeons.score_calculation.message_for_270_score=270分時的消息 +skytils.config.dungeons.score_calculation.receive_score_calculation_party_assist=接收隊員的Mimic擊殺訊息以計算分數 +skytils.config.dungeons.score_calculation.allow_mimic_dead_from_other_mods=允許接收其他模組的Mimic擊殺訊息 +skytils.config.dungeons.score_calculation.send_message_on_270_score=達到270分時發送訊息 +skytils.config.dungeons.score_calculation.message_for_270_score=270分訊息 skytils.config.dungeons.score_calculation.create_title_on_270_score=達到270分時顯示標題 -skytils.config.dungeons.score_calculation.270_title_message=270分標題消息 -skytils.config.dungeons.score_calculation.send_message_on_300_score=達到300分時發送消息 -skytils.config.dungeons.score_calculation.message_for_300_score=300分時的消息 +skytils.config.dungeons.score_calculation.270_title_message=270分標題訊息 +skytils.config.dungeons.score_calculation.send_message_on_300_score=達到300分時發送訊息 +skytils.config.dungeons.score_calculation.message_for_300_score=300分訊息 skytils.config.dungeons.score_calculation.create_title_on_300_score=達到300分時顯示標題 -skytils.config.dungeons.score_calculation.300_title_message=300分標題消息 -skytils.config.dungeons.quality_of_life.blood_camp_helper=血房擊殺助手 -skytils.config.dungeons.quality_of_life.blood_camp_helper_color=血房擊殺助手的顏色 -skytils.config.dungeons.quality_of_life.box_starred_mobs=標記星標怪物 -skytils.config.dungeons.quality_of_life.box_starred_mobs_color=標記星標怪物的顏色 -skytils.config.dungeons.quality_of_life.box_skeleton_masters=標記骷髏大師 -skytils.config.dungeons.quality_of_life.box_spirit_bear=標記靈魂熊 -skytils.config.dungeons.quality_of_life.box_spirit_bow=標記靈魂弓 -skytils.config.dungeons.quality_of_life.dungeon_chest_reroll_confirmation=地牢寶箱刷新確認 -skytils.config.dungeons.quality_of_life.dungeon_chest_reroll_protection_threshold=地牢寶箱刷新需要的點擊次數 -skytils.config.dungeons.quality_of_life.dungeon_secret_display=地牢房間祕密數量顯示 -skytils.config.dungeons.quality_of_life.ghost_leap_names=鬼魂傳送顯示名字 -skytils.config.dungeons.quality_of_life.hide_archer_bone_passive=隱藏弓箭手被動的骨粉 -skytils.config.dungeons.quality_of_life.hide_damage_in_boss=在Boss階段隱藏傷害文本 -skytils.config.dungeons.quality_of_life.hide_wither_king_dragon_death=隱藏凋靈王的龍的死亡動畫 -skytils.config.dungeons.quality_of_life.hide_fairies=隱藏仙女 -skytils.config.dungeons.quality_of_life.hide_floor_4_crowd_messages=隱藏F4/M4的看臺觀衆消息 -skytils.config.dungeons.quality_of_life.hide_oruo_messages=隱藏問答解密房Oruo的消息 +skytils.config.dungeons.score_calculation.300_title_message=300分標題訊息 +skytils.config.dungeons.quality_of_life.blood_camp_helper=血之房間輔助顯示 +skytils.config.dungeons.quality_of_life.blood_camp_helper_color=血之房間輔助顯示的顏色 +skytils.config.dungeons.quality_of_life.box_starred_mobs=標記星星怪物 +skytils.config.dungeons.quality_of_life.box_starred_mobs_color=標記星星怪物的顏色 +skytils.config.dungeons.quality_of_life.box_skeleton_masters=標記Skeleton Master +skytils.config.dungeons.quality_of_life.box_spirit_bear=標記Spirit Bear +skytils.config.dungeons.quality_of_life.box_spirit_bow=標記Spirit Bow +skytils.config.dungeons.quality_of_life.dungeon_chest_reroll_confirmation=Dungeon寶箱重骰確認 +skytils.config.dungeons.quality_of_life.dungeon_chest_reroll_protection_threshold=Dungeon寶箱重骰需要的點選次數 +skytils.config.dungeons.quality_of_life.dungeon_secret_display=顯示Dungeon房間祕密數量 +skytils.config.dungeons.quality_of_life.ghost_leap_names=鬼魂傳送選單顯示名字 +skytils.config.dungeons.quality_of_life.hide_archer_bone_passive=隱藏Archer被動技能的骨粉 +skytils.config.dungeons.quality_of_life.hide_damage_in_boss=在Boss階段時隱藏傷害文字 +skytils.config.dungeons.quality_of_life.hide_wither_king_dragon_death=隱藏Wither King的龍死亡動畫 +skytils.config.dungeons.quality_of_life.hide_fairies=隱藏Fairy +skytils.config.dungeons.quality_of_life.hide_floor_4_crowd_messages=隱藏F4/M4的觀眾台詞 +skytils.config.dungeons.quality_of_life.hide_oruo_messages=隱藏Oruo的台詞 skytils.config.dungeons.quality_of_life.hide_spirit_animal_nametags=隱藏靈魂動物名稱 -skytils.config.dungeons.quality_of_life.hide_terminal_completion_titles=隱藏終端完成的標題 -skytils.config.dungeons.quality_of_life.hide_wither_miner_nametags=隱藏凋靈礦工頭頂名稱 -skytils.config.dungeons.quality_of_life.hide_terracotta_nametags=隱藏陶土人名稱 -skytils.config.dungeons.quality_of_life.hide_nonstarred_mobs_nametags=隱藏非星標怪物頭頂名稱 -skytils.config.dungeons.quality_of_life.larger_bat_models=更大的蝙蝠模型 -skytils.config.dungeons.quality_of_life.change_hurt_color_on_the_wither_kings_dragons=改變凋靈王的龍受傷顏色 -skytils.config.dungeons.quality_of_life.retexture_wither_kings_dragons=重新給凋靈王的龍上材質 +skytils.config.dungeons.quality_of_life.hide_terminal_completion_titles=隱藏終端機完成的浮動文字 +skytils.config.dungeons.quality_of_life.hide_wither_miner_nametags=隱藏Wither Miner頭頂名稱 +skytils.config.dungeons.quality_of_life.hide_terracotta_nametags=隱藏Terracotta名稱 +skytils.config.dungeons.quality_of_life.hide_nonstarred_mobs_nametags=隱藏非星星怪物頭頂名稱 +skytils.config.dungeons.quality_of_life.larger_bat_models=蝙蝠模型加大 +skytils.config.dungeons.quality_of_life.change_hurt_color_on_the_wither_kings_dragons=改變Wither King的龍受傷顏色 +skytils.config.dungeons.quality_of_life.retexture_wither_kings_dragons=替Wither King的龍重新上色 skytils.config.dungeons.quality_of_life.revive_stone_names=復活石顯示名字 -skytils.config.dungeons.quality_of_life.say_blaze_done=發送烈焰人解密完成消息 +skytils.config.dungeons.quality_of_life.say_blaze_done=發送烈焰使者解謎完成訊息 skytils.config.dungeons.quality_of_life.show_bat_hitboxes=顯示蝙蝠碰撞箱 -skytils.config.miscellaneous.brewing.color_brewing_stands=根據釀造臺狀態改變顏色 -skytils.config.miscellaneous.items.show_dungeon_floor_as_stack_size=在地牢樓層選項上顯示層數 +skytils.config.miscellaneous.brewing.color_brewing_stands=根據釀造台狀態改變顏色 +skytils.config.miscellaneous.items.show_dungeon_floor_as_stack_size=在Dungeon樓層選項上顯示層數 skytils.config.miscellaneous.items.held_item_scale=手持物品縮放 skytils.config.dungeons.quality_of_life.show_giant_hp=顯示巨人生命值 skytils.config.dungeons.quality_of_life.show_giant_hp_at_feet=在巨人腳下顯示生命值 -skytils.config.dungeons.quality_of_life.show_guardian_respawn_timer=顯示守衛者重生計時器 -skytils.config.dungeons.quality_of_life.show_wither_king_statue_box=顯示凋靈王雕像邊界框 -skytils.config.dungeons.quality_of_life.show_sadans_interest=顯示Sadan的陶土人階段倒計時 -skytils.config.dungeons.quality_of_life.show_terracotta_respawn_time=顯示陶土人重生時間 +skytils.config.dungeons.quality_of_life.show_guardian_respawn_timer=顯示深海守衛重生計時器 +skytils.config.dungeons.quality_of_life.show_wither_king_statue_box=顯示Wither King雕像邊界框 +skytils.config.dungeons.quality_of_life.show_sadans_interest=顯示Sadan的Terracota階段倒數計時 +skytils.config.dungeons.quality_of_life.show_terracotta_respawn_time=顯示Terracota重生時間 skytils.config.dungeons.quality_of_life.show_necrons_hp=顯示F7/M7 Boss的生命值 -skytils.config.dungeons.quality_of_life.show_wither_kings_dragons_color_as_text=凋靈王的龍的顏色顯示爲文本 -skytils.config.dungeons.quality_of_life.show_wither_kings_dragons_hp=顯示凋靈王的龍的生命值 -skytils.config.dungeons.quality_of_life.show_wither_kings_dragons_spawn_timer=顯示凋靈王的龍的生成倒計時 -skytils.config.dungeons.quality_of_life.spirit_bear_timer=靈魂熊生成倒計時 -skytils.config.dungeons.quality_of_life.spirit_leap_names=傳送珍珠傳送時顯示名字 -skytils.config.dungeons.quality_of_life.highlight_door_opener=高亮門的開啓者 -skytils.config.dungeons.quality_of_life.spirit_leap_highlights=傳送珍珠傳送高亮 -skytils.config.dungeons.quality_of_life.spirit_pet_warning=靈魂寵物警告 -skytils.config.dungeons.quality_of_life.wither_king_dragon_dimensional_slash_alert=站在凋靈王的閃電技能上的警告 -skytils.config.dungeons.quality_of_life.wither_king_dragon_spawn_alert=凋靈王的龍的生成警告 -skytils.config.dungeons.solvers.blaze_solver=烈焰人射箭解密房的解密器 -skytils.config.dungeons.solvers.show_next_blaze=顯示下一個烈焰人 -skytils.config.dungeons.solvers.line_to_next_blaze=到下一個烈焰人的線條指引 -skytils.config.dungeons.solvers.lowest_blaze_color=最低血量烈焰人的顏色 -skytils.config.dungeons.solvers.highest_blaze_color=最高血量烈焰人的顏色 -skytils.config.dungeons.solvers.next_blaze_color=下一個烈焰人的顏色 -skytils.config.dungeons.solvers.line_to_next_blaze_color=到下一個烈焰人的線條指引的顏色 -skytils.config.dungeons.solvers.boulder_solver=推箱子解密房的解密器 -skytils.config.dungeons.solvers.creeper_beams_solver=爬行者激光連線解密房的解密器 -skytils.config.dungeons.solvers.ice_fill_solver=冰塊填充解密房的解密器 -skytils.config.dungeons.solvers.ice_path_solver=冰上推蠹蟲解密房的解密器 -skytils.config.dungeons.solvers.teleport_maze_solver=3x3傳送迷宮解密房的解密器 -skytils.config.dungeons.solvers.teleport_maze_solver_color=傳送迷宮解密器顏色 -skytils.config.dungeons.solvers.three_weirdos_solver=三箱推理解密房的解密器 -skytils.config.dungeons.solvers.tic_tac_toe_solver=井字棋遊戲解密房的解密器 -skytils.config.dungeons.solvers.tic_tac_toe_solver_color=井字棋遊戲解密器的顏色 -skytils.config.dungeons.solvers.trivia_solver=問答解密房的解密器 -skytils.config.dungeons.solvers.water_board_solver=流水解密房的解密器 +skytils.config.dungeons.quality_of_life.show_wither_kings_dragons_color_as_text=以文字類示Wither King的龍顏色 +skytils.config.dungeons.quality_of_life.show_wither_kings_dragons_hp=顯示Wither King的龍生命值 +skytils.config.dungeons.quality_of_life.show_wither_kings_dragons_spawn_timer=顯示Wither King的龍的生成倒數計時 +skytils.config.dungeons.quality_of_life.spirit_bear_timer=Spirit Bear生成倒數計時 +skytils.config.dungeons.quality_of_life.spirit_leap_names=Spirit Leap傳送時顯示名字 +skytils.config.dungeons.quality_of_life.highlight_door_opener=醒目顯示開門者 +skytils.config.dungeons.quality_of_life.spirit_leap_highlights=醒目顯示Spirit Leap介面玩家資訊 +skytils.config.dungeons.quality_of_life.spirit_pet_warning=Spirit寵物警告 +skytils.config.dungeons.quality_of_life.wither_king_dragon_dimensional_slash_alert=Wither King的Dimensional Slash範圍警告 +skytils.config.dungeons.quality_of_life.wither_king_dragon_spawn_alert=Wither King的龍生成警告 +skytils.config.dungeons.solvers.blaze_solver=烈焰使者謎題 (Higher or Lower) 解答器 +skytils.config.dungeons.solvers.show_next_blaze=顯示下一個烈焰使者 +skytils.config.dungeons.solvers.line_to_next_blaze=以直線連接下一個烈焰使者 +skytils.config.dungeons.solvers.lowest_blaze_color=最低血量烈焰使者的顏色 +skytils.config.dungeons.solvers.highest_blaze_color=最高血量烈焰使者的顏色 +skytils.config.dungeons.solvers.next_blaze_color=下一個烈焰使者的顏色 +skytils.config.dungeons.solvers.line_to_next_blaze_color=到下一個烈焰使者的連接線顏色 +skytils.config.dungeons.solvers.boulder_solver=推箱子 (Boudler) 解答器 +skytils.config.dungeons.solvers.creeper_beams_solver=苦力怕光線 (Creeper Beams) 解答器 +skytils.config.dungeons.solvers.ice_fill_solver=冰塊一筆劃 (Ice Fill) 解答器 +skytils.config.dungeons.solvers.ice_path_solver=滑冰蠹魚 (Ice Path) 解答器 +skytils.config.dungeons.solvers.teleport_maze_solver=3x3傳送迷宮 (Teleport Maze) 解答器 +skytils.config.dungeons.solvers.teleport_maze_solver_color=傳送迷宮解答器顏色 +skytils.config.dungeons.solvers.three_weirdos_solver=三怪咖 (Three Weirdos) 推理解答器 +skytils.config.dungeons.solvers.tic_tac_toe_solver=井字棋 (Tic Tac Toe) 解答器 +skytils.config.dungeons.solvers.tic_tac_toe_solver_color=井字棋解答器的顏色 +skytils.config.dungeons.solvers.trivia_solver=機智問答 (Trivia) 解答器 +skytils.config.dungeons.solvers.water_board_solver=水流圖版 (Water Board) 解答器 skytils.config.dungeons.solvers.find_correct_livid=尋找正確的Livid skytils.config.dungeons.solvers.type_of_livid_finder=Livid標記的類型 -skytils.config.dungeons.tank_helper_tools.boxed_tanks=標記坦克 -skytils.config.dungeons.tank_helper_tools.boxed_tank_color=標記坦克的顏色 +skytils.config.dungeons.tank_helper_tools.boxed_tanks=標記Tank +skytils.config.dungeons.tank_helper_tools.boxed_tank_color=標記Tank的顏色 skytils.config.dungeons.tank_helper_tools.box_protected_teammates=標記被保護的隊友 skytils.config.dungeons.tank_helper_tools.protected_teammate_box_color=被保護的隊友框顏色 -skytils.config.dungeons.tank_helper_tools.tank_protection_range_display=坦克保護範圍顯示 -skytils.config.dungeons.tank_helper_tools.tank_range_wall=坦克保護範圍顯示的圓柱形邊框 -skytils.config.dungeons.tank_helper_tools.tank_range_wall_color=坦克保護範圍顯示的圓柱形邊框顏色 -skytils.config.dungeons.terminal_solvers.block_incorrect_terminal_clicks=阻止終端內不正確的點擊 -skytils.config.dungeons.terminal_solvers.middle_click_on_terminals=終端左鍵點擊替換爲中鍵 -skytils.config.dungeons.terminal_solvers.change_all_to_same_color_solver=變同色終端的解密器 -skytils.config.dungeons.terminal_solvers.change_all_to_same_color_solver_mode=變同色解密器模式 -skytils.config.dungeons.terminal_solvers.click_in_order_solver=按順序點擊終端的解密器 -skytils.config.dungeons.terminal_solvers.click_in_order_first_color=按順序點擊第一顏色 -skytils.config.dungeons.terminal_solvers.click_in_order_second_color=按順序點擊第二顏色 -skytils.config.dungeons.terminal_solvers.click_in_order_third_color=按順序點擊第三顏色 -skytils.config.dungeons.terminal_solvers.select_all_colors_solver=選擇指定顏色的全部物品終端的解密器 -skytils.config.dungeons.terminal_solvers.starts_with_sequence_solver=選擇某字母開頭的全部物品終端的解密器 -skytils.config.dungeons.terminal_solvers.item_frame_alignment_solver=物品展示框轉箭設備的解密器 -skytils.config.dungeons.terminal_solvers.predict_clicks_for_alignment_solver=預測轉箭設備的點擊次數 -skytils.config.dungeons.terminal_solvers.shoot_the_target_solver=射擊指定目標設備的解密器 -skytils.config.dungeons.terminal_solvers.simon_says_solver=按順序點按鈕設備(Simon Says)的解密器 -skytils.config.dungeons.terminal_solvers.predict_clicks_for_simon_says_solver=預測按順序點按鈕設備(Simon Says)的點擊 -skytils.config.events.mayor_jerry.display_jerry_perks=顯示Jerry特權 -skytils.config.events.mayor_jerry.hidden_jerry_alert=特殊Jerry的警報 -skytils.config.events.mayor_jerry.hidden_jerry_timer=特殊Jerry的計時器 -skytils.config.events.mayor_jerry.track_mayor_jerry_items=跟蹤Jerry市長物品 -skytils.config.events.mythological.show_griffin_burrows=顯示獅鷲巢穴 +skytils.config.dungeons.tank_helper_tools.tank_protection_range_display=顯示Tank保護範圍 +skytils.config.dungeons.tank_helper_tools.tank_range_wall=以圓柱形邊框顯示的Tank保護範圍 +skytils.config.dungeons.tank_helper_tools.tank_range_wall_color=Tank保護範圍圓柱形邊框顏色 +skytils.config.dungeons.terminal_solvers.block_incorrect_terminal_clicks=阻止終端機內的錯誤點選 +skytils.config.dungeons.terminal_solvers.middle_click_on_terminals=終端機左鍵替換為中鍵 +skytils.config.dungeons.terminal_solvers.change_all_to_same_color_solver=「九宮格同色」終端機的解答器 +skytils.config.dungeons.terminal_solvers.change_all_to_same_color_solver_mode=「九宮格同色」解答器模式 +skytils.config.dungeons.terminal_solvers.click_in_order_solver=「14格依序點選」終端機的解答器 +skytils.config.dungeons.terminal_solvers.click_in_order_first_color=「14格依序點選」第一顏色 +skytils.config.dungeons.terminal_solvers.click_in_order_second_color=「14格依序點選」第二顏色 +skytils.config.dungeons.terminal_solvers.click_in_order_third_color=「14格依序點選」第三顏色 +skytils.config.dungeons.terminal_solvers.select_all_colors_solver=「選擇特定顏色」終端機的解答器 +skytils.config.dungeons.terminal_solvers.starts_with_sequence_solver=「選擇特定字母開頭」終端機的解答器 +skytils.config.dungeons.terminal_solvers.item_frame_alignment_solver=「旋轉箭矢」裝置的解答器 +skytils.config.dungeons.terminal_solvers.predict_clicks_for_alignment_solver=預測「旋轉箭矢」的點選次數 +skytils.config.dungeons.terminal_solvers.shoot_the_target_solver=「射靶」裝置的解答器 +skytils.config.dungeons.terminal_solvers.simon_says_solver=「依序按鈕」 (Simon Says) 裝置的解答器 +skytils.config.dungeons.terminal_solvers.predict_clicks_for_simon_says_solver=判斷「依序按鈕」的下一個時納入隊友動作 +skytils.config.events.mayor_jerry.display_jerry_perks=顯示Jerry的Perk +skytils.config.events.mayor_jerry.hidden_jerry_alert=警示特殊Jerry現身 +skytils.config.events.mayor_jerry.hidden_jerry_timer=特殊Jerry計時器 +skytils.config.events.mayor_jerry.track_mayor_jerry_items=追蹤Jerry市長物品掉落紀錄 +skytils.config.events.mythological.show_griffin_burrows=顯示獅鷲巢穴 (Griffin Burrow) skytils.config.events.mythological.emptystart_burrow_color=空白/起始巢穴顏色 skytils.config.events.mythological.mob_burrow_color=怪物巢穴顏色 skytils.config.events.mythological.treasure_burrow_color=寶藏巢穴顏色 skytils.config.events.mythological.broadcast_rare_drop_notifications=稀有掉落通知 skytils.config.events.mythological.display_gaia_construct_hits=顯示Gaia Construct的護盾擊打次數 -skytils.config.events.mythological.track_mythological_creatures=跟蹤神話生物數據 -skytils.config.events.spooky.trick_or_treat_chest_alert=不給糖就搗蛋寶箱警報 -skytils.config.events.technoblade.show_shiny_orb_waypoints=顯示閃耀寶珠路徑點 -skytils.config.events.technoblade.show_shiny_pig_locations=顯示閃耀豬位置 +skytils.config.events.mythological.track_mythological_creatures=追蹤神話生物統計 +skytils.config.events.spooky.trick_or_treat_chest_alert=Trick or Treat寶箱警報 +skytils.config.events.technoblade.show_shiny_orb_waypoints=顯示Shiny Orb路徑點 +skytils.config.events.technoblade.show_shiny_pig_locations=顯示Shiny Pig位置 skytils.config.farming.garden.plot_cleanup_helper=農場清理助手 -skytils.config.farming.garden.show_sams_scythe_blocks=Sam的鐮刀破壞方塊預覽 -skytils.config.farming.garden.color_of_sams_scythe_marked_blocks=Sam的鐮刀破壞方塊預覽的顏色 +skytils.config.farming.garden.show_sams_scythe_blocks=預覽Sam的鐮刀破壞方塊範圍 +skytils.config.farming.garden.color_of_sams_scythe_marked_blocks=Sam的鐮刀破壞方塊的顏色 skytils.config.farming.garden.visitor_offer_helper=訪客需求助手 skytils.config.farming.garden.visitor_notifications=訪客通知 -skytils.config.farming.quality_of_life.hide_farming_rng_titles=隱藏農業RNG標題 -skytils.config.farming.solvers.hungry_hiker_solver=Hungry Hiker解密器 -skytils.config.farming.solvers.treasure_hunter_solver=Treasure Hunter解密器 -skytils.config.farming.quality_of_life.click_to_accept_trapper_task=點擊接受Trapper的任務 -skytils.config.farming.quality_of_life.trapper_cooldown_alarm=Trapper的冷卻警報 +skytils.config.farming.quality_of_life.hide_farming_rng_titles=隱藏農業RNG標題文字 +skytils.config.farming.solvers.hungry_hiker_solver=Hungry Hiker解答器 +skytils.config.farming.solvers.treasure_hunter_solver=Treasure Hunter解答器 +skytils.config.farming.quality_of_life.click_to_accept_trapper_task=點選任意處接受Trapper的任務 +skytils.config.farming.quality_of_life.trapper_cooldown_alarm=Trapper冷卻警示 skytils.config.farming.quality_of_life.talbots_theodolite_helper=Talbots Theodolite定位助手 -skytils.config.kuudra.performance.hide_nonnametag_armor_stands_on_kuudra=在Kuudra內隱藏無名稱的盔甲架 +skytils.config.kuudra.performance.hide_nonnametag_armor_stands_on_kuudra=在Kuudra內隱藏無名稱的盔甲座 skytils.config.kuudra.price_checking.kuudra_chest_profit=Kuudra寶箱利潤 -skytils.config.kuudra.price_checking.kuudra_chest_profit_includes_essence=Kuudra寶箱利潤將包括精華 -skytils.config.kuudra.price_checking.kuudra_chest_profit_subtracts_key=Kuudra寶箱利潤將扣除鑰匙 -skytils.config.kuudra.price_checking.show_kuudra_lowest_bin_price=顯示Kuudra最低一口價 -skytils.config.mining.quality_of_life.dark_mode_mist=深色模式的Mist區域 -skytils.config.mining.quality_of_life.highlight_completed_commissions=高亮已完成的委託 -skytils.config.mining.quality_of_life.highlight_disabled_hotm_perks=高亮已禁用的HOTM特權 -skytils.config.mining.quality_of_life.more_visible_ghosts=更清晰可見的幽靈 -skytils.config.mining.quality_of_life.powder_ghast_ping=粉塵惡魂警報 -skytils.config.mining.quality_of_life.raffle_warning=抽獎事件警告 -skytils.config.mining.quality_of_life.raffle_waypoint=抽獎事件路徑點 -skytils.config.mining.quality_of_life.recolor_carpets=給Dwarven Mines的地毯重新上色 -skytils.config.mining.quality_of_life.skymall_reminder=HOTM的Skymall特權刷新提醒 -skytils.config.mining.solvers.fetchur_solver=Fetchur的解密器 -skytils.config.mining.solvers.puzzler_solver=Puzzler的解密器 -skytils.config.mining.crystal_hollows.crystal_hollows_death_waypoints=水晶礦洞死亡路徑點 -skytils.config.mining.crystal_hollows.crystal_hollows_map=水晶礦洞地圖 -skytils.config.mining.crystal_hollows.crystal_hollows_map_special_places=水晶礦洞地圖標記特殊地點 -skytils.config.mining.crystal_hollows.crystal_hollows_waypoints=水晶礦洞路徑點 +skytils.config.kuudra.price_checking.kuudra_chest_profit_includes_essence=Kuudra寶箱利潤包含精華 +skytils.config.kuudra.price_checking.kuudra_chest_profit_subtracts_key=Kuudra寶箱利潤扣除鑰匙 +skytils.config.kuudra.price_checking.show_kuudra_lowest_bin_price=顯示Kuudra最低直購價 +skytils.config.mining.quality_of_life.dark_mode_mist=Mist區域深色模式 +skytils.config.mining.quality_of_life.highlight_completed_commissions=醒目顯示已完成的委託 +skytils.config.mining.quality_of_life.highlight_disabled_hotm_perks=醒目顯示已停用的HOTM天賦 +skytils.config.mining.quality_of_life.more_visible_ghosts=使Ghost更更清晰可見 +skytils.config.mining.quality_of_life.powder_ghast_ping=Powder Ghast警示 +skytils.config.mining.quality_of_life.raffle_warning=Raffle活動警示 +skytils.config.mining.quality_of_life.raffle_waypoint=Raffle活動路徑點 +skytils.config.mining.quality_of_life.recolor_carpets=替Dwarven Mines的地毯重新上色 +skytils.config.mining.quality_of_life.skymall_reminder=Skymall加成更新提醒 +skytils.config.mining.solvers.fetchur_solver=Fetchur的解答器 +skytils.config.mining.solvers.puzzler_solver=Puzzler的解答器 +skytils.config.mining.crystal_hollows.crystal_hollows_death_waypoints=Crystal Hollows死亡路徑點 +skytils.config.mining.crystal_hollows.crystal_hollows_map=Crystal Hollows地圖 +skytils.config.mining.crystal_hollows.crystal_hollows_map_special_places=Crystal Hollows地圖標記特殊地點 +skytils.config.mining.crystal_hollows.crystal_hollows_waypoints=Crystal Hollows路徑點 skytils.config.mining.crystal_hollows.king_yolkar_waypoint=哥布林王路徑點 -skytils.config.mining.crystal_hollows.crystal_hollows_chat_coordinates_grabber=水晶礦洞聊天座標抓取器 -skytils.config.mining.crystal_hollows.crystal_hollows_treasure_helper=水晶礦洞寶藏寶箱助手 -skytils.config.miscellaneous.chat_tabs.chat_tabs=聊天頻道 -skytils.config.miscellaneous.chat_tabs.prefill_chat_commands=預填充聊天指令 +skytils.config.mining.crystal_hollows.crystal_hollows_chat_coordinates_grabber=Crystal Hollows聊天座標擷取器 +skytils.config.mining.crystal_hollows.crystal_hollows_treasure_helper=Crystal Hollows寶箱助手 +skytils.config.miscellaneous.chat_tabs.chat_tabs=聊天頻道頁籤 +skytils.config.miscellaneous.chat_tabs.prefill_chat_commands=預先填入聊天指令 skytils.config.miscellaneous.chat_tabs.auto_switch_chat_channel=自動切換聊天頻道 -skytils.config.miscellaneous.chat_tabs.copy_chat_messages=複製聊天消息 -skytils.config.miscellaneous.fixes.boss_bar_fix=Boss血條修復 -skytils.config.miscellaneous.fixes.fix_falling_sand_rendering=修復下落沙渲染 -skytils.config.miscellaneous.fixes.fix_world_time=修復世界時間 +skytils.config.miscellaneous.chat_tabs.copy_chat_messages=複製聊天訊息 +skytils.config.miscellaneous.fixes.boss_bar_fix=修正Boss血條 +skytils.config.miscellaneous.fixes.fix_falling_sand_rendering=修正掉落中的沙子渲染 +skytils.config.miscellaneous.fixes.fix_world_time=修正世界時間 skytils.config.miscellaneous.fixes.prevent_log_spam=防止垃圾日誌 -skytils.config.miscellaneous.fixes.twitch_fix=Twitch修復 +skytils.config.miscellaneous.fixes.twitch_fix=Twitch修正 skytils.config.miscellaneous.items.price_paid=記錄支付的價格 -skytils.config.miscellaneous.items.disable_block_animation=禁用格擋動畫 +skytils.config.miscellaneous.items.disable_block_animation=停用格擋動畫 skytils.config.miscellaneous.items.dropped_item_size=掉落物品大小 -skytils.config.miscellaneous.items.hide_implosion_particles=隱藏爆炸粒子 +skytils.config.miscellaneous.items.hide_implosion_particles=隱藏Implosion技能粒子 skytils.config.miscellaneous.items.hide_midas_staff_gold=隱藏Midas Staff的黃金方塊 -skytils.config.miscellaneous.items.highlight_filled_bazaar_orders=高亮已填滿的集市訂單 -skytils.config.miscellaneous.items.item_cooldown_display=物品冷卻顯示 +skytils.config.miscellaneous.items.highlight_filled_bazaar_orders=醒目顯示已到貨的集市訂單 +skytils.config.miscellaneous.items.item_cooldown_display=顯示物品冷卻 skytils.config.miscellaneous.items.item_stars_display=物品星星顯示模式 -skytils.config.miscellaneous.items.show_item_quality=顯示物品質量 -skytils.config.miscellaneous.items.head_display_size=揹包內頭顱顯示大小 +skytils.config.miscellaneous.items.show_item_quality=顯示物品之品質 +skytils.config.miscellaneous.items.head_display_size=物品欄內頭顱顯示大小 skytils.config.miscellaneous.items.prevent_placing_weapons=防止放置武器 -skytils.config.miscellaneous.items.wither_shield_cooldown_tracker=Wither Shield冷卻跟蹤器 -skytils.config.miscellaneous.items.wither_shield_has_wither_impact=改爲Wither Impact的護盾冷卻時長 -skytils.config.miscellaneous.items.show_enchanted_book_abbreviation=顯示附魔書名字縮寫 -skytils.config.miscellaneous.items.show_attribute_shard_abbreviation=顯示屬性碎片名字縮寫 -skytils.config.miscellaneous.items.show_attribute_shard_level=顯示屬性碎片等級 +skytils.config.miscellaneous.items.wither_shield_cooldown_tracker=Wither Shield冷卻追蹤 +skytils.config.miscellaneous.items.wither_shield_has_wither_impact=改以Wither Impact的冷卻時間顯示Wither Shield +skytils.config.miscellaneous.items.show_enchanted_book_abbreviation=顯示附魔書名稱縮寫 +skytils.config.miscellaneous.items.show_attribute_shard_abbreviation=顯示Attribute Shard名稱縮寫 +skytils.config.miscellaneous.items.show_attribute_shard_level=顯示Attribute Shard等級 skytils.config.miscellaneous.items.show_enchanted_book_tier=顯示附魔書等級 skytils.config.miscellaneous.items.combine_helper=附魔書合併助手 skytils.config.miscellaneous.items.show_etherwarp_teleport_position=顯示Etherwarp傳送位置 skytils.config.miscellaneous.items.etherwarp_teleport_position_color=Etherwarp傳送位置顏色 skytils.config.miscellaneous.items.show_gemstones=顯示寶石 -skytils.config.miscellaneous.items.show_head_floor_number=顯示金頭/鑽頭所屬地牢層數 +skytils.config.miscellaneous.items.show_head_floor_number=顯示Golden Hean與Diamond Head所屬Dungeon層數 skytils.config.miscellaneous.items.show_item_origin=顯示物品來源 skytils.config.miscellaneous.items.show_new_year_cake_year=顯示新年蛋糕年份 skytils.config.miscellaneous.items.show_npc_sell_price=顯示NPC出售價格 @@ -223,203 +223,203 @@ skytils.config.miscellaneous.items.show_potion_tier=顯示藥水等級 skytils.config.miscellaneous.items.show_pet_candies=顯示寵物使用糖果數 skytils.config.miscellaneous.items.show_item_star_count=顯示物品星星數量 skytils.config.miscellaneous.items.stacking_enchant_progress_display=成長型附魔進度顯示 -skytils.config.miscellaneous.items.radioactive_bonus=狼蛛頭技能的加成顯示 +skytils.config.miscellaneous.items.radioactive_bonus=顯示Radioactive技能加成 skytils.config.miscellaneous.item_rarity.show_item_rarity=顯示物品稀有度 -skytils.config.miscellaneous.item_rarity.item_rarity_shape=物品稀有度顯示形狀 +skytils.config.miscellaneous.item_rarity.item_rarity_shape=以形狀顯示物品稀有度 skytils.config.miscellaneous.item_rarity.show_pet_rarity=顯示寵物稀有度 skytils.config.miscellaneous.item_rarity.item_rarity_transparency=物品稀有度透明度 -skytils.config.miscellaneous.minions.only_collect_enchanted_items=僅收集僕從附魔物品 -skytils.config.miscellaneous.minions.show_minion_tier=顯示僕從等級 -skytils.config.miscellaneous.quality_of_life.always_show_item_name_highlight=總是顯示物品名稱高亮 +skytils.config.miscellaneous.minions.only_collect_enchanted_items=收取Minion產品時僅收取附魔物品 +skytils.config.miscellaneous.minions.show_minion_tier=顯示Minion等級 +skytils.config.miscellaneous.quality_of_life.always_show_item_name_highlight=持續顯示手持物品名稱 skytils.config.miscellaneous.quality_of_life.low_health_vignette_threshold=低生命值警告的血線 skytils.config.miscellaneous.quality_of_life.low_health_vignette_color=低生命值警告顏色 -skytils.config.miscellaneous.other.hide_tooltips_while_on_storage=揹包界面隱藏物品描述 -skytils.config.miscellaneous.other.copy_deaths_to_clipboard=複製死亡消息到剪貼板 -skytils.config.miscellaneous.other.auto_copy_rng_drops_to_clipboard=自動複製RNG掉落消息到剪貼板 -skytils.config.miscellaneous.other.also_copy_very_rare_drops_to_clipboard=複製極其稀有掉落消息到剪貼板 -skytils.config.miscellaneous.other.dupe_tracker=違禁的複製的物品追蹤器 -skytils.config.miscellaneous.other.dupe_tracker_overlay_color=違禁的複製的物品追蹤器覆蓋顏色 -skytils.config.miscellaneous.other.endstone_protector_spawn_timer=末地石守護者生成計時器 -skytils.config.miscellaneous.other.players_in_range_display=範圍內玩家數量顯示 -skytils.config.miscellaneous.other.placed_summoning_eye_display=已放置召喚之眼顯示 -skytils.config.miscellaneous.other.ping_display=Ping顯示 -skytils.config.miscellaneous.other.random_stuff=隨機東西 +skytils.config.miscellaneous.other.hide_tooltips_while_on_storage=隱藏背包選單中的物品描述 +skytils.config.miscellaneous.other.copy_deaths_to_clipboard=複製死亡訊息到剪貼簿 +skytils.config.miscellaneous.other.auto_copy_rng_drops_to_clipboard=自動複製RNG掉落訊息到剪貼簿 +skytils.config.miscellaneous.other.also_copy_very_rare_drops_to_clipboard=自動複製VERY RARE掉落訊息到剪貼簿 +skytils.config.miscellaneous.other.dupe_tracker=追蹤違規複製的物品 +skytils.config.miscellaneous.other.dupe_tracker_overlay_color=違規複製物品的覆蓋顏色 +skytils.config.miscellaneous.other.endstone_protector_spawn_timer=Endstone Protector生成計時器 +skytils.config.miscellaneous.other.players_in_range_display=顯示範圍內玩家數量 +skytils.config.miscellaneous.other.placed_summoning_eye_display=顯示已放置Summonine Eye +skytils.config.miscellaneous.other.ping_display=顯示Ping +skytils.config.miscellaneous.other.random_stuff=隨機功能 skytils.config.miscellaneous.other.scam_check=交易詐騙檢查 skytils.config.miscellaneous.other.show_bestiary_level=顯示圖鑑等級 -skytils.config.miscellaneous.other.show_selected_arrow=顯示使用的箭 +skytils.config.miscellaneous.other.show_selected_arrow=顯示選擇中箭矢 skytils.config.miscellaneous.other.show_world_age=顯示世界年齡 -skytils.config.miscellaneous.other.transparent_armor_layer=透明盔甲層 -skytils.config.miscellaneous.other.head_layer_transparency=頭顱層透明度 -skytils.config.miscellaneous.other.fix_summon_skin=修復召喚物皮膚 -skytils.config.miscellaneous.other.use_player_skin=使用玩家皮膚 -skytils.config.miscellaneous.quality_of_life.custom_auction_price_input=自定義拍賣價格輸入界面 -skytils.config.miscellaneous.quality_of_life.better_stash=更好的儲物箱 -skytils.config.miscellaneous.quality_of_life.container_sell_value=容器界面顯示物品價值 -skytils.config.miscellaneous.quality_of_life.include_item_modifiers=包括物品修正 +skytils.config.miscellaneous.other.transparent_armor_layer=盔甲透明度 +skytils.config.miscellaneous.other.head_layer_transparency=頭顱透明度 +skytils.config.miscellaneous.other.fix_summon_skin=修正召喚物外觀 +skytils.config.miscellaneous.other.use_player_skin=使用玩家外觀 +skytils.config.miscellaneous.quality_of_life.custom_auction_price_input=自訂拍賣價格輸入界面 +skytils.config.miscellaneous.quality_of_life.better_stash=更好的Stash +skytils.config.miscellaneous.quality_of_life.container_sell_value=容器界面顯示物品價格 +skytils.config.miscellaneous.quality_of_life.include_item_modifiers=計算時包含物品附加數值 skytils.config.miscellaneous.quality_of_life.max_displayed_items=最大顯示物品數 -skytils.config.miscellaneous.quality_of_life.custom_damage_splash_style=自定義傷害顯示樣式 -skytils.config.miscellaneous.quality_of_life.disable_enderman_teleportation=禁用末影人傳送動畫 -skytils.config.miscellaneous.quality_of_life.disable_night_vision=禁用夜視 -skytils.config.miscellaneous.quality_of_life.dungeon_pot_lock=地牢藥水鎖定 -skytils.config.miscellaneous.quality_of_life.enchant_glint_fix=修復附魔發光 -skytils.config.miscellaneous.quality_of_life.hide_absorption_hearts=隱藏傷害吸收的心形血條 -skytils.config.miscellaneous.quality_of_life.hide_air_display=隱藏水下氣泡屏幕效果 +skytils.config.miscellaneous.quality_of_life.custom_damage_splash_style=自訂傷害顯示樣式 +skytils.config.miscellaneous.quality_of_life.disable_enderman_teleportation=停用終界使者傳送動畫 +skytils.config.miscellaneous.quality_of_life.disable_night_vision=停用夜視 +skytils.config.miscellaneous.quality_of_life.dungeon_pot_lock=鎖定可購買的Dungeon藥水 +skytils.config.miscellaneous.quality_of_life.enchant_glint_fix=修正附魔發光 +skytils.config.miscellaneous.quality_of_life.hide_absorption_hearts=隱藏吸收效果的心形血條 +skytils.config.miscellaneous.quality_of_life.hide_air_display=隱藏水下氣泡特效 skytils.config.miscellaneous.quality_of_life.hide_armor_display=隱藏護甲值顯示 -skytils.config.miscellaneous.quality_of_life.hide_cheap_coins=隱藏便宜硬幣 +skytils.config.miscellaneous.quality_of_life.hide_cheap_coins=隱藏低價值硬幣 skytils.config.dungeons.quality_of_life.hide_dying_mobs=隱藏死亡的怪物 skytils.config.miscellaneous.quality_of_life.hide_fire_on_entities=隱藏實體上的火焰 -skytils.config.miscellaneous.quality_of_life.hide_fishing_hooks=隱藏其他人的釣魚鉤 +skytils.config.miscellaneous.quality_of_life.hide_fishing_hooks=隱藏其他人的釣鉤 skytils.config.miscellaneous.quality_of_life.hide_hunger_display=隱藏飢餓值顯示 skytils.config.miscellaneous.quality_of_life.hide_jerry_rune=隱藏Jerry符文效果 skytils.config.miscellaneous.quality_of_life.hide_lightning=隱藏閃電 skytils.config.miscellaneous.quality_of_life.hide_mob_death_particles=隱藏怪物死亡粒子 -skytils.config.miscellaneous.quality_of_life.hide_pet_health_display=隱藏寵物生命值顯示 +skytils.config.miscellaneous.quality_of_life.hide_pet_health_display=隱藏原版Minecraft座騎生命值 skytils.config.miscellaneous.quality_of_life.hide_players_in_spawn=隱藏出生點玩家 skytils.config.miscellaneous.quality_of_life.hide_potion_effects_in_inventory=隱藏物品欄藥水效果 -skytils.config.miscellaneous.quality_of_life.hide_scoreboard_score=隱藏記分板分數 -skytils.config.miscellaneous.quality_of_life.hide_vanilla_health_display=隱藏原版生命值顯示 -skytils.config.miscellaneous.quality_of_life.highlight_disabled_potion_effects=高亮禁用的藥水效果 -skytils.config.miscellaneous.quality_of_life.highlight_salvageable_items=高亮可分解物品 -skytils.config.miscellaneous.quality_of_life.highlight_dungeonsellable_items=高亮可出售的地牢物品 -skytils.config.miscellaneous.quality_of_life.lower_enderman_nametags=降低末影人名稱牌 -skytils.config.miscellaneous.quality_of_life.middle_click_gui_items=中鍵點擊GUI物品 -skytils.config.miscellaneous.quality_of_life.moveable_action_bar=可移動快捷欄 -skytils.config.miscellaneous.quality_of_life.moveable_item_name_highlight=可移動的物品名稱高亮 +skytils.config.miscellaneous.quality_of_life.hide_scoreboard_score=隱藏計分板分數 +skytils.config.miscellaneous.quality_of_life.hide_vanilla_health_display=隱藏原版Minecraft生命值 +skytils.config.miscellaneous.quality_of_life.highlight_disabled_potion_effects=醒目顯示停用的藥水效果 +skytils.config.miscellaneous.quality_of_life.highlight_salvageable_items=醒目顯示可分解物品 +skytils.config.miscellaneous.quality_of_life.highlight_dungeonsellable_items=醒目顯示可出售的Dungeon物品 +skytils.config.miscellaneous.quality_of_life.lower_enderman_nametags=降低終界使者名稱浮動文字位置 +skytils.config.miscellaneous.quality_of_life.middle_click_gui_items=中鍵點選GUI物品 +skytils.config.miscellaneous.quality_of_life.moveable_action_bar=可動式快捷列 +skytils.config.miscellaneous.quality_of_life.moveable_item_name_highlight=可動式手持物品名稱 skytils.config.miscellaneous.quality_of_life.no_fire=無火焰效果 skytils.config.miscellaneous.quality_of_life.no_hurtcam=無受傷抖動 -skytils.config.miscellaneous.quality_of_life.party_addons=隊伍插件 -skytils.config.miscellaneous.quality_of_life.prevent_cursor_reset=防止鼠標光標重置 +skytils.config.miscellaneous.quality_of_life.party_addons=組隊附加功能 (Party Addons) +skytils.config.miscellaneous.quality_of_life.prevent_cursor_reset=防止滑鼠游標位置重設 skytils.config.miscellaneous.quality_of_life.prevent_moving_on_death=死亡時防止移動 -skytils.config.miscellaneous.quality_of_life.power_orb_lock=防止重複放置同級或更好的狼獵手球 -skytils.config.miscellaneous.quality_of_life.power_orb_lock_duration=允許重複放置狼獵手球的剩餘時間 -skytils.config.miscellaneous.quality_of_life.press_enter_to_confirm_sign_popups=按回車確認告示牌窗口 +skytils.config.miscellaneous.quality_of_life.power_orb_lock=防止重複放置Power Orb +skytils.config.miscellaneous.quality_of_life.power_orb_lock_duration=允許重複放置Power Orb的間隔 +skytils.config.miscellaneous.quality_of_life.press_enter_to_confirm_sign_popups=按Enter鍵以確定告示牌數值並關閉彈出介面 skytils.config.miscellaneous.quality_of_life.protect_items=保護物品 skytils.config.miscellaneous.quality_of_life.protect_items_above_value=保護價值高於某值的物品 -skytils.config.miscellaneous.quality_of_life.protect_starred_items=保護已打星物品 -skytils.config.miscellaneous.quality_of_life.quiver_display=箭袋顯示 +skytils.config.miscellaneous.quality_of_life.protect_starred_items=保護已升星物品 +skytils.config.miscellaneous.quality_of_life.quiver_display=顯示箭袋內箭矢 skytils.config.miscellaneous.quality_of_life.restock_arrows_warning=箭矢不足警告 -skytils.config.miscellaneous.quality_of_life.spiders_den_rain_timer=蜘蛛島嶼下雨計時器 +skytils.config.miscellaneous.quality_of_life.spiders_den_rain_timer=Spider's Den下雨計時器 skytils.config.miscellaneous.quality_of_life.show_arachne_spawn=顯示Arachne生成 skytils.config.miscellaneous.quality_of_life.show_arachne_hp=顯示Arachne生命值 skytils.config.miscellaneous.quality_of_life.show_coins_per_bit=顯示每Bits價值的硬幣數 skytils.config.miscellaneous.quality_of_life.show_coins_per_copper=顯示每個銅幣價值的硬幣數 -skytils.config.miscellaneous.quality_of_life.show_lowest_bin_price=顯示最低一口價 -skytils.config.miscellaneous.quality_of_life.stop_clicking_nonsalvageable_items=阻止點擊不可分解物品 -skytils.config.miscellaneous.quality_of_life.view_relic_waypoints=查看遺物路徑點 -skytils.config.miscellaneous.quality_of_life.find_rare_relics=查找稀有遺物 -skytils.config.miscellaneous.quality_of_life.reset_found_relic_waypoints=重置已找到的遺物路徑點 -skytils.config.miscellaneous.quality_of_life.potion_duration_notifications=藥水效果即將消失警告 -skytils.config.miscellaneous.quality_of_life.stop_hook_sinking_in_lava=防止魚鉤在岩漿中下沉 -skytils.config.miscellaneous.quality_of_life.fishing_hook_age=魚鉤計時器 -skytils.config.miscellaneous.quality_of_life.trophy_fish_tracker=獎盃魚數據跟蹤器 -skytils.config.miscellaneous.quality_of_life.show_trophy_fish_totals=顯示每個獎盃魚總數 -skytils.config.miscellaneous.quality_of_life.show_total_trophy_fish=顯示總獎盃魚數 -skytils.config.pets.quality_of_life.autopet_message_hider=自動換寵物消息隱藏 -skytils.config.pets.quality_of_life.highlight_active_pet=高亮選中寵物 -skytils.config.pets.quality_of_life.active_pet_highlight_color=選中寵物高亮的顏色 -skytils.config.pets.quality_of_life.highlight_favorite_pets=高亮收藏寵物 -skytils.config.pets.quality_of_life.favorite_pet_highlight_color=收藏寵物高亮的顏色 +skytils.config.miscellaneous.quality_of_life.show_lowest_bin_price=顯示最低直購價 +skytils.config.miscellaneous.quality_of_life.stop_clicking_nonsalvageable_items=防止點選不可分解之物品 +skytils.config.miscellaneous.quality_of_life.view_relic_waypoints=檢視Relic路徑點 +skytils.config.miscellaneous.quality_of_life.find_rare_relics=尋找稀有Relic +skytils.config.miscellaneous.quality_of_life.reset_found_relic_waypoints=重設已找到的Relic路徑點 +skytils.config.miscellaneous.quality_of_life.potion_duration_notifications=藥水效果即將消失時通知 +skytils.config.miscellaneous.quality_of_life.stop_hook_sinking_in_lava=防止釣鉤在岩漿中下沉 +skytils.config.miscellaneous.quality_of_life.fishing_hook_age=釣鉤計時器 +skytils.config.miscellaneous.quality_of_life.tropy_fish_tracker=追蹤Trophy Fish統計 +skytils.config.miscellaneous.quality_of_life.show_trophy_fish_totals=顯示每種Trophy Fish數量 +skytils.config.miscellaneous.quality_of_life.show_total_trophy_fish=顯示Trophy Fish總數 +skytils.config.pets.quality_of_life.autopet_message_hider=隱藏Autopet訊息 +skytils.config.pets.quality_of_life.highlight_active_pet=醒目標示使用中寵物 +skytils.config.pets.quality_of_life.active_pet_highlight_color=使用中寵物的標示顏色 +skytils.config.pets.quality_of_life.highlight_favorite_pets=醒目標示最愛寵物 +skytils.config.pets.quality_of_life.favorite_pet_highlight_color=最愛寵物的標示顏色 skytils.config.pets.quality_of_life.pet_item_confirmation=寵物物品確認 -skytils.config.slayer..current_revenant_rng_meter=當前殭屍獵手RNG里程條 -skytils.config.slayer..current_tarantula_rng_meter=當前蜘蛛獵手RNG里程條 -skytils.config.slayer..current_sven_rng_meter=當前狼獵手RNG里程條 -skytils.config.slayer..current_voidgloom_rng_meter=當前末影人獵手RNG里程條 -skytils.config.slayer..current_inferno_rng_meter=當前烈焰人獵手RNG里程條 -skytils.config.slayer..current_bloodfiend_rng_meter=當前吸血鬼獵手RNG里程條 -skytils.config.slayer.quality_of_life.click_to_open_maddox_menu=點擊打開Maddox菜單 +skytils.config.slayer..current_revenant_rng_meter=當前殭屍Slayer的RNG量表 +skytils.config.slayer..current_tarantula_rng_meter=當前蜘蛛Slayer的RNG量表 +skytils.config.slayer..current_sven_rng_meter=當前狼Slayer的RNG量表 +skytils.config.slayer..current_voidgloom_rng_meter=當前終界使者Slayer的RNG量表 +skytils.config.slayer..current_inferno_rng_meter=當前烈焰使者Slayer的RNG量表 +skytils.config.slayer..current_bloodfiend_rng_meter=當前吸血鬼Slayer的RNG量表 +skytils.config.slayer.quality_of_life.click_to_open_maddox_menu=點選任意處開啟Maddox選單 skytils.config.slayer.general.carry_mode=帶人模式 -skytils.config.slayer.general.use_hits_to_detect_slayer=使用擊中檢測獵手 -skytils.config.slayer.quality_of_life.ping_when_in_atoned_horror_danger_zone=進入殭屍獵手TNT技能危險區時警報 -skytils.config.slayer.quality_of_life.slayer_boss_hitbox=獵手boss碰撞箱 -skytils.config.slayer.quality_of_life.slayer_miniboss_spawn_alert=獵手的Miniboss刷新警報 -skytils.config.slayer.quality_of_life.show_rngesus_meter=顯示RNG里程條 -skytils.config.slayer.quality_of_life.show_slayer_armor_kills=顯示獵手護甲的擊殺數 -skytils.config.slayer.quality_of_life.show_slayer_display=顯示獵手信息 -skytils.config.slayer.quality_of_life.show_slayer_time_to_kill=顯示獵手擊殺時間 -skytils.config.slayer.voidgloom_seraph.hide_others_broken_heart_radiation=隱藏他人末影人獵手的Broken Heart Radiation激光技能 -skytils.config.slayer.voidgloom_seraph.recolor_seraph_boss=給末影人獵手boss上色 -skytils.config.slayer.voidgloom_seraph.seraph_beacon_phase_color=末影人獵手信標存在時顏色 -skytils.config.slayer.voidgloom_seraph.seraph_hits_phase_color=末影人獵手護盾階段顏色 -skytils.config.slayer.voidgloom_seraph.seraph_normal_phase_color=末影人獵手常規階段顏色 -skytils.config.slayer.voidgloom_seraph.show_seraph_display=顯示末影人獵手信息 -skytils.config.slayer.voidgloom_seraph.experimental_yang_glyph_detection=實驗性Yang Glyph信標技能檢測 -skytils.config.slayer.voidgloom_seraph.yang_glyph_ping=Yang Glyph信標技能警報 -skytils.config.slayer.voidgloom_seraph.yang_glyph_ping_on_land=Yang Glyph信標落地警報 -skytils.config.slayer.voidgloom_seraph.highlight_yang_glyph=高亮Yang Glyph信標 -skytils.config.slayer.voidgloom_seraph.point_to_yang_glyph=指向Yang Glyph信標 -skytils.config.slayer.voidgloom_seraph.yang_glyph_highlight_color=Yang Glyph信標高亮的顏色 -skytils.config.slayer.voidgloom_seraph.highlight_nukekebi_fixation_heads=高亮Nukekebi Fixation技能的頭顱 +skytils.config.slayer.general.use_hits_to_detect_slayer=以擊中的攻擊偵測Slayer +skytils.config.slayer.quality_of_life.ping_when_in_atoned_horror_danger_zone=殭屍Slayer的TNT技能危險區警告 +skytils.config.slayer.quality_of_life.slayer_boss_hitbox=Slayer boss碰撞箱 +skytils.config.slayer.quality_of_life.slayer_miniboss_spawn_alert=Slayer Miniboss生成警報 +skytils.config.slayer.quality_of_life.show_rngesus_meter=顯示RNG量表 +skytils.config.slayer.quality_of_life.show_slayer_armor_kills=顯示Slayer盔甲的擊殺數 +skytils.config.slayer.quality_of_life.show_slayer_display=顯示Slayer資訊 +skytils.config.slayer.quality_of_life.show_slayer_time_to_kill=顯示Slayer擊殺時間 +skytils.config.slayer.voidgloom_seraph.hide_others_broken_heart_radiation=隱藏他人終界使者Slayer的Broken Heart Radiation技能光線 +skytils.config.slayer.voidgloom_seraph.recolor_seraph_boss=替終界使者Slayer的boss上色 +skytils.config.slayer.voidgloom_seraph.seraph_beacon_phase_color=終界使者Slayer烽火台存在時顏色 +skytils.config.slayer.voidgloom_seraph.seraph_hits_phase_color=終界使者Slayer護盾階段顏色 +skytils.config.slayer.voidgloom_seraph.seraph_normal_phase_color=終界使者Slayer一般階段顏色 +skytils.config.slayer.voidgloom_seraph.show_seraph_display=顯示終界使者Slayer資訊 +skytils.config.slayer.voidgloom_seraph.experimental_yang_glyph_detection=實驗性Yang Glyph技能偵測 +skytils.config.slayer.voidgloom_seraph.yang_glyph_ping=Yang Glyph烽火台技能警報 +skytils.config.slayer.voidgloom_seraph.yang_glyph_ping_on_land=Yang Glyph烽火台落地警報 +skytils.config.slayer.voidgloom_seraph.highlight_yang_glyph=醒目顯示Yang Glyph烽火台 +skytils.config.slayer.voidgloom_seraph.point_to_yang_glyph=箭頭指向Yang Glyph烽火台 +skytils.config.slayer.voidgloom_seraph.yang_glyph_highlight_color=Yang Glyph烽火台的顏色 +skytils.config.slayer.voidgloom_seraph.highlight_nukekebi_fixation_heads=醒目顯示Nukekebi Fixation技能的頭顱 skytils.config.slayer.voidgloom_seraph.nukekebi_fixation_head_color=Nukekebi Fixation技能的頭顱顏色 -skytils.config.slayer.voidgloom_seraph.show_soulflow_display=顯示靈魂流 -skytils.config.slayer.voidgloom_seraph.low_soulflow_ping=靈魂流不足警報 -skytils.config.slayer.inferno_demonlord.show_totem_display=顯示圖騰技能信息 -skytils.config.slayer.inferno_demonlord.totem_ping=圖騰倒計時警報 -skytils.config.slayer.inferno_demonlord.hide_pacified_blazes=隱藏已平息的烈焰人 -skytils.config.slayer.inferno_demonlord.ping_when_in_inferno_demonlord_fire=進入烈焰人獵手的十字火時警報 -skytils.config.slayer.inferno_demonlord.recolor_demonlord_boss_by_attunement=根據Attunement護盾給烈焰人獵手上色 -skytils.config.slayer..vampire_slayer_one_shot_alert=吸血鬼獵手斬殺提醒 -skytils.config.slayer..twinclaw_alert=雙向爪(Twinclaw)技能警報 -skytils.config.sounds.abilities.disable_cooldown_sounds=禁用冷卻音效 -skytils.config.sounds.abilities.disable_jerrychine_gun_sounds=禁用Jerry槍音效 -skytils.config.sounds.abilities.disable_flower_of_truth_sounds=禁用Flower of Truth音效 -skytils.config.sounds.dungeons.disable_terracotta_sounds=禁用陶土人音效 +skytils.config.slayer.voidgloom_seraph.show_soulflow_display=顯示Soulflow +skytils.config.slayer.voidgloom_seraph.low_soulflow_ping=Soulflow不足警報 +skytils.config.slayer.inferno_demonlord.show_totem_display=顯示Totem技能訊息 +skytils.config.slayer.inferno_demonlord.totem_ping=Totem倒數計時警報 +skytils.config.slayer.inferno_demonlord.hide_pacified_blazes=隱藏不具攻擊性的烈焰使者 +skytils.config.slayer.inferno_demonlord.ping_when_in_inferno_demonlord_fire=烈焰使者Slayer的十字火範圍警報 +skytils.config.slayer.inferno_demonlord.recolor_demonlord_boss_by_attunement=根據Attunement護盾替boss上色 +skytils.config.slayer..vampire_slayer_one_shot_alert=吸血鬼Slayer終結提醒 +skytils.config.slayer..twinclaw_alert=Twinclaw技能警報 +skytils.config.sounds.abilities.disable_cooldown_sounds=停用冷卻音效 +skytils.config.sounds.abilities.disable_jerrychine_gun_sounds=停用Jerry-chine Gun音效 +skytils.config.sounds.abilities.disable_flower_of_truth_sounds=停用Flower of Truth音效 +skytils.config.sounds.dungeons.disable_terracotta_sounds=停用Terracota音效 skytils.config.spam.display.text_shadow=文字陰影 -skytils.config.spam.abilities.implosion_hider=爆炸消息隱藏 -skytils.config.spam.abilities.midas_staff_hider=Midas Staff消息隱藏 -skytils.config.spam.abilities.spirit_sceptre_hider=Spirit Sceptre消息隱藏 -skytils.config.spam.abilities.giant_sword_hider=Giant's Sword消息隱藏 -skytils.config.spam.abilities.livid_dagger_hider=Livid Dagger消息隱藏 -skytils.config.spam.abilities.ray_of_hope_hider=Staff of the Rising Sun消息隱藏 -skytils.config.spam.abilities.mining_ability_hider=挖礦能力消息隱藏 -skytils.config.spam.abilities.mana_use_hider=魔法值使用消息隱藏 -skytils.config.spam.abilities.healing_message_hider=治療消息隱藏 -skytils.config.spam.dungeons.blessing_hider=祝福消息隱藏 -skytils.config.spam.dungeons.blood_key_hider=血房鑰匙消息隱藏 -skytils.config.spam.dungeons.boss_messages_hider=Boss消息隱藏 -skytils.config.spam.dungeons.wither_essence_hider=凋靈精華消息隱藏 -skytils.config.spam.dungeons.undead_essence_hider=亡靈精華消息隱藏 -skytils.config.spam.dungeons.countdown_and_ready_messages_hider=倒計時和準備就緒消息隱藏 -skytils.config.spam.dungeons.dungeon_abilities_messages_hider=地牢能力消息隱藏 -skytils.config.spam.dungeons.mort_messages_hider=Mort消息隱藏 -skytils.config.spam.dungeons.superboom_pickup_hider=Superboom物品拾取消息隱藏 -skytils.config.spam.dungeons.revive_stone_pickup_hider=復活石拾取消息隱藏 -skytils.config.spam.dungeons.wither_key_hider=凋靈鑰匙消息隱藏 -skytils.config.spam.dungeons.tether_hider=奶媽技能綁定消息隱藏 -skytils.config.spam.dungeons.self_orb_pickup_hider=自己拾取治療球消息隱藏 -skytils.config.spam.dungeons.other_orb_pickup_hider=隊員拾取治療球消息隱藏 -skytils.config.spam.dungeons.trap_damage_hider=陷阱傷害消息隱藏 -skytils.config.spam.dungeons.toast_time=懸浮消息時間 -skytils.config.spam.miscellaneous.blocks_in_the_way_hider=被方塊阻擋的消息隱藏 -skytils.config.spam.miscellaneous.cant_use_ability_hider=無法使用能力消息隱藏 -skytils.config.spam.miscellaneous.combo_hider=連擊消息隱藏 -skytils.config.spam.miscellaneous.autorecombobulator_hider=Auto Recombobulator消息隱藏 -skytils.config.spam.miscellaneous.compact_building_tools=合併建築工具消息 -skytils.config.spam.miscellaneous.compact_mining_powder_gain=合併挖礦粉塵獲取消息 -skytils.config.spam.miscellaneous.cooldown_hider=冷卻消息隱藏 -skytils.config.spam.miscellaneous.no_enemies_nearby_hider=附近無敵人消息隱藏 -skytils.config.spam.miscellaneous.out_of_mana_hider=魔法值耗盡消息隱藏 -skytils.config.spam.miscellaneous.profile_message_hider=存檔消息隱藏 -skytils.config.spam.miscellaneous.spook_message_hider=萬聖節消息隱藏 -skytils.config.spam.fishing.blessing_enchant_hider=祝福附魔消息隱藏 -skytils.config.spam.fishing.blessed_bait_hider=祝福誘餌消息隱藏 -skytils.config.spam.fishing.sea_creature_catch_hider=海洋生物捕獲消息隱藏 -skytils.config.spam.fishing.legendary_sea_creature_catch_hider=傳奇海洋生物捕獲消息隱藏 -skytils.config.spam.fishing.good_fishing_treasure_hider=較好釣魚寶藏消息隱藏 -skytils.config.spam.fishing.great_fishing_treasure_hider=極好釣魚寶藏消息隱藏 -skytils.config.spam.miscellaneous.compact_hider=合成附魔消息隱藏 -skytils.config.spam.miscellaneous.pristine_hider=Pristine消息隱藏 -skytils.config.spam.miscellaneous.wind_direction_hider=風向消息隱藏 -skytils.config.general=通用 -skytils.config.dungeons=地牢 +skytils.config.spam.abilities.implosion_hider=隱藏Implosion技能訊息 +skytils.config.spam.abilities.midas_staff_hider=隱藏Midas Staff訊息 +skytils.config.spam.abilities.spirit_sceptre_hider=隱藏Spirit Sceptre訊息 +skytils.config.spam.abilities.giant_sword_hider=隱藏Giant's Sword訊息 +skytils.config.spam.abilities.livid_dagger_hider=隱藏Livid Dagger訊息 +skytils.config.spam.abilities.ray_of_hope_hider=隱藏Staff of the Rising Sun訊息 +skytils.config.spam.abilities.mining_ability_hider=隱藏挖礦技能訊息 +skytils.config.spam.abilities.mana_use_hider=隱藏魔力使用訊息 +skytils.config.spam.abilities.healing_message_hider=隱藏治療訊息 +skytils.config.spam.dungeons.blessing_hider=隱藏祝福訊息 +skytils.config.spam.dungeons.blood_key_hider=隱藏血之房間鑰匙訊息 +skytils.config.spam.dungeons.boss_messages_hider=隱藏Boss台詞 +skytils.config.spam.dungeons.wither_essence_hider=隱藏凋零精華訊息 +skytils.config.spam.dungeons.undead_essence_hider=隱藏亡靈精華訊息 +skytils.config.spam.dungeons.countdown_and_ready_messages_hider=隱藏倒數計時和準備就緒訊息 +skytils.config.spam.dungeons.dungeon_abilities_messages_hider=隱藏Dungeon能力訊息 +skytils.config.spam.dungeons.mort_messages_hider=隱藏Mort訊息 +skytils.config.spam.dungeons.superboom_pickup_hider=隱藏Superboom物品拾取訊息 +skytils.config.spam.dungeons.revive_stone_pickup_hider=隱藏復活石拾取訊息 +skytils.config.spam.dungeons.wither_key_hider=隱藏凋零鑰匙訊息 +skytils.config.spam.dungeons.tether_hider=隱藏Healer技能綁定訊息 +skytils.config.spam.dungeons.self_orb_pickup_hider=隱藏自己拾取治療球訊息 +skytils.config.spam.dungeons.other_orb_pickup_hider=隱藏隊員拾取治療球訊息 +skytils.config.spam.dungeons.trap_damage_hider=隱藏陷阱傷害訊息 +skytils.config.spam.dungeons.toast_time=彈出式通知(Toast)持續時間 +skytils.config.spam.miscellaneous.blocks_in_the_way_hider=隱藏被方塊阻擋的訊息 +skytils.config.spam.miscellaneous.cant_use_ability_hider=隱藏無法使用技能訊息 +skytils.config.spam.miscellaneous.combo_hider=隱藏連擊訊息 +skytils.config.spam.miscellaneous.autorecombobulator_hider=隱藏Auto Recombobulator訊息 +skytils.config.spam.miscellaneous.compact_building_tools=合併建築工具訊息 +skytils.config.spam.miscellaneous.compact_mining_powder_gain=合併挖礦粉塵 (Powder) 取得訊息 +skytils.config.spam.miscellaneous.cooldown_hider=隱藏冷卻訊息 +skytils.config.spam.miscellaneous.no_enemies_nearby_hider=隱藏附近沒有敵人之訊息 +skytils.config.spam.miscellaneous.out_of_mana_hider=隱藏魔力耗盡訊息 +skytils.config.spam.miscellaneous.profile_message_hider=隱藏存檔 (Profile) 訊息 +skytils.config.spam.miscellaneous.spook_message_hider=隱藏Spooky節訊息 +skytils.config.spam.fishing.blessing_enchant_hider=隱藏Blessing附魔訊息 +skytils.config.spam.fishing.blessed_bait_hider=隱藏Blessed Bait訊息 +skytils.config.spam.fishing.sea_creature_catch_hider=隱藏釣起海洋生物訊息 +skytils.config.spam.fishing.legendary_sea_creature_catch_hider=隱藏傳說一釣訊息 +skytils.config.spam.fishing.good_fishing_treasure_hider=隱藏GOOD CATCH訊息 +skytils.config.spam.fishing.great_fishing_treasure_hider=隱藏GREAT CATCH訊息 +skytils.config.spam.miscellaneous.compact_hider=隱藏Compact附魔訊息 +skytils.config.spam.miscellaneous.pristine_hider=隱藏Pristine訊息 +skytils.config.spam.miscellaneous.wind_direction_hider=隱藏Gone with the Wind風向訊息 +skytils.config.general=一般 +skytils.config.dungeons=Dungeon skytils.config.miscellaneous=雜項 -skytils.config.events=事件 +skytils.config.events=活動 skytils.config.farming=農業 skytils.config.kuudra=Kuudra skytils.config.mining=挖礦 skytils.config.pets=寵物 -skytils.config.slayer=獵手 +skytils.config.slayer=Slayer skytils.config.sounds=音效 -skytils.config.spam=消息 +skytils.config.spam=訊息過濾 skytils.config.general.api=API skytils.config.general.command_aliases=指令別名 skytils.config.general.local_api=本地API @@ -427,44 +427,44 @@ skytils.config.general.other=其他 skytils.config.general.reparty=重新組隊 skytils.config.general.hypixel=Hypixel skytils.config.general.updates=更新 -skytils.config.dungeons.fixes=修復 +skytils.config.dungeons.fixes=修正 skytils.config.dungeons.hud=HUD skytils.config.dungeons.miscellaneous=雜項 -skytils.config.dungeons.quality_of_life=生活質量 -skytils.config.dungeons.party_finder=隊伍尋找 +skytils.config.dungeons.quality_of_life=改善遊戲體驗 +skytils.config.dungeons.party_finder=隊伍搜尋器 skytils.config.dungeons.score_calculation=分數計算 skytils.config.miscellaneous.brewing=釀造 skytils.config.miscellaneous.items=物品 -skytils.config.dungeons.solvers=解密器 -skytils.config.dungeons.tank_helper_tools=坦克助手工具 -skytils.config.dungeons.terminal_solvers=終端解密器 +skytils.config.dungeons.solvers=解答器 +skytils.config.dungeons.tank_helper_tools=Tank輔助工具 +skytils.config.dungeons.terminal_solvers=終端機 (Terminal) 解答器 skytils.config.events.mayor_jerry=市長Jerry -skytils.config.events.mythological=神話 -skytils.config.events.spooky=萬聖節 +skytils.config.events.mythological=神話儀式 (Mythological Ritual) +skytils.config.events.spooky=Spooky skytils.config.events.technoblade=Technoblade -skytils.config.farming.garden=花園 -skytils.config.farming.quality_of_life=生活質量 -skytils.config.farming.solvers=解密器 +skytils.config.farming.garden=農園 +skytils.config.farming.quality_of_life=改善遊戲體驗 +skytils.config.farming.solvers=解答器 skytils.config.kuudra.performance=性能 skytils.config.kuudra.price_checking=價格檢查 -skytils.config.mining.quality_of_life=生活質量 -skytils.config.mining.solvers=解密器 -skytils.config.mining.crystal_hollows=水晶礦洞 +skytils.config.mining.quality_of_life=改善遊戲體驗 +skytils.config.mining.solvers=解答器 +skytils.config.mining.crystal_hollows=Crystal Hollows skytils.config.miscellaneous.chat_tabs=聊天頻道 -skytils.config.miscellaneous.fixes=修復 +skytils.config.miscellaneous.fixes=修正 skytils.config.miscellaneous.item_rarity=物品稀有度 -skytils.config.miscellaneous.minions=僕從 -skytils.config.miscellaneous.quality_of_life=生活質量 +skytils.config.miscellaneous.minions=Minion +skytils.config.miscellaneous.quality_of_life=改善遊戲體驗 skytils.config.miscellaneous.other=其他 -skytils.config.pets.quality_of_life=生活質量 -skytils.config.slayer.quality_of_life=生活質量 -skytils.config.slayer.general=通用 -skytils.config.slayer.voidgloom_seraph=末影人獵手 -skytils.config.slayer.inferno_demonlord=烈焰人獵手 -skytils.config.sounds.abilities=能力 -skytils.config.sounds.dungeons=地牢 +skytils.config.pets.quality_of_life=改善遊戲體驗 +skytils.config.slayer.quality_of_life=改善遊戲體驗 +skytils.config.slayer.general=一般 +skytils.config.slayer.voidgloom_seraph=終界使者Slayer +skytils.config.slayer.inferno_demonlord=烈焰使者Slayer +skytils.config.sounds.abilities=技能 +skytils.config.sounds.dungeons=Dungeon skytils.config.spam.display=顯示 -skytils.config.spam.abilities=能力 -skytils.config.spam.dungeons=地牢 +skytils.config.spam.abilities=技能 +skytils.config.spam.dungeons=Dungeon skytils.config.spam.miscellaneous=雜項 -skytils.config.spam.fishing=釣魚 \ No newline at end of file +skytils.config.spam.fishing=釣魚 From 0b188765480b5b174b18d52ffd319a4c879f260a Mon Sep 17 00:00:00 2001 From: My-Name-Is-Jeff <37018278+My-Name-Is-Jeff@users.noreply.github.com> Date: Thu, 13 Jun 2024 13:23:26 -0400 Subject: [PATCH 46/46] version: 1.10.0-pre9 --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 3dcb502b4..897fd582b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,7 +34,7 @@ plugins { signing } -version = "1.10.0-pre8" +version = "1.10.0-pre9" group = "gg.skytils" repositories {